天天看點

架構篇:分布式鎖

前言

java有synchronize和Lock,mysql 修改類的sql也帶有鎖。鎖定資料狀态,讓資料狀态在并發場景,按我們預想邏輯進行狀态轉移,然而在分布式,叢集的情況下,怎麼去鎖定資料狀态呢

  • 資料庫的分布式鎖方案
  • 基于redis實作分布式鎖
  • 基于zookeeper實作分布式鎖

關注公衆号,一起交流,微信搜一搜: 潛行前行

資料庫分布鎖的難點

  • 單點故障? 資料庫可以多搞個資料庫備份
  • 沒有失效時間? 每次加鎖時,插入一個期待的有效時間;A:定時任務,隔一段時間清理時間失效鎖。B:下次加鎖時則先判斷目前時間是否大于鎖的有效時間,以此判斷鎖是否失效
  • 不可重入? 在資料加鎖時加入一個幂等唯一值字段,下次擷取時,先判斷這個字段是否一緻,一緻則說明是目前操作重入操作

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

歡迎指正文中錯誤

參數文章

  • 記一次分布式鎖-基于資料庫
  • Redis分布式鎖的正确實作方式
  • redis實作分布式鎖,單機-叢集-紅鎖
  • 如何能通俗的講解Zookeeper分布式鎖的應用場景?
  • 基于Zookeeper開源用戶端Curator實作分布式鎖