出乎意料的現象 我們有一張測試表 t1,表中有一些數據,當 session1 開啟一個事務,並執行了 select for update 操作後仍未提交事務,在併發事務(如 session2)開啟事務並行執行一些操作會有不同的鎖現象,表現在: select for update 會出現鎖等待 del ...
分散式鎖是控制分散式系統間同步訪問共用資源的一種方式,其可以保證共用資源在併發場景下的數據一致性。
1. 工作原理
當有多個線程要訪問某一個共用資源(DBMS中的數據或Redis中的數據,或共用文件等)時,為了達到協調多個線程的同步訪問,此時就需要使用分散式鎖了。
為了達到同步訪問的目的,規定:讓這些線程在訪問共用資源之前先要獲得一個令牌token,只有具有令牌的線程才可以訪問共用資源。這個令牌就是通過各種技術實現的分散式鎖。而這個分散式鎖是一種”互斥資源“,即只有一個。只要有線程搶到了鎖,那麼其它線程只能等待,直到鎖被釋放或等待超時。
2. Redisson
2.1 原理
Redisson 內部使用Lua腳本實現了對可重入鎖的添加、重入、續約(續命)、釋放。Redisson需要用戶為鎖指定一個key,但無需為鎖指定過期時間,因為它有預設過期時間(當然,也可指定)。由於該鎖具有“可重入”功能,所以Redisson會為該鎖生成一個計數器,記錄一個線程重入鎖的次數。
2.2. 加鎖與釋放鎖
其核心是通過Lua腳本實現
加鎖的Lua腳本
//exists',KEYS[1])==0 不存在,沒鎖 "if (redis.call('exists',KEYS[1])==0) then "+ --看有沒有鎖 // 命令:hset,1:第一回 "redis.call('hset',KEYS[1],ARGV[2],1) ; "+ --無鎖 加鎖 // 配置鎖的生命周期 "redis.call('pexpire',KEYS[1],ARGV[1]) ; "+ "return nil; end ;" + //可重入操作,判斷是不是我加的鎖 "if (redis.call('hexists',KEYS[1],ARGV[2]) ==1 ) then "+ --我加的鎖 //hincrby 在原來的鎖上加1 "redis.call('hincrby',KEYS[1],ARGV[2],1) ; "+ --重入鎖 "redis.call('pexpire',KEYS[1],ARGV[1]) ; "+ "return nil; end ;" + //否則,鎖存在,返回鎖的有效期,決定下次執行腳本時間 "return redis.call('pttl',KEYS[1]) ;" --不能加鎖,返回鎖的時間
lua的作用:保證這段複雜業務邏輯執行的原子性。
lua的解釋:
- KEYS[1]) : 加鎖的key
- ARGV[1] : key的生存時間,預設為30秒
- ARGV[2] : 加鎖的客戶端ID (UUID.randomUUID()) + “:” + threadId)
釋放鎖的lua腳本
# 如果key已經不存在,說明已經被解鎖,直接發佈(publish)redis消息(無鎖,直接返回) "if (redis.call('exists', KEYS[1]) == 0) then " + "redis.call('publish', KEYS[2], ARGV[1]); " + "return 1; " + "end;" + # key和field不匹配,說明當前客戶端線程沒有持有鎖,不能主動解鎖。 不是我加的鎖 不能解鎖 (有鎖不是我加的,返回) "if (redis.call('hexists', KEYS[1], ARGV[3]) == 0) then " + "return nil;" + "end; " + # 將value減1 (有鎖是我加的,進行hincrby -1 ) "local counter = redis.call('hincrby', KEYS[1], ARGV[3],-1); " + # 如果counter>0說明鎖在重入,不能刪除key "if (counter > 0) then " + "redis.call('pexpire', KEYS[1], ARGV[2]); " + "return 0; " + # 刪除key並且publish 解鎖消息 # 可重入鎖減完了,進行del操作 "else " + "redis.call('del', KEYS[1]); " + #刪除鎖 "redis.call('publish', KEYS[2], ARGV[1]); " + "return 1; "+ "end; " + "return nil;",
- – KEYS[1] :需要加鎖的key,這裡需要是字元串類型。
- – KEYS[2] :redis消息的ChannelName,一個分散式鎖對應唯一的一個channelName: “redisson_lockchannel{” + getName() + “}”
- – ARGV[1] :reids消息體,這裡只需要一個位元組的標記就可以,主要標記redis的key已經解鎖,再結合 redis的Subscribe,能喚醒其他訂閱解鎖消息的客戶端線程申請鎖。
- – ARGV[2] :鎖的超時時間,防止死鎖
- – ARGV[3] :鎖的唯一標識,也就是剛纔介紹的 id(UUID.randomUUID()) + “:” + threadId
2.3 網址
https://github.com/redisson/redisson
知識wiki
https://github.com/redisson/redisson/wiki/Table-of-Content
3. Redisson 常用鎖
3.1 可重入鎖
Redisson的分散式鎖RLock是一種可重入鎖。當一個線程獲取到鎖之後,這個線程可以再次獲取本對象上的鎖,而其他的線程是不可以的。
- JDK中的reentrantLock(entrant是進入者、新會員的意思,reentrant是 可重入、可重入的 的意思)是可重入鎖,其是通過AQS(抽象對象同步器)實現的鎖機制。
- synchronized也是可重入鎖,其是通過監視器模式(本質是OS的互斥鎖)實現的鎖機制。
3.2 公平鎖 Fair Lock
Redisson的可重入鎖RLock預設是一種非公平鎖,但也支持可重入公平鎖 Fair Lock。當有多個線程同時申請鎖時,這些線程會進入到一個FIFO隊列,只有隊首元素才會獲取到鎖,其它元素等待。只有當鎖被釋放後,才會再將鎖分配給當前的隊首元素。
3.3 聯鎖 MultiLock
Redisson 分散式鎖可以實現聯鎖 MultiLock。當一個線程需要同時處理多個共用資源時,可使用聯鎖。即一次性申請多個鎖,同時鎖定多個共用資源。聯鎖可以預防死鎖。相當於對共用資源的申請實現了原子性:要麼都申請到,只要缺少一個資源,則將申請到的資源釋放。其是OS底層原理中 AND 型信號量機制的典型應用。
3.4 紅鎖 RedLock
Redisson 分散式鎖可以實現紅鎖 RedLock。紅鎖由多個鎖(同時至少是向三個Redis集群申請的鎖)構成,只有當這些鎖中的大部分鎖申請成功時,紅鎖才申請成功。紅鎖一般用於解決Redis主從集群鎖丟失問題。
紅鎖和聯鎖的區別:紅鎖實現的是對同一個共用資源的同步訪問控制,而聯鎖實現的是多個共用資源的同步訪問控制。
3.5 讀寫鎖 RReadWriteLock
通過Redisson可以獲取到讀寫鎖 RReadWriteLock。通過 RReadWriteLock 實例可分別獲取到讀鎖 RedissonReadLock 和 寫鎖 RedissonWriteLock。讀鎖與寫鎖分別是實現了RLock的可重入鎖。
一個共用資源,在沒有寫鎖的情況下,允許同時添加多個讀鎖。只要添加了寫鎖,任何讀鎖與寫鎖都不可以再次添加。即讀鎖是共用資源,寫鎖是排他鎖。
3.6 信號量 Semaphore
通過Redisson可以獲取到信號量RSemaphore。RSemaphore的常用場景有兩種:一種是,無論誰添加的鎖,任何其它線程都可以解鎖,就可以使用RSemaphore。另外,當一個線程需要一次申請多個資源時,可使用RSemaphore。RSemaphore是信號量機制的典型應用。
3.7 可過期性信號量 PermitExpirableSemaphore
通過Redisson可以獲取到可過期信號量PermitExpirableSemaphore。該信號量是在RSemaphore基礎上,為每個信號量增加了一個過期時間,且每個信號都可以通過獨立的ID來辨識。釋放時也只能通過提交該ID才能釋放。
不過,一個線程每次只能申請一個信號量,當然每次也只會釋放一個信號量。這是與RSemaphore不同的地方。
該信號量為互斥信號量時,其就等同於可重入鎖。或者說,可重入鎖就相當於信號量為1的可過期信號量。
可過期信號量與可重入鎖的區別,可重入鎖:相當於用戶每次只能申請1個信號量,且只有一個用戶可以申請成功。可過期信號量:用戶每次只能申請1個信號量,但可以有多個用戶申請成功。
3.8 閉鎖 RCountDownLatch
通過Redisson 可以獲取到分散式閉鎖 RCountDownLatch,其與JDK的JUC中的閉鎖CountDownLatch原理一樣,用法類似。其常用於一個或多個線程的執行必須在其它某些任務執行完畢的場景。例如,大規模分散式並行計算中,最終的合併計算必須基於很多並行計算的運行完畢。
閉鎖中定義了一個計數器和一個阻塞隊列。阻塞隊列中存放者待執行的線程(又稱 合併線程)。每當一個並行任務執行完畢(又稱 條件線程),計數器就減1.當計數器遞減到0時就會喚醒阻塞隊列中的所有線程。
如果不使用Redisson,那麼通常使用Barrier隊列解決該問題,而Barrier隊列通常使用Zookeeprt實現。
學習筆記--參閱特別聲明
1.【Redis視頻從入門到高級】
【https://www.bilibili.com/video/BV1U24y1y7jF?p=11&vd_source=0e347fbc6c2b049143afaa5a15abfc1c】
2.《Redis:Redisson分散式鎖的使用方式(推薦使用)》
https://www.jb51.net/database/319949d8d.htm