目錄
一、本地鎖存在的問題
二、redis實現分布式鎖原理
三、使用示例
四、鎖誤刪問題
?解決思路
獲取鎖和釋放鎖代碼優化
五、鎖釋放的原子性問題
解決思路(Lua腳本)
使用流程
總結
? ? ? ? 大家好,我是千語。上期給大家講了使用悲觀鎖來解決“一人一單”的并發場景。但上期使用的是一個本地鎖,本地鎖在集群模式下會失效。具體可以看一下我上一篇博客。
【并發問題】一人一單(悲觀鎖解決)-CSDN博客
一、本地鎖存在的問題
在集群模式下,該項目會啟動多個實例,且每個實例都會有各種的jvm。我們上面使用到的鎖其實都是本地鎖,所以就可能會出現這樣的情況:
張三在進行并發地判斷自己是否滿足一人一單時,第一個請求被分配到了實例A,獲取鎖并判斷到數據庫中還沒有改商品的訂單,可以搶購,但當還沒有完全提交事務到數據庫時,即使還沒有釋放鎖。
張三發送第二個請求被分配到了實例B,那么用戶嘗試獲取鎖時,是可以獲取到的。然后判斷到數據庫沒有訂單,可以搶單的操作,這樣又造成了一個用戶搶到了多個訂單的操作。
解析:因為每個實例都會有自己的JVM,而JVM里面都會有自己的鎖監視器,并且每個實例的鎖都是存儲在它自己的jvm里面的,所以請求分配到不同的實例,鎖監視器監視到的鎖都是打開的狀態。也就是說我們上面應用鎖的方式只是在單機的情況下適用,集群模式下就不適用了。
二、redis實現分布式鎖原理
? ? ? ? 原理就是使用redis的setnx命令,這個命令是給redis里面set值,但是只有這個鍵不存在的時候才set,所以我們要獲取鎖時,setnx一個固定的鍵,獲取鎖成功;當其他線程也想要獲取鎖時,也使用setnx命令,這時候是set不到的,所以這個線程就獲取鎖失敗。當業務執行完釋放鎖時,就把這個鍵刪除就可以了。
圖例:
三、使用示例
@Component
public class RedisLock {@Autowiredprivate RedisTemplate<String, String> redisTemplate;/*** 嘗試獲取分布式鎖* @param lockKey 鎖的鍵* @param expireTime 過期時間* @param timeUnit 時間單位* @return 獲取鎖成功與否*/public String tryLock(String lockKey, long expireTime, TimeUnit timeUnit) {// 使用setIfAbsent方法嘗試獲取鎖(對應Redis的SETNX命令)Boolean locked = redisTemplate.opsForValue().setIfAbsent(lockKey, lockValue, expireTime, timeUnit);//設置鎖超時時間,避免死鎖return locked != null && locked; //set成功表明獲取鎖成功}/*** 釋放分布式鎖* @param lockKey 鎖的鍵* @return 是否釋放成功*/public boolean releaseLock(String lockKey) {return redisTemplate.delete(lockKey)}
}
業務中實際加鎖操作:
public String lockTest(){String lockKey = "product_stock_lock";try {// 嘗試獲取鎖,超時時間10秒,鎖持有時間30秒lockValue = redisLockHelper.tryLock(lockKey, 30, TimeUnit.SECONDS);if (lockValue != null) {// 獲取鎖成功,執行業務邏輯System.out.println("獲取鎖成功,處理庫存扣減...");// 模擬業務處理Thread.sleep(5000); return "庫存扣減成功";} else {// 獲取鎖失敗return "系統繁忙,請稍后重試";}} catch (InterruptedException e) {Thread.currentThread().interrupt();return "操作被中斷";} finally {// 釋放鎖(只有持有鎖的線程才能釋放)if (lockValue != null) {boolean released = redisLockHelper.releaseLock(lockKey, lockValue);System.out.println("鎖釋放結果: " + released);}}}
四、鎖誤刪問題
? ? ? ? 在上述的使用示例當中,實際上會存在鎖誤刪的問題。具體如下:
- 線程1獲取鎖成功,執行業務代碼后阻塞,未執行到手動釋放鎖的操作,鎖超時后自動釋放了
- 由于鎖超時被釋放,線程2獲取鎖成功,執行業務
- 線程1阻塞過后,繼續執行任務,執行了釋放鎖操作。但此時鎖其實是線程2的,由于沒有做判斷,線程1執行了釋放鎖的操作。
- 由于鎖已經被線程1釋放,線程3可以獲取鎖,執行業務。
- 結果:線程2和線程3都同時在執行了只能單個線程執行的業務。
圖例:
?解決思路
獲取鎖時,判斷一下標識是否一致;
在setnx時,value的值可以設置成當前線程的name或者
id。因為線程id在jvm里面是自增的,所以在集群模式下,多個jvm可能會存在id相同的線程,所以也是會沖突的,所以id不可行,往下看。
所以可以使用uuid+線程id作為鎖的標識
當要釋放鎖時,先獲取鎖的值,如果是自己當前的線程id,再進行釋放鎖
獲取鎖和釋放鎖代碼優化
@Component
public class RedisLockHelper {@Autowiredprivate RedisTemplate<String, String> redisTemplate;//生成當前鎖持有者的唯一標識的uuid前綴private static final String ID_PREFIX= UUID.randomUUID().toString(true) + "-";/*** 嘗試獲取分布式鎖* @param lockKey 鎖的鍵* @param expireTime 過期時間* @param timeUnit 時間單位* @return 鎖的唯一標識,獲取失敗時為null*/public String tryLock(String lockKey, long expireTime, TimeUnit timeUnit) {// 使用UUID前綴+當前線程id作為鎖持有者的唯一標識String lockValue = ID_PREFIX + Thread.currentThread().getid();// 使用setIfAbsent方法嘗試獲取鎖(對應Redis的SETNX命令)Boolean locked = redisTemplate.opsForValue().setIfAbsent(lockKey, lockValue, expireTime, timeUnit);return locked != null && locked ? lockValue : null;}/*** 釋放分布式鎖* @param lockKey 鎖的鍵* @return 是否釋放成功*/public boolean releaseLock(String lockKey) {//獲取當前線程的標識String currentThreadLock = ID_PREFIX + Thread.currentThread().getid();// 獲取分布式鎖內的鎖標識String lockValue = redisTemplate.opsForValue().get(lockKey) //釋放鎖時,先判斷該鎖是不是當前線程持有的 if(currentThreadLock.equals(lockValue)) {//如果當前線程是鎖的持有者,就釋放鎖return redisTemplate.delete(lockKey);}else{return false;}}
}
?業務層使用鎖的代碼不需要修改
五、鎖釋放的原子性問題
上一個問題是執行業務時線程阻塞,阻塞結束后誤刪了鎖。
所以我們在釋放鎖前先判斷一下標識,看是否是當前線程的鎖再釋放就可以解決
但是,當我們判斷完標識是一致后,線程1在進行釋放鎖之前被阻塞了(由于這兩者不是原子性)
等到鎖過期,其他線程成功獲取鎖執行業務,那么線程1又誤刪了鎖:
圖例
解決思路(Lua腳本)
使用Lua腳本,在腳本里面寫一系列操作,然后使用redis客戶端調用該腳本,這些操作就會一次性執行,滿足原子性。
使用流程
(1)創建并填寫Lua腳本文件:
注意:Lua腳本是使用lua語言來寫的。具體可以去看一下語法內容,下面只給出一種解決思路和大概的解決流程。后續可以使用redission來簡化這些操作
(2)讀取lua腳本,形成一個RedisScript,便于后續調用api
(3)執行Lua腳本,釋放鎖
(4)鎖使用:
業務中使用鎖的方法都不需要邊
總結
- 分布式鎖利用set nx ex的原理。(set nx的互斥性,ex保證超時釋放鎖,避免死鎖)
- 釋放鎖時要看看鎖是不是該線程的持有者,避免誤刪
- 使用Lua腳本滿足一組操作的原子性