框架篇:分散式鎖

潛行前行發表於2021-06-22

前言

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演算法的實現步驟
    image.png

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);
}

歡迎指正文中錯誤

引數文章

相關文章