分佈式鎖和spring事務管理

最近開發一個小程序遇到一個需求需要實現分佈式事務管理

業務需求

用戶在使用小程序的過程中可以查看景點,對景點地區或者城市標記是否想去,那麼需要統計一個地點被標記的人數,以及記錄某個用戶對某個地點是否標記爲想去,用兩個表存儲數據,一個地點表記錄改地點被標記的次數,一個用戶意向表記錄某個用戶對某個地點是否標記爲想去。由於可能有多個用戶同時標記一個地點,每個用戶在前端點擊想去按鈕之後,後臺接收到請求,從數據庫查詢某個城市的標記人數,再加1,然後更新到數據庫。從數據庫查詢標記人數,再加1,然後更新到數據庫這個過程數據庫數據必須加鎖,一次只能一個進程處理。否則數據會出現不同步問題

我使用的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);
   }
 }

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

1、spring註解事務@Transactional和分佈式鎖不能一起使用
這是因爲@Transactional是通過方法是否拋出異常來判斷事務是否回滾還是提交,此時方法已經結束。但是我們必須在方法結束之前釋放鎖,
因此在釋放鎖之後,此時還沒提交,由於鎖已經釋放,其他進程可以獲得鎖,並從數據庫查詢地點標記數,但是此時前一個進程沒有提交數據。該進程查到的數據不是最新的數據。
這個問題我排查的時候花了很久,因爲鎖釋放和提交事務之間只要幾毫秒的時間,之前一直以爲這麼短的時間不可能是這裏的問題,有懷疑過但是自己又放棄了
儘管這個過程只要很短的時間(我實際測試過程中這個過程只要幾毫秒),但是高併發的情況還是會出問題。

解決1:

由於不能使用註解事務,我改爲手動事務管理,增加如下代碼。

 public markScenicSpot(){
   //設置鎖爲destId
   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);
   }
 }

問題:鎖超時事物異常

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

解決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);
   }
 }

總結

高併發情況下,分佈式事務很容易出問題,要對各種情況分析是否可能出問題,並要對所有可能出問題的情況做充分的測試才能保證程序健壯。

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