天天看點

分布式-鎖-初見

介紹幾種常見的分布式鎖寫法

多線程中為了防止多個線程同時執行同一段代碼,我們可以用 synchronized 關鍵字或 JUC 裡面的 ReentrantLock 類來控制,

但是目前幾乎任何一個系統都是部署多台機器的,單機部署的應用很少,synchronized 和 ReentrantLock 發揮不出任何作用,

此時就需要一把全局的鎖,來代替 JAVA 中的 synchronized 和 ReentrantLock。

分布式鎖的實作方式流行的主要有三種

  1. 分别是基于緩存 Redis 的實作方式
  2. 基于 zk 臨時順序節點的實作
  3. 基于資料庫行鎖的實作。

官網

目錄 · redisson/redisson Wiki · GitHub

分布式-鎖-初見

Jedis

使用 Redis 做分布式鎖的思路是:

在 redis 中設定一個值表示加了鎖,然後釋放鎖的時候就把這個 key 删除。

/**          * 嘗試擷取分布式鎖          *          * @param jedis      Redis用戶端          * @param lockKey    鎖          * @param requestId  請求辨別          * @param expireTime 超期時間          * @return 是否擷取成功          */         public static boolean tryGetDistributedLock(Jedis jedis, String lockKey, String requestId, int expireTime) {             // set支援多個參數 NX(not exist) XX(exist) EX(seconds) PX(million seconds)             String result = jedis.set(lockKey, requestId, "NX", "EX", expireTime);             if (LOCK_SUCCESS.equals(result)) {                 return true;             }             return false;         }         /**          * 釋放分布式鎖          *          * @param jedis     Redis用戶端          * @param lockKey   鎖          * @param requestId 請求辨別,目前工作線程線程的名稱          * @return 是否釋放成功          */         public static boolean releaseDistributedLock(Jedis jedis, String lockKey, String requestId) {             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));             if (RELEASE_SUCCESS.equals(result)) {                 return true;             }             return false;         }           

問題

  • 用 SET key value ,而沒有使用 SETNX+EXPIRE 的指令,原因是 SETNX+EXPIRE 是兩條指令無法保證原子性,而 SET 是原子操作。
  • 那這裡為什麼要設定逾時時間呢?

    原因是當一個用戶端獲得了鎖在執行任務的過程中挂掉了,來不及顯式地釋放鎖,這塊資源将會永遠被鎖住,

    這将會導緻死鎖,是以必須設定一個逾時時間。

  • A 加的鎖 B 不能去 del 掉,誰加的鎖就誰去解,我們一般把 value 設為目前線程的 Id,

    Thread.currentThread().getId(),然後在删的時候判斷下是不是目前線程。

  • 驗證和釋放鎖是兩個獨立操作,不是原子性,

    使用 Lua 腳本,即 if redis.call('get', KEYS[1]) == ARGV[1] then returnredis.call('del', KEYS[1]) else return 0 end,它能給我們保證原子性。

當 redis.call() 在執行指令的過程中發生錯誤時,腳本會停止執行,并傳回一個腳本錯誤,

錯誤的輸出資訊會說明錯誤造成的原因:

Redisson

Redisson 是 Java 的 Redis 用戶端之一,提供了一些 API 友善操作 Redis。

Redisson 跟 Jedis 定位不同,它不是一個單純的 Redis 用戶端,

而是基于 Redis 實作的分布式的服務,

鎖隻是它的冰山一角,并且它對主從,哨兵,叢集等模式都支援。

public class LockTest{     	private static Redis sonClient redissonClient;     	static {     		Config config = new Config();     		config. useSingleServer().setAddress("redis://127.0.0.1:6379");     		redissonClient = Redisson.create ( config);         }         public static void main(String[] args) throws InterruptedException {     		RLock rLock = redissonClient . getLock(”zwt" );     		//最多等待100秒、 上鎖10s以後自動解鎖     		if (rLock.tryLock(100, 10, TimeUnit. SECONDS)) {     			System . out . println("擷取鎖成功" );     		}     		//Thread. sleep(20000);     		rLock. unlock( );     		redissonClient . shutdown();     	}     }           

這裡擷取鎖有很多種的方式,有公平鎖有讀寫鎖,我們使用的是 redissonClient.getLock, 這是一個可重入鎖。

在加鎖的時候,寫入了一個 HASH 類型的值,key 是鎖名稱 zwt,field 是線程的名稱,而 value 是 1(即表示鎖的重入次數)。

點進 tryLock() 方法的 tryAcquire() 方法,再到->tryAcquireAsync() 再到->tryLockInnerAsync(),

終于見到廬山真面目了,原來它最終也是通過 Lua 腳本來實作的。

<T> RFuture<T> tryLockInnerAsync (long leaseTime, TimeUnit unit, long threadId, RedisStrictCommand<T> command) {     	internalLockLeaseTime = unit. toMillis( leaseTime);         return commandExecutor . evalWriteAsync getName(), LongCodec . INSTANCE, command,     				"if (redis.call( 'exists', KEYS[1]) == 0) then" +     					"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     					"redis. call( 'hincrby', KEYS[1], ARGV[2], 1);" +     					"redis. call( 'pexpire', KEYS[1], ARGV[1]);" +     					"return nil;" +     				"end;" +     				"return redis.call('pttl', KEYS[1]);",     				Collections.<~>singLetonL ist(getName()), internalLockLeaseTime, getLockName (threadId));     }           
Lua腳本拉出來分析一下:     // KEYS[1] 鎖名稱 updateAccount     // ARGV[1] key 過期時間 10000ms     // ARGV[2] 線程名稱     // 鎖名稱不存在     if (redis.call('exists', KEYS[1]) == 0) then     // 建立一個 hash,key=鎖名稱,field=線程名,value=1     redis.call('hset', KEYS[1], ARGV[2], 1);     // 設定 hash 的過期時間     redis.call('pexpire', KEYS[1], ARGV[1]);     return nil;     end;     // 鎖名稱存在,判斷是否目前線程持有的鎖     if (redis.call('hexists', KEYS[1], ARGV[2]) == 1) then     // 如果是,value+1,代表重入次數+1     redis.call('hincrby', KEYS[1], ARGV[2], 1);     // 重新獲得鎖,需要重新設定 Key 的過期時間     redis.call('pexpire', KEYS[1], ARGV[1]);     return nil;     end;     // 鎖存在,但是不是目前線程持有,傳回過期時間(毫秒)     return redis.call('pttl', KEYS[1]);           
unlock() 中的 unlockInnerAsync() 釋放鎖,同樣也是通過 Lua 腳本實作。     // KEYS[1] 鎖的名稱 updateAccount     // KEYS[2] 頻道名稱 redisson_lock__channel:{updateAccount}     // ARGV[1] 釋放鎖的消息 0     // ARGV[2] 鎖釋放時間 10000     // ARGV[3] 線程名稱     // 鎖不存在(過期或者已經釋放了)     if (redis.call('exists', KEYS[1]) == 0) then     // 釋出鎖已經釋放的消息     redis.call('publish', KEYS[2], ARGV[1]);     return 1;     end;     // 鎖存在,但是不是目前線程加的鎖     if (redis.call('hexists', KEYS[1], ARGV[3]) == 0) then     return nil;     end;     // 鎖存在,是目前線程加的鎖     // 重入次數-1     local counter = redis.call('hincrby', KEYS[1], ARGV[3], -1);     // -1 後大于 0,說明這個線程持有這把鎖還有其他的任務需要執行     if (counter > 0) then     // 重新設定鎖的過期時間     redis.call('pexpire', KEYS[1], ARGV[2]);     return 0;     else     // -1 之後等于 0,現在可以删除鎖了     redis.call('del', KEYS[1]);     // 删除之後釋出釋放鎖的消息     redis.call('publish', KEYS[2], ARGV[1]);     return 1;     end;     // 其他情況傳回 nil     return nil;           
  • 業務沒執行完,鎖到期了怎麼辦,這個由watchdog來保障。
  • 叢集模式下,如果對多個master加鎖,導緻重複加鎖怎麼辦,Redission會自動選擇同一個 master。
  • 業務沒執行完,Redis master挂掉了怎麼辦,沒關系,Redis slave還有這個資料。

RedLock

名字由來

RedLock 的中文是直譯過來的,就叫紅鎖。

紅鎖并非是一個工具,而是 Redis 官方提出的一種分布式鎖的算法。

我們知道如果采用單機部署模式,會存在單點問題,隻要 redis 故障了,加鎖就不行了。

如果采用 master-slave 模式,加鎖的時候隻對一個節點加鎖,

即便通過 sentinel 做了高可用,但是如果 master 節點故障了,發生主從切換,

此時就會有可能出現鎖丢失的問題。

基于以上的考慮,其實 redis 的作者 Antirez 也考慮到這個問題,他提出了一個 RedLock 的算法。

算法實作

通過以下步驟擷取一把鎖:     1.擷取目前時間戳,機關是毫秒     2.輪流嘗試在每個 master 節點上建立鎖,過期時間設定較短,一般就幾十毫秒     3.嘗試在大多數節點上建立一個鎖,比如5個節點就要求是3個節點(n / 2 +1)     4.用戶端計算建立好鎖的時間,如果建立鎖的時間小于逾時時間,就算建立成功了     5.要是鎖建立失敗了,那麼就依次删除這個鎖     6.隻要别人建立了一把分布式鎖,你就得不斷輪詢去嘗試擷取鎖           
RLock lock1 = redissonInstance1. getLock("lock1");     RLock lock2 = redissonInstance2. getLock("lock2");     RLock 1ock3 = redissonInstance3. getLock("lock3");     RedissonRedLock lock = new RedissonRedlock(lock1, lock2, lock3);     //同時加鎖。lock1 lock2 lock3     //紅鎖在大部分節點上加鎖成功就算成功。     lock.lock();     //…………     lock.unlock();           

Zookeeper寫法(Curator)

擷取鎖

分布式-鎖-初見
Client1 得到了鎖,Client2 監聽了 Lock1,Client3 監聽了 Lock2。這恰恰形成了一個等待隊列

釋放鎖

1.任務完成,用戶端顯示釋放

當任務完成時,Client1 會顯示調用删除節點 Lock1 的指令。
分布式-鎖-初見

2.任務執行過程中,用戶端崩潰

獲得鎖的 Client1 在任務執行過程中,如果 Duang 的一聲崩潰,則會斷開與 Zookeeper 服務端的連結。

根據臨時節點的特性,相關聯的節點 Lock1 會随之自動删除。

Client2 一直監聽着 Lock1 的存在狀态,當 Lock1 節點被删除,Client2 會立刻收到通知。

這時候 Client2 會再次查詢 ParentLock 下面的所有節點,确認自己建立的節點 Lock2 是不是目前最小的節點。

如果是最小,則 Client2 順理成章獲得了鎖。

Curator

在 Apache 的開源架構 Apache Curator 中,包含了對 Zookeeper 分布式鎖的實作。

<dependency>         <groupId>org.apache.curator</groupId>         <artifactId>curator-recipes</artifactId>         <version>4.3.0</version>     </dependency>           

Curator的幾種鎖的實作

分布式-鎖-初見
  • InterProcessMutex:分布式可重入排它鎖
  • InterProcessSemaphoreMutex:分布式排它鎖
  • InterProcessMultiLock:将多個鎖作為單個實體管理的容器
public class ZkDistributedLock implements DistributedLock {         private final CuratorF ramework client;         public ZkDistributedLock ( CuratorFramework client) { this.client = client; }         @override         public void acquire(String key) throws Exception {     		InterProcessMutex lock = new InterProcessMutex(client, key);     	lock.acquire();     	}         @Override         public boolean acquire(String key, long maxwait, TimeUnit waitunit) throws Exception{     		InterProcessMutex lock = new InterProcessMutex(client, key);     		return lock.acquire (maxWait, waitUnit);     	}         @Override     	public void release(String key) throws Exception {     		InterProcessMutex lock = new InterProcessMutex(client, key);     		lock. release();     	}     }           

總結

zookeeper 天生設計定位就是分布式協調,強一緻性,鎖很健壯。

如果擷取不到鎖,隻需要添加一個監聽器就可以了,不用一直輪詢,性能消耗較小。

缺點: 在高請求高并發下,系統瘋狂的加鎖釋放鎖,最後 zk 承受不住這麼大的壓力可能會存在當機的風險。

zk 鎖性能比 redis 低的原因:zk 中的角色分為 leader,flower,

每次寫請求隻能請求 leader,leader 會把寫請求廣播到所有 flower,

如果 flower 都成功才會送出給 leader,在加鎖的時候是一個寫請求,

當寫請求很多時,zk 會有很大的壓力,最後導緻伺服器響應很慢。

redis 鎖實作簡單,了解邏輯簡單,性能好,可以支撐高并發的擷取、釋放鎖操作。

缺點: Redis 容易單點故障,叢集部署,并不是強一緻性的,鎖的不夠健壯;

​ key 的過期時間設定多少不明确,隻能根據實際情況調整;

​ 需要自己不斷去嘗試擷取鎖,比較消耗性能。

最後不管 redis 還是 zookeeper,它們都應滿足分布式鎖的特性:

  • 具備可重入特性(已經獲得鎖的線程在執行的過程中不需要再次獲得鎖)
  • 異常或者逾時自動删除,避免死鎖
  • 互斥性,隻有一個用戶端能夠持有鎖
  • 分布式環境下高性能、高可用、容錯機制

各有千秋,具體業務場景具體使用。

下一篇: Redis-初見