我使用的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時出錯 ------- 不能執行機率非常低