鎖和事務衝突

我使用的RedLock做分佈式鎖管理,用spring註解事務管理。
在實現過程中遇到如下兩個映像深刻的問題:
1、分佈式鎖與spring註解事務共用產生的問題
2、鎖在事務提交前超時問題

使用分佈式鎖RedLock及spring事務實現

public markScenicSpot(){
  //設置鎖爲destId
   RLock lock = redisson.getLock("Afanti_markScenicSpot_updateCountwantAndCountbeenLock_" + ID);
   //嘗試獲取鎖
   long lockTimeOut = 30; //持有鎖超時時間
   **boolean success = lock.tryLock(5, lockTimeOut, TimeUnit.SECONDS);**
   if (success) {
      try {
          //業務邏輯實現
      }catch (Exception e){
          throw e;
      } finally{
          //釋放鎖
          **lock.unlock();**
      }
   } else {
       log.error("獲取鎖失敗!更新失敗!");
       throw new BizException(ErrorCodeEnum.PROCESS_DATA_ERROR);
   }
 }

問題:高併發是鎖沒有生效 

spring註解事務@Transactional和分佈式鎖一起使用的問題

這是因爲@Transactional是通過方法是否拋出異常來判斷事務是否回滾還是提交,此時方法已經結束。但是我們必須在方法結束之後釋放鎖,因此在釋放鎖之後,此時事務還沒提交,由於鎖已經釋放,其他進程可以獲得鎖,並從數據庫查詢地點標記數,但是此時前一個進程沒有提交數據。該進程查到的數據不是最新的數據。儘管這個過程只要很短的時間(我實際測試過程中這個過程只要幾毫秒),但是高併發的情況還是會出問題。

解決1:可以手動控制事務的提交,可以控制在事務提交後釋放鎖

RLock lock = redisson.getLock("Afanti_markScenicSpot_updateCountwantAndCountbeenLock_" + ID);
   //嘗試獲取鎖
   long lockTimeOut = 30; //持有鎖超時時間
   boolean success = lock.tryLock(5, lockTimeOut, TimeUnit.SECONDS);
   if(success){
      **DefaultTransactionDefinition def = new DefaultTransactionDefinition();
      def.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRED); // 事物隔離級別
      TransactionStatus status = transactionManager.getTransaction(def); // 獲得事務狀態**
      try {
          //業務邏輯實現
          //......
          **//提交事務
          transactionManager.commit(status);**
      }catch (Exception e){
          **//回滾事務
          transactionManager.rollback(status);**
      } finally{
          //釋放鎖
          lock.unlock();
      }
   } else {
       log.error("獲取鎖失敗!更新失敗!");
       throw new BizException(ErrorCodeEnum.PROCESS_DATA_ERROR);
   }
 }

問題:鎖超時事物異常 

鎖超時問題
在進行手動事務管理之後,解決的同步問題。但是出現另外一個問題,鎖超時但是事務仍未提交。由於此時當前進程鎖超時但是沒有提交,此時其他進程可以獲得鎖並從數據庫查詢目的地標記數,但是不是更新之後的數據,取得的數據有誤。

解決2:針對鎖超時的情況,只需要當前進程提交之前增加一個判斷,判斷是否超時,如果超時拋出異常退出即可。
增加如下代碼:

public markScenicSpot(){
   //設置鎖爲destId
   RLock lock = redisson.getLock("Afanti_markScenicSpot_updateCountwantAndCountbeenLock_" + ID);
   //嘗試獲取鎖
   long lockTimeOut = 30; //持有鎖超時時間
   boolean success = lock.tryLock(5, lockTimeOut, TimeUnit.SECONDS);
   **//獲取鎖時間
   long getLockTime=System.currentTimeMillis();**
   if(success){
      //事務管理
      DefaultTransactionDefinition def = new DefaultTransactionDefinition();
      def.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRED); // 事物隔離級別
      TransactionStatus status = transactionManager.getTransaction(def); // 獲得事務狀態
      try {
          //業務邏輯實現
          //......
          //提交事務,判斷鎖是否超時
          **if(System.currentTimeMillis()-getLockTime<lockTimeOut*1000){
            transactionManager.commit(status);
            log.info("提交事務");
          } else {
            log.error("異常:程序執行時間過長,鎖超時!");
            throw new BizException(ErrorCodeEnum.PROCESS_DATA_ERROR);
          }**
      }catch (Exception e){
          //回滾事務
          transactionManager.rollback(status);
      } finally{
          //釋放鎖
          lock.unlock();
      }
   } else {
       log.error("獲取鎖失敗!更新失敗!");
       throw new BizException(ErrorCodeEnum.PROCESS_DATA_ERROR);
   }
 }

2、分佈式鎖在事務提交前進行釋放的問題:

事務是在service方法中,將釋放鎖的代碼上提到controller層進行處理,這樣就是事務提交後再釋放鎖。

 

可以通過拆分一個Service的事務、鎖的兩部分工作,拆成2個Service,

Controller調用第一個Service(加鎖、釋放鎖),第一個Service再調用第二個Service(事務控制部分)

示例,修改前

HelloController
   --HelloService
               事務開始
                 加鎖
                     // 業務代碼
                     解鎖
                 事務結束

示例,修改後

HelloController
   --LockService
            加鎖
              --HelloService
                   事務開始
                         // 業務代碼
                    事務結束
            解鎖

 這個還是比較靠譜

 

根據事例總解如下:

 

單機裏面,完美解決了鎖與事務

一、使用鎖的原因分析:

1、使用鎖的目的

------------多個外部線程同時來競爭使用同一資源時,會彼此影響,導致混亂

------------鎖的目的,將資源的使用做排它性處理,使同一時間,僅一個線程能訪問資源

2、並不是所有的資源,都無法同時服務多個線程 ------ 比如,無狀態的資源

3、無成員變量/成員變量不存在變化的類---- 就是無狀態類 ----- 這種類是線程安全的

4、有狀態的對象,也不一定是不安全的

---------- 如果狀態變化是原子的(即沒有中間變遷過程,變化不需要時間,沒有中間態) ---- 那麼它一樣是線程安全的

5、重要的概念,動作的原子性

6、總結:鎖的本質

鎖要解決的問題是 ------- 資源數據會不一致

鎖要達成的目標是 ------- 讓資源使用起來,像原子性一樣

鎖達成目標的手段 ------- 讓使用者訪問資源時,只能排隊,一個一個地去訪問資源

二、在單機應用裏,JVM可以通過以下工具,可協調資源像原子性一樣操作

1、sychronized ------ java語言天生支持

2、lock ---- jdk有接口標準

 

三、分佈式環境下,如何協調資源達到原子性的操作?

1、sychronized / lock 這些java天然的實現,無法跨JVM發揮作用

2、只得去尋求分佈式環境裏,大家都公認的服務來做見證人,以協調資源

3、常見的公證人 ------》 mysql/zk/file/redis

4、目標 ----- 通過公證人發出信號,來協調分佈式的訪問者,排隊訪問資源

5、條件 ----- 任何一個能夠提供【是/否】信號量的事物,都可以來做公證人

6、陷阱 ----- 發出鎖信號量的動作,本身必須是原子性的

7、mysql來充當公證人,利用的是一條sql語句執行的成功/失敗,是原子的,流程如下:

8、redis來充當公證人,利用的其 setnx指令的成功/失敗,是原子的,流程如下:

9、爲了防止線程宕機,造成鎖死在那裏擋道,需要給鎖認定一個有效期限,

------此期限的自動失效解鎖,與線程的主動解鎖之間,會存在衝突,reids的解鎖流程必須考慮這一點:

10、上圖的解鎖邏輯雖然是正確的,但因爲整個動作不是原子的,因爲不安全。需要改爲lua腳本來執行

11、lua 腳本爲什麼是原子性的

----- redis是單線程執行指令的,因此內部不存在線程競爭

(1)服務器A依次發送了ab指令到redis

(2)服務器B依次發送了cd指令到redis

(3)兩臺機器同向redis發送的四條指令,最終在指令隊列裏順序是:acbd

(4)可以看到,服務器A發送的ab兩條指令,中間穿插了c指令,破壞了其完整性,因此,ab兩條指令不是原子的

(5)lua腳本,被放進隊列時,ab指令是放在一起的,因爲ab會順序一起被執行,成爲了原子性動作

四、事務的概念

1、鎖的問題   ----- 多對一的問題 ------ 是多個線程同時訪問同一個資源,造成資源狀態不一致

2、事務的問題 ----- 一對多的問題 ----- 是一個線程進數據庫,操作多條sql,其中,某條sql的失敗,致使整個業務失去意義;

3、數據庫中事務的實現方式:

------------------ service執行一個操作,要執行N條sql( 一條sql 是一個原子性操作)

--------- 數據庫內部,如何實現事務?

--------- 所有的sql執行完畢之前,結果都以副本形式存在,如下圖

------- commit操作 ------ 業務線程向數據庫發指令 ----- 把副本轉正

------ roback操作 ------- 把副本丟掉

4、jdbc規範裏,定義這樣標準---- 事務管理器

事務管理器定義三個標準接口,即:

1、啓動事務(啓動副本),

2、副本轉正

3、副本丟棄

 

五、分佈式事務

1、分佈式事務,是指多臺數據庫的執行sql,也想要達到一致性的標準,即:多臺一起commit或rollback

2、參照單機事務的模型,分佈式事務的思路延襲,也想通過三個標準接口的模式來完成(啓副本/commit/rollback)

3、按這個思路, X/Open組織提出了分佈式事務的規範 ----- XA

4、XA的核心,便是全局事務,通過XA二階段提交協議,與各分佈式數據交互,分準備與提交兩個階段,如下圖:

課程回顧:

1、鎖的本質,資源的操作不是原子 -------- 鎖目標,讓一系列操作,一次性做完。排隊

2、分佈式環境下,一切能夠發出兩個信號量事物都能夠做鎖 ----- 做鎖要求:加鎖/解鎖兩個動作,一定是原子的

3、mysql來做鎖 ------- 一條sql的執行,是原子的

4、redis有做鎖 -------- setnx操作,是原子的

5、redis要做安全的鎖,----- 加鎖進程死掉,--------  有效期使用解鎖過程複雜化--鎖判斷----lua腳本來保證原子性

6、鎖 ---- 多個線程操作一個資源 ; 事務 ----- 一個線程,操作多個資源問題。

7、事務 ----- ACID ----- 啓事務/commit/rollback

8、X/open組織提出分佈式事務規範 ---- XA

 

本堂課內容:

1、事務執行的中間態說明:

----- sql執行後,尚未提交 ,此時對應的數據狀態:

                  -----正本數據(原來的數據)   ----- 鎖定狀態-------允許其它線程讀不允許改(此時易出現幻讀)

                  -----副本數據(sql執行結果)  ------ 隔離狀態 ------其它線程是無法接觸到的-------(可設置事務隔離級別,使其可讀)未生效的數據 ---- 髒讀

                  ----- commit/rollback時出錯     ------- 不能執行機率非常低

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章