線程1現在持有鎖之后,在執行業務邏輯過程中,他正準備刪除鎖,而且已經走到了條件判斷的過程中,比如他已經拿到了當前這把鎖確實是屬于他自己的,正準備刪除鎖,但是此時他的鎖到期了,那么此時線程2進來,但是線程1他會接著往后執行,當他卡頓結束后,他直接就會執行刪除鎖那行代碼,相當于條件判斷并沒有起到作用,這就是刪鎖時的原子性問題,之所以有這個問題,是因為線程1的拿鎖,比鎖,刪鎖,實際上并不是原子性的,我們要防止剛才的情況發生
Redis提供了Lua腳本功能,在一個腳本中編寫多條Redis命令,確保多條命令執行時的原子性。Lua是一種編程語言,它的基本語法大家可以參考網站:https://www.runoob.com/lua/lua-tutorial.html,這里重點介紹Redis提供的調用函數,我們可以使用lua去操作redis,又能保證他的原子性,這樣就可以實現拿鎖比鎖刪鎖是一個原子性動作了,作為Java程序員這一塊并不作一個簡單要求,并不需要大家過于精通,只需要知道他有什么作用即可。
接下來就是我們之前釋放鎖的邏輯:
釋放鎖的業務流程是這樣的
? 1、獲取鎖中的線程標示
? 2、判斷是否與指定的標示(當前線程標示)一致
? 3、如果一致則釋放鎖(刪除)
? 4、如果不一致則什么都不做
如果用Lua腳本來表示則是這樣的:
最終我們操作redis的拿鎖比鎖刪鎖的lua腳本就會變成這樣
-- 這里的 KEYS[1] 就是鎖的key,這里的ARGV[1] 就是當前線程標示
-- 獲取鎖中的標示,判斷是否與當前線程標示一致
if (redis.call('GET', KEYS[1]) == ARGV[1]) then-- 一致,則刪除鎖return redis.call('DEL', KEYS[1])
end
-- 不一致,則直接返回
return 0
下面就是在java中如何調用,我們的RedisTemplate中,可以利用execute方法去執行lua腳本。
Java代碼
private static final DefaultRedisScript<Long> UNLOCK_SCRIPT;static {UNLOCK_SCRIPT = new DefaultRedisScript<>();UNLOCK_SCRIPT.setLocation(new ClassPathResource("unlock.lua"));UNLOCK_SCRIPT.setResultType(Long.class);}@Overridepublic void unLock() {// 調用lua腳本stringRedisTemplate.execute(UNLOCK_SCRIPT,Collections.singletonList(KEY_PREFIX + name),Collections.singletonList(ID_PREFIX + Thread.currentThread().getId()));}
小總結:
基于Redis的分布式鎖實現思路:
- 利用set nx ex獲取鎖,并設置過期時間,保存線程標示
- 釋放鎖時先判斷線程標示是否與自己一致,一致則刪除鎖
- 特性:
- 利用set nx滿足互斥性
- 利用set ex保證故障時鎖依然能釋放,避免死鎖,提高安全性
- 利用Redis集群保證高可用和高并發特性
- 特性:
測試邏輯:
第一個線程進來,得到了鎖,手動刪除鎖,模擬鎖超時了,其他線程會執行lua來搶鎖,當第一天線程利用lua刪除鎖時,lua能保證他不能刪除他的鎖,第二個線程刪除鎖時,利用lua同樣可以保證不會刪除別人的鎖,同時還能保證原子性。