什么是分布式鎖?
要介紹分布式鎖,首先要提到與分布式鎖相對應的是線程鎖、進程鎖。
線程鎖:主要用來給方法、代碼塊加鎖。當某個方法或代碼使用鎖,在同一時刻僅有一個線程執行該方法或該代碼段。線程鎖只在同一JVM中有效果,因為線程鎖的實現在根本上是依靠線程之間共享內存實現的,比如synchronized是共享對象頭,顯示鎖Lock是共享某個變量(state)。
進程鎖:為了控制同一操作系統中多個進程訪問某個共享資源,因為進程具有獨立性,各個進程無法訪問其他進程的資源,因此無法通過synchronized等線程鎖實現進程鎖。
分布式鎖:當多個進程不在同一個系統中,用分布式鎖控制多個進程對資源的訪問。
前言
現在的業務場景越來越復雜,使用的架構也就越來越復雜,分布式、高并發已經是業務要求的常態。像騰訊系的不少服務,還有CDN優化、異地多備份等處理。
說到分布式,就必然涉及到分布式鎖的概念,如何保證不同機器不同線程的分布式鎖同步呢?
實現要點
- 互斥性,同一時刻,智能有一個客戶端持有鎖。
- 防止死鎖發生,如果持有鎖的客戶端崩潰沒有主動釋放鎖,也要保證鎖可以正常釋放及其他客戶端可以正常加鎖。
- 加鎖和釋放鎖必須是同一個客戶端。
- 容錯性,只有redis還有節點存活,就可以進行正常的加鎖解鎖操作。
正確的redis分布式鎖實現
錯誤加鎖方式
錯誤方式一
保證互斥和防止死鎖,首先想到的使用redis的setnx命令保證互斥,為了防止死鎖,鎖需要設置一個超時時間。
1
2
3
4
5
6
7
|
public static void wrongLock(Jedis jedis, String key, String uniqueId, int expireTime) { Long result = jedis.setnx(key, uniqueId); if (1 == result) { //如果該redis實例崩潰,那就無法設置過期時間了 jedis.expire(key, expireTime); } } |
在多線程并發環境下,任何非原子性的操作,都可能導致問題。這段代碼中,如果設置過期時間時,redis實例崩潰,就無法設置過期時間。如果客戶端沒有正確的釋放鎖,那么該鎖(永遠不會過期),就永遠不會被釋放。
錯誤方式二
比較容易想到的就是設置值和超時時間為原子原子操作就可以解決問題。那使用setnx命令,將value設置為過期時間不就ok了嗎?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
|
public static boolean wrongLock(Jedis jedis, String key, int expireTime) { long expireTs = System.currentTimeMillis() + expireTime; // 鎖不存在,當前線程加鎖成果 if (jedis.setnx(key, String.valueOf(expireTs)) == 1) { return true; } String value = jedis.get(key); //如果當前鎖存在,且鎖已過期 if (value != null && NumberUtils.toLong(value) < System.currentTimeMillis()) { //鎖過期,設置新的過期時間 String oldValue = jedis.getSet(key, String.valueOf(expireTs)); if (oldValue != null && oldValue.equals(value)) { // 多線程并發下,只有一個線程會設置成功 // 設置成功的這個線程,key的舊值一定和設置之前的key的值一致 return true; } } // 其他情況,加鎖失敗 return true; } |
乍看之下,沒有什么問題。但仔細分析,有如下問題:
value設置為過期時間,就要求各個客戶端嚴格的時鐘同步,這就需要使用到同步時鐘。即使有同步時鐘,分布式的服務器一般來說時間肯定是存在少許誤差的。
鎖過期時,使用 jedis.getSet雖然可以保證只有一個線程設置成功,但是不能保證加鎖和解鎖為同一個客戶端,因為沒有標志鎖是哪個客戶端設置的嘛。
錯誤解鎖方式
解鎖錯誤方式一
直接刪除key
1
2
3
4
|
public static void wrongReleaseLock(Jedis jedis, String key) { //不是自己加鎖的key,也會被釋放 jedis.del(key); } |
簡單粗暴,直接解鎖,但是不是自己加鎖的,也會被刪除,這好像有點太隨意了吧!
解鎖錯誤方式二
判斷自己是不是鎖的持有者,如果是,則只有持有者才可以釋放鎖。
1
2
3
4
5
6
|
public static void wrongReleaseLock(Jedis jedis, String key, String uniqueId) { if (uniqueId.equals(jedis.get(key))) { // 如果這時鎖過期自動釋放,又被其他線程加鎖,該線程就會釋放不屬于自己的鎖 jedis.del(key); } } |
看起來很完美啊,但是如果你判斷的時候鎖是自己持有的,這時鎖超時自動釋放了。然后又被其他客戶端重新上鎖,然后當前線程執行到jedis.del(key),這樣這個線程不就刪除了其他線程上的鎖嘛,好像有點亂套了哦!
正確加鎖釋放鎖方式
基本上避免了以上幾種錯誤方式之外,就是正確的方式了。要滿足以下幾個條件:
命令必須保證互斥
設置的key必須要有過期時間,防止崩潰時鎖無法釋放
value使用唯一id標志每個客戶端,保證只有鎖的持有者才可以釋放鎖
加鎖直接使用set命令同時設置唯一id和過期時間;其中解鎖稍微復雜些,加鎖之后可以返回唯一id,標志此鎖是該客戶端鎖擁有;釋放鎖時要先判斷擁有者是否是自己,然后刪除,這個需要redis的lua腳本保證兩個命令的原子性執行。
下面是具體的加鎖和釋放鎖的代碼:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
|
@Slf4j public class RedisDistributedLock { private static final String LOCK_SUCCESS = "OK"; private static final Long RELEASE_SUCCESS = 1L; private static final String SET_IF_NOT_EXIST = "NX"; private static final String SET_WITH_EXPIRE_TIME = "PX"; // 鎖的超時時間 private static int EXPIRE_TIME = 5 * 1000; // 鎖等待時間 private static int WAIT_TIME = 1 * 1000; private Jedis jedis; private String key; public RedisDistributedLock(Jedis jedis, String key) { this.jedis = jedis; this.key = key; } // 不斷嘗試加鎖 public String lock() { try { // 超過等待時間,加鎖失敗 long waitEnd = System.currentTimeMillis() + WAIT_TIME; String value = UUID.randomUUID().toString(); while (System.currentTimeMillis() < waitEnd) { String result = jedis.set(key, value, SET_IF_NOT_EXIST, SET_WITH_EXPIRE_TIME, EXPIRE_TIME); if (LOCK_SUCCESS.equals(result)) { return value; } try { Thread.sleep(10); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } } } catch (Exception ex) { log.error("lock error", ex); } return null; } public boolean release(String value) { if (value == null) { return false; } // 判斷key存在并且刪除key必須是一個原子操作 // 且誰擁有鎖,誰釋放 String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end"; Object result = new Object(); try { result = jedis.eval(script, Collections.singletonList(key), Collections.singletonList(value)); if (RELEASE_SUCCESS.equals(result)) { log.info("release lock success, value:{}", value); return true; } } catch (Exception e) { log.error("release lock error", e); } finally { if (jedis != null) { jedis.close(); } } log.info("release lock failed, value:{}, result:{}", value, result); return false; } } |
單是一個redis的分布式鎖就有這么多道道,不知道你是否看明白了?留言討論下吧!
總結
以上所述是小編給大家介紹的Redis分布式鎖的實現方式(redis面試題),希望對大家有所幫助,如果大家有任何疑問請給我留言,小編會及時回復大家的。在此也非常感謝大家對服務器之家網站的支持!如果你覺得本文對你有幫助,歡迎轉載,煩請注明出處,謝謝!
原文鏈接:https://blog.csdn.net/chanllenge/article/details/102983597