帶你輕松掌握Redis分佈式鎖
目前很多大型網站及應用都是分佈式部署的,分佈式場景中的數據一致性問題一直是一個比較重要的話題。
基於 CAP理論,任何一個分佈式系統都無法同時滿足一致性(Consistency)、可用性(Availability)和分區容錯性(Partition tolerance),最多隻能同時滿足兩項。
我們為瞭保證數據的最終一致性,需要很多的技術方案來支持,比如分佈式事務、分佈式鎖等。通常大傢都會采redis做分佈式鎖,但這樣就可以高枕無憂瞭嗎?
1. 什麼是分佈式鎖
分佈式與單機情況下最大的不同在於其不是多線程而是多進程,而數據隻有一份(或有限制),也就是說單機的共享內存已解決不瞭一致性寫問題,此時需要利用鎖的技術控制某一時刻修改數據的進程數。
當在分佈式模型下,分佈式鎖還是可以將標記存在內存,隻是該內存不是某個進程分配的內存而是公共內存(Redis、Memcache)。至於利用數據庫、文件等做鎖與單機的實現是一樣的,隻要保證標記能互斥就行。
2. 分佈式鎖該具備的特性
- 最好是可重入鎖(避免死鎖)
- 最好是一把阻塞鎖(根據業務需求決定)
- 最好是一把公平鎖(根據業務需求決定)
- 有高可用、高性能的獲取鎖和釋放鎖功能
3. 基於數據庫做分佈式鎖
- 基於樂觀鎖,CAS,但如果是insert的情況采用主鍵沖突防重,在大並發情況下有可能會造成鎖表現象
- 基於悲觀鎖,也就是排他鎖,會有各種各樣的問題(操作數據庫需要一定的開銷,使用數據庫的行級鎖並不一定靠譜,性能不靠譜)
如果按分佈式該具備的特性來逐條匹配,特別是高可用(存在單點)、高性能是硬傷
4. 基於Redis做分佈式鎖
一般都使用 setnx(set if not exists) 指令,隻允許被一個客戶端占有,先來先得, 用完後再通過 del 指令釋放。
如果中間邏輯執行時發生異常,可能會導致 del 指令沒有被執行,這樣就會陷入死鎖,怎麼破?
對,給鎖加個過期時間(即使出現異常也可以保證幾秒之後鎖會自動釋放)!
但setnx 和 expire 之間redis服務器突然掛掉,怎麼破?
其實該問題的根源就在於 setnx 和 expire 是兩條指令而不是原子指令。為瞭解決這個疑難,Redis 開源社區湧現瞭一堆分佈式鎖的 解決方案。為瞭治理這個亂象,Redis 2.8 版本中加入瞭 set 指令的擴展參數,使得 setnx 和 expire 指令可以一起執行,徹底解決瞭分佈式鎖的亂象。
總之,setnx 和 expire 組合就是分佈式鎖的奧義所在。
4.1 超時問題
如果在加鎖和釋放鎖之間的邏輯執行的太長,超出瞭超時限制,怎麼破?
也就是說第一個線程持有的鎖過期瞭但臨界區的邏輯還沒有執行完,這個時候第二個線程就提前重新持有瞭這把鎖,導致每個請求執行臨界區代碼時不能嚴格的串行執行。
Redis 的分佈式鎖不能解決超時問題,建議分佈式鎖不要用於較長時間的任務。
稍微安全一點的方案是為 set 指令的 value 參數設置為一個隨機數,釋放鎖時先匹配隨機數是否一致,一致的話再刪除 key,這是可以確保當前線程占有的鎖不會被其它線程釋放,但是並不能解決鎖被redis服務器自動釋放的。
int tag = random.nextint()//隨機數 boolean nx=true; int ex=5; if(redis.set(key, tag, nx, ex)){ do_something() redis.delifequals(key, tag)//不存在這樣的命令 }
但是匹配 value 和刪除 key 不是一個原子操作,怎麼破?
需要使用 Lua 腳本來處理瞭,因為 Lua 腳本可以保證連續多個指令的原子性執行。
#delifequals.lua文件,下面的是社區熱門代碼 if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end //java調用 public void delifequals(){ String script = readScript("delifequals.lua"); int tag = 5; String key = "key"; Object eval = jedis.eval(script, Lists.newArrayList(key), Lists.newArrayList(tag)); System.out.println(eval); }
4.2 可重入鎖
redis有類似Java 語言裡有個 ReentrantLock 就是可重入鎖嗎?
要支持可重入,需要對jedis 的 set 方法進行包裝,思路是:使用 Threadlocal 存儲當前持有鎖的計數。可重入鎖加重瞭客戶端的復雜性,精確一點還需要考慮內存鎖計數的過期時間,代碼復雜度將會繼續升高。
public class JedisWithReentrantLock { private Jedis jedis; /** * 當前線程的鎖及計數 */ private ThreadLocal<Map<String, Integer>> lockers = new ThreadLocal<>(); public JedisWithReentrantLock(Jedis jedis) { this.jedis = jedis; } private boolean set(String key) { return jedis.set(key, "", "nx", "ex", 5L) != null; } private void del(String key) { jedis.del(key); } private Map<String, Integer> getLockers() { Map<String, Integer> refs = lockers.get(); if (refs != null) { return refs; } lockers.set(Maps.newHashMap()); return lockers.get(); } public boolean lock(String key) { Map<String, Integer> refs = getLockers(); Integer refCount = refs.get(key); if (refCount != null) { refs.put(key, refCount + 1); return true; } if (!this.set(key)) { return false; } refs.put(key, 1); return true; } public boolean unlock(String key) { Map<String, Integer> refs = getLockers(); Integer refCount = refs.get(key); if (refCount == null) { return false; } refCount -= 1; if (refCount > 0) { refs.put(key, refCount); } else { refs.remove(key); this.del(key); } return true; } } @Test public void runJedisWithReentrantLock() { JedisWithReentrantLock redis = new JedisWithReentrantLock(jedis); System.out.println(redis.lock("alex")); System.out.println(redis.lock("alex")); System.out.println(redis.unlock("alex")); System.out.println(redis.unlock("alex")); }
4.3 集群環境的缺陷
在集群環境下,這種方式是有缺陷的(數據不一致的情況)。比如在 Sentinel 集群中,主節點掛掉時(原先第一個客戶端在主節點中申請成功瞭一把鎖),從節點A 會取而代之並晉升為主(但是這把鎖還沒有來得及同步),雖然客戶端上卻並沒有明顯感知,但是這時另一個客戶端過來請求 從節點A 可以成功加鎖,這樣就會導致系統中同樣一把鎖被兩個客戶端同時持有。
主從發生故障轉移,一般持續時間極短,數據不一致的情況基本上都是小概率事件。
4.4 Redlock
上面的集群同步問題導致的缺陷,難道就沒有解決方案嗎?
為此Antirez 發明瞭 Redlock 算法,它的流程比較復雜,不過已經有瞭很多開源的實現。
原理
使用 Redlock,需要提供多個 Redis 實例,這些實例之前相互獨立沒有主從關系。同很多分佈式算法一樣,redlock 也使用少數服從多數。
加鎖時,它會向過半節點發送 set(key, value, nx, ex) 指令,隻要過半節點 set 成功,那就認為加鎖成功。釋放鎖時,需要向所有節點發送 del 指令。缺陷:因為 Redlock 需要向多個節點進行讀寫,意味著相比單實例 Redis 性能會下降一些。
註:Redlock算法還需要考慮出錯重試、時鐘漂移等很多細節問題
使用場景
如果你很在乎高可用性,希望掛瞭一臺 redis 完全不受影響,那就應該考慮 redlock。
引用資料
How to do distributed locking
Redlock的實現
以上為個人經驗,希望能給大傢一個參考,也希望大傢多多支持WalkonNet。