前言
java有synchronize和Lock,mysql 修改類的sql也帶有鎖。鎖定資料狀態,讓資料狀態在併發場景,按我們預想邏輯進行狀態轉移,然而在分散式,叢集的情況下,怎麼去鎖定資料狀態呢
- 資料庫的分散式鎖方案
- 基於redis實現分散式鎖
- 基於zookeeper實現分散式鎖
關注公眾號,一起交流,微信搜一搜: 潛行前行
資料庫的分散式鎖方案
資料庫分佈鎖的難點
- 單點故障? 資料庫可以多搞個資料庫備份
- 沒有失效時間? 每次加鎖時,插入一個期待的有效時間;A:定時任務,隔一段時間清理時間失效鎖。B:下次加鎖時則先判斷當前時間是否大於鎖的有效時間,以此判斷鎖是否失效
- 不可重入? 在資料加鎖時加入一個冪等唯一值欄位,下次獲取時,先判斷這個欄位是否一致,一致則說明是當前操作重入操作
基於redis實現分散式鎖
- redis 是一個快速訪問的高效能服務,相比資料庫,在redis實現鎖比直接在資料庫的資料加鎖,效能好。同時也為資料庫減壓,減少事務執行因為鎖的問題阻塞
- 引入jedis
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
<version>2.9.0</version>
</dependency>
setnx + expire
- setnx + expire 存在死鎖的問題。setnx()方法作用就是SET IF NOT EXIST,expire()方法就是給鎖加一個過期時間。由於這是兩條Redis命令,不具有原子性
Long result = jedis.setnx(lockKey, requestId);
if (result == 1) {
// 這裡程式突然崩潰,則無法設定過期時間,將發生死鎖
jedis.expire(lockKey, expireTime);
}
lua指令碼(正確方式)
- lua指令碼在Redis的執行過程是原子性,要麼成功,要麼失敗。
// setnx + expire 放在lua指令碼執行
String script = "if redis.call('setnx',KEYS[1],ARGV[1]) == 1 then" +
" redis.call('expire',KEYS[1],ARGV[2]) return 1 else return 0 end";
Object result = jedis.eval(script, Collections.singletonList(lockKey), Collections.singletonList(requestId));
if(result.equals(1)){
.... //加鎖成功的操作
}
set {key} {value} nx ex {second}
(正確方式)
- 這是Redis的SET指令擴充套件引數,具有原子性
String lockKey = "鎖的KEY值";//固定的
String requestId = "當次加鎖操作的唯一標識";
int expireTime = 1000;//失效時間
String result = jedis.set(lockKey, requestId, "NX", "PX", expireTime);
刪除redis分佈鎖
//-------- 錯誤方式 ------------
// 判斷加鎖與解鎖是不是同一個客戶端
if (requestId.equals(jedis.get(lockKey))) {
// 若在此時,這把鎖突然不是這個客戶端的,則會誤解鎖
jedis.del(lockKey);
}
//-------- 正確的方式 使用 lua ------------
String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
Object result = jedis.eval(script, Collections.singletonList(lockKey), Collections.singletonList(requestId));
基於Redlock演算法實現分散式鎖
- 以上redis分佈鎖的缺點就是它加鎖時只作用在一個Redis節點上,即使redis通過sentinel保證高可用,如果這個master節點由於某些原因發生了主從切換,那麼就會出現鎖丟失的情況。redis主從同步不能保證一致性,master會優先返回結果,在同步資料到slave
- 例如:在redis的master節點上拿到了鎖 -> 這個加鎖的key還沒有同步到slave節點 -> master故障,發生故障轉移,slave節點升級為master節點 -> 導致鎖丟失
- RedLock演算法的實現步驟
1: 獲取當前時間,以毫秒為單位
2: 按順序向5個master節點請求加鎖。客戶端設定網路連線和響應超時時間,並且超時時間要小於鎖的失效時間。(假設鎖自動失效時間為10秒,則超時時間一般在5-50毫秒之間,我們就假設超時時間是50ms吧)。如果超時,跳過該master節點,儘快去嘗試下一個master節點
3: 加鎖後客戶端使用當前時間減去開始獲取鎖時間(即步驟1記錄的時間),得到獲取鎖使用的時間。當且僅當超過一半(N/2+1,這裡是5/2+1=3個節點)的Redis master節點都獲得鎖,並且獲取鎖使用的時間小於鎖失效時間時,鎖才算獲取成功。(如上圖:10s> 30ms+40ms+50ms+20ms+50ms)
4: 如果成功取到鎖,key的真正有效時間等於 鎖失效時間 減去 獲取鎖所使用的時間。
5: 如果獲取鎖失敗(沒有在至少N/2+1個master例項取到鎖,或者獲取鎖時間已經超過了鎖失效時間),客戶端要在所有的master節點上解鎖(即便有些master節點根本就沒有加鎖成功,也需要解鎖,以防止有些漏網之魚)
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson</artifactId>
<version>3.4.3</version>
</dependency>
- 程式碼示例
Config config = new Config().useSingleServer().setAddress("127.0.0.1:6380").setDatabase(0);
RedissonClient rLock1 = Redisson.create(config);
config = new Config().useSingleServer().setAddress("127.0.0.1:6381").setDatabase(0);
RedissonClient rLock2 = Redisson.create(config);
config = new Config().useSingleServer().setAddress("127.0.0.1:6382").setDatabase(0);
RedissonClient rLock3 = Redisson.create(config);
//初始化
String lockKey = "XXX";
RLock rLock1 = redissonRed1.getLock(lockKey);
RLock rLock2 = redissonRed2.getLock(lockKey);
RLock rLock3 = redissonRed2.getLock(lockKey);
RedissonRedLock rLock = new RedissonRedLock(rLock1,rLock2,rLock3);
//加鎖
rLock.lock();
//釋放
rLock.unlock();
基於 zookeeper 實現分散式鎖
- maven引入
<dependency>
<groupId>org.apache.curator</groupId>
<artifactId>curator-recipes</artifactId>
<version>2.4.1</version>
</dependency>
- Redlock演算法往往需要多個redis叢集才能實現,東西越多,就越容易出錯。但是如何實現一個高效高可用的分散式鎖呢 ? zookeeper
- zookeeper特點
- 最終一致性:客戶端的操作狀態會在 zookeepr 叢集保持一致
- 可靠性:zookeeper 叢集具有簡單、健壯、良好的效能
- 原子性:操作只能成功或者失敗,沒有中間狀態
- 時間順序性:如果訊息 A 在訊息 B 釋出,則 A 則排在 B 前面
- zookeeper 臨時順序節點:臨時節點的生命週期和客戶端會話繫結。也就是說,如果客戶端會話失效,那麼這個節點就會自動被清除掉(可解決分散式鎖的自動失效)。另外,在臨時節點下面不能建立子節點,叢集zk環境下,同一個路徑的臨時節點只能成功建立一個
- zookeeper 監視器:zookeeper建立一個節點時,會註冊一個該節點的監視器,當節點狀態發生改變時,watch會被觸發,zooKeeper將會向客戶端傳送一條通知
- zookeeper 分散式鎖原理
建立臨時有序節點,每個執行緒均能建立節點成功,但是其序號不同,只有序號最小的可以擁有鎖,其它執行緒只需要監聽比自己序號小的節點狀態即可
1: 在指定的節點下建立一個鎖目錄lock
2: 執行緒X進來獲取鎖在lock目錄下,並建立臨時有序節點
3: 執行緒X獲取lock目錄下所有子節點,並獲取比自己小的兄弟節點,如果不存在比自己小的節點,說明當前執行緒序號最小,順利獲取鎖
4: 此時執行緒Y進來建立臨時節點並獲取兄弟節點,判斷自己是否為最小序號節點,發現不是,於是設定監聽(watch)比自己小的節點(這裡是為了發生上面說的羊群效應)
5: 執行緒X執行完邏輯,刪除自己的節點,執行緒Y監聽到節點有變化,進一步判斷自己是已經是最小節點,順利獲取鎖
- 程式碼例項
//初始化
CuratorFramework curatorFramework= CuratorFrameworkFactory.newClient("zookeeper1.tq.master.cn:2181",new ExponentialBackoffRetry(1000,3));
curatorFramework.start();
//建立臨時節點鎖
String lockPath = "/distributed/lock/";//根節點
//可重入排它鎖
String lockName = "xxxx";
InterProcessMutex interProcessMutex = new InterProcessMutex(curatorFramework, lockPath + lockName);
//加鎖
interProcessMutex.acquire(2, TimeUnit.SECONDS)
//釋放鎖
if(interProcessMutex.isAcquiredInThisProcess()){
interProcessMutex.release();
curatorFramework.delete().inBackground().forPath(lockPath + lockName);
}