redis 分布式锁 看门狗_漫谈分布式锁之Redis实现
筆耕墨耘,深研術道。
01寫在前面Redis是一個高性能的內存數據庫,常用于數據庫、緩存和消息中間件。它提供了豐富的數據結構,更適合各種業務場景;基于AP模型,Redis保證了其高可用和高性能。本文主要內容:
Redis實現分布式鎖的依據
基于setnx +?expire的不斷探索
分布式利器—Redisson
Redlock算法及其爭議
Redis分布式鎖的實現的核心依據:redis的單線程模型,保證最終只有一個setnx命令可以執行成功;
如何理解Redis的單線程模型(選讀)?
Redis基于Reactor模式開發了自己網絡事件處理器:這個處理器被稱為文件事件處理器(file event handler):文件事件處理器使用I/O多路復用(multiplexing)程序來同時監聽多個套接字,并根據套接字目前執行的任務來為套接字關聯不同的事件處理器;
當被監聽的套接字準備好執行連接應答(accept)、讀取(read)、寫入(write)、關閉(close)等操作時,與操作相對應的文件事件就會產生,這時文件事件處理器就會調用套接字之前關聯好的事件處理器來處理這些事件。
注:文件事件處理器是統稱,是網絡事件處理的模塊(或者說方案),其包括四個組成部分:套接字、I/O多路復用程序、文件事件分派器(dispatcher),以及事件處理器。
文件事件處理器四個組成部分如下:文件事件處理器的四個組成部分
I/O多路復用程序負責監聽多個套接字,對于產生了事件的套接字,將它們放入一個隊列里面,然后通過這個隊列,以有序、同步、每次一個套接字的方式向文件事件分派器傳送套接字(單線程模型);
當上一個套接字產生的事件被處理完畢之后,I/O多路復用程序才會繼續向文件事件分派器傳送下一個套接字;
文件事件分派器接受I/O多路復用程序傳來的套接字,并根據套接字產生的事件類型,調用相應的事件處理器;
這些事件處理器是一個個函數,它們會執行相應的操作。
基本實現代碼:這里給出了阻塞和非阻塞兩種方式。
public interface DistributedLock { ? ?/** ? ? * 阻塞 ? ? * ? ? * @param lockKey ? ? * @param timeout ? ? */????void?lock(String?lockKey,?long?timeout); ? ?/** ? ? * 非阻塞 ? ? * ? ? * @param lockKey ? ? * @param timeout ? ? * @return ? ? */????boolean?tryLock(String?lockKey,?long?timeout); ? ?/** ? ? * 釋放鎖 ? ? * ? ? * @param lockKey ? ? */????void?unlock(String?lockKey);}利用stringRedisTemplate實現,以下代碼并不嚴謹,僅僅是演示某些問題。
@Override public void lock(String lockKey, long timeout) { BoundValueOperations<String, String> boundValueOps = stringRedisTemplate.boundValueOps(lockKey); while (true) { Boolean b = boundValueOps.setIfAbsent("1"); if (b) { Boolean expire = boundValueOps.expire(timeout, TimeUnit.SECONDS); if (expire) { return; } } // 休眠1s后再重試 LockSupport.parkNanos(1*1000*1000); } } @Override public boolean tryLock(String lockKey, long timeout) { BoundValueOperations<String, String> boundValueOps = stringRedisTemplate.boundValueOps(lockKey); Boolean b = boundValueOps.setIfAbsent("1"); if (b) { Boolean expire = boundValueOps.expire(timeout, TimeUnit.SECONDS); if (expire) { return true; } } return false; } @Override public void unlock(String lockKey) { stringRedisTemplate.delete(lockKey); }上述代碼,是一種簡單的分布式鎖的實現,但是問題也較明顯:
非原子性:setnx + expire兩條命令非原子操作,有可能導致死鎖;
鎖誤解除:B線程加的鎖,可能被A線程釋放掉,導致鎖失效;
超時并發:業務執行時間超過鎖超時時間,導致鎖失效,產生并發安全問題;
不可重入:不支持可重入;
集群問題:Redis哨兵模式和集群模式帶來的問題,主從發生failover時候帶來的鎖失效。
針對以上問題,提供以下解決方案:
Q1非原子性方案一:在高版本redis中(Redis 2.6.12以后),官方完善了setnx:SET key value [EX seconds] [PX milliseconds] [NX|XX];
方案二:使用LUA腳本,如下:
如下圖:
當鎖超時釋放后,會有其他線程爭搶鎖。此時,會發生線程A釋放了線程B占用鎖的情況,并且會導致鎖失效產生并發問題(圖中棕色所示)。針對鎖誤解除的問題,可以:加鎖時候,設置value值,這個value值來標記當前線程,嚴格來說,該值在所有客戶端和所有鎖定請求中必須唯一。
This?value?must?be?unique?across?all?clients?and?all?lock?requests.
在解鎖的時候判斷解鎖線程是否是占有鎖的線程,出于安全性(原子性)考慮,這段解鎖邏輯使用LUA腳本編寫。如下:
Basically?the?random?value?is?used?in?order?to?release?the?lock?in?a?safe?way,?with?a?script?that?tells?Redis:?remove?the?key?only?if?it?exists?and?the?value?stored?at?the?key?is?exactly?the?one?I?expect?to?be.
if redis.call("get",KEYS[1]) == ARGV[1] then ? ?return redis.call("del",KEYS[1])else ? ?return 0end針對鎖超時帶來的并發問題,可以:
為獲取鎖的線程(圖中線程A)設置一個守護線程,守護線程周期性地給當前鎖續期,當線程A執行完成任務,會顯示關閉守護線程;即使線程A掛掉,由于線程A和守護線程在同一個進程,守護線程也會停下。這把鎖到了超時的時候,沒人給它續命,也就自動釋放了。如下圖:補充:Redisson的鎖續期實現,基于netty的時間輪算法。
Q4不可重入參考Redisson利用hash數據結構實現對線程的重入計數。這個問題將在Redisson分布式鎖源碼分析里面講述。
Q5集群問題我們知道Redis的主從復制是異步的,主從發生failover時將帶來鎖失效問題。
What?happens?if?the?Redis?master?goes?down??Well,?let’s?add?a?slave!?And?use?it?if?the?master?is?unavailable.?This?is?unfortunately?not?viable.?By?doing?so?we?can’t?implement?our?safety?property?of?mutual?exclusion,?because?Redis?replication?is?asynchronous.
如上圖所示:
左邊client在Master獲取到鎖;
在將鎖信息同步到slave之前,master掛掉,此時發生failover;
- slave節點升級為新的master(New Master),此時,其他線程來獲取鎖,發現并沒有其他線程占用,也加鎖成功。這導致了鎖失效。
可以使用Redlock算法。
We?propose?an?algorithm,?called?Redlock,?which?implements?a?DLM?which?we?believe?to?be?safer?than?the?vanilla?single?instance?approach.
04分布式利器—RedissonRedisson是一個在Redis的基礎上實現的Java駐內存數據網格(In-Memory Data Grid)。它不僅提供了一系列的分布式的Java常用對象,還提供了許多分布式服務。這里我們關注其對分布式鎖的支持,它解決了我們上面論述的幾種問題。
概覽Redisson鎖的實現
如上圖Redisson的類圖,總結常用對象:
可重入鎖(Reentrant Lock)
公平鎖(Fair Lock)
聯鎖(MultiLock)
紅鎖(RedLock)
讀寫鎖(ReadWriteLock)
信號量(Semaphore)
這里我們以RedissonLock(最常用的)為例,了解下其常用的一些api:
public void lock();//?不建議使用,鎖續期僅僅在leaseTime = -1時生效public void lock(long leaseTime, TimeUnit unit);public boolean tryLock();// 不建議使用,鎖續期僅僅在leaseTime = -1時生效public boolean tryLock(long waitTime, long leaseTime, TimeUnit unit);public boolean tryLock(long waitTime, TimeUnit unit);上述api的實際使用,這里不再演示,相對比較簡單,可以通過其官網學習:
https://github.com/redisson/redisson/wiki
注意事項
鎖續期生效的場景:鎖續期僅僅在leaseTime = -1時生效;
鎖【lock.lock()】的使用必須緊跟try代碼塊,且unlock要放到finally塊第一行。這點是阿里規范,說明如下:
Redlock算法
為了使用Redlock算法,需要提供多個Redis實例,并且這些實例之間相互獨立,沒有主從關系(不會有數據的同步等)。同很多分布式算法一樣,Redlock也使用大多數機制。加鎖時候,它會向過半節點發送set(key,value,nx=True,ex=xxx)指令,只要過半節點set成功,就認為加鎖成功。釋放鎖時候,需要向所有節點發送del指令。不過Redlock算法還需要考慮超時問題、出錯重試、時鐘漂移等很多細節問題,同時因為Redlock需要向多個節點進行讀寫,意味著其相比單實例的Redis的性能會下降一些。
Redlock算法的爭議
可以參閱:[1] https://redis.io/topics/distlock
[2] http://martin.kleppmann.com/2016/02/08/how-to-do-distributed-locking.html
[3] http://antirez.com/news/101
06總結至此,簡單介紹了分布式鎖基于Redis的實現。實際項目中建議使用Redisson提供的鎖來保證臨界資源的安全性。對于追求業務強一致的業務場景,可以利用分布式協調器來實現,如基于zookeeper的實現,這將在后面提到。07引用[1] https://redis.io/topics/distlock[2] https://github.com/redisson/redisson/wiki
[3] https://mp.weixin.qq.com/s/8fdBKAyHZrfHmSajXT_dnA
[4] https://learnku.com/articles/47769
[5] 黃健宏,《Redis設計與實現》,機械工業出版社
[6] Josiah?L. Carlson,黃健宏,《Redis實戰》,中國工信出版社
[7] 錢文品,《Redis深度歷險》,中國工信出版社
發現“在看”和“贊”了嗎,戳我試試吧總結
以上是生活随笔為你收集整理的redis 分布式锁 看门狗_漫谈分布式锁之Redis实现的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 纸板箱机器人制作比例图纸_造一个黄油机器
- 下一篇: 用paddleocr识别汉字_汉字设计中