最近開發一個小程式遇到一個需求需要實現分散式事務管理
業務需求
使用者在使用小程式的過程中可以檢視景點,對景點地區或者城市標記是否想去,那麼需要統計一個地點被標記的人數,以及記錄某個使用者對某個地點是否標記為想去,用兩個表儲存資料,一個地點表記錄改地點被標記的次數,一個使用者意向表記錄某個使用者對某個地點是否標記為想去。由於可能有多個使用者同時標記一個地點,每個使用者在前端點選想去按鈕之後,後臺接收到請求,從資料庫查詢某個城市的標記人數,再加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);
}
}
總結
高併發情況下,分散式事務很容易出問題,要對各種情況分析是否可能出問題,並要對所有可能出問題的情況做充分的測試才能保證程式健壯。