java如何保证redis设置过期时间的原子性_redis专题系列22 -- 如何优雅的基于redis实现分布式锁
幾個概念
線程鎖:主要用來給方法、代碼塊加鎖。當某個方法或代碼使用鎖,在同一時刻僅有一個線程執行該方法或該代碼段。線程鎖只在同一JVM中有效果,因為線程鎖的實現在根本上是依靠線程之間共享內存實現的,比如synchronized是共享對象頭,顯示鎖Lock是共享某個變量(state)。
進程鎖:為了控制同一操作系統中多個進程訪問某個共享資源,因為進程具有獨立性,各個進程無法訪問其他進程的資源,因此無法通過synchronized等線程鎖實現進程鎖。
分布式鎖:當多個進程不在同一個系統中,用分布式鎖控制多個進程對資源的訪問。
合理的分布式鎖應該具有哪些特性
在分布式系統環境下,一個方法在同一時間只能被一個機器的一個線程執行高可用的獲取鎖與釋放鎖高性能的獲取鎖與釋放鎖具備可重入特性(可理解為重新進入,由多于一個任務并發使用,而不必擔心數據錯誤)具備鎖失效機制,防止死鎖具備非阻塞鎖特性,即沒有獲取到鎖將直接返回獲取鎖失敗目前可供選擇的分布式鎖技術方案
1.利用數據庫本身的樂觀鎖(新增字段version,每次更新版本號+1,參考mysql的MVCC機制)或者排他鎖來實現(for update)
2.利用 Zookeeper 的順序臨時節點,來實現分布式鎖和等待隊列。Zookeeper 設計的初衷,就是為了實現分布式鎖服務的。
3.利用redis的SET my_key my_value NX PX milliseconds命令實現或者內嵌的lua腳本,鑒于redis的特性,他們都具有原子性
4.利用 Memcached 的 add 命令。此命令是原子性操作,只有在 key 不存在的情況下,才能 add 成功,也就意味著線程得到了鎖。
5.Chubby:Google 公司實現的粗粒度分布式鎖服務,底層利用了 Paxos 一致性算法。
6.基于 Consul 做分布式鎖,主要利用 Consul 的 Key / Value 存儲 API 中的 acquire 和 release 操作來實現
談談如何優雅的通過redis實現分布式鎖
在設計redis分布式鎖,我們要考慮的點:
1.命令具有原子性,不允許多個客戶端可以同時執行同一條指令,包含加鎖和釋放鎖的命令
2.鎖超時,針對鎖應該設置超時時間防止單點客戶端宕機后鎖得不到釋放造成其他等待鎖的線程無限等待
3.鎖續約,簡單來說,假設我們給鎖設置的失效時間為2s,但業務執行完畢需要3s,導致其他線程過早拿到鎖,可能對臨界區資源造成數據安全問題,同時,執行unlock的時候,釋放掉了其他進程持有的鎖。
大概思維導圖如下:
下面拆分:
1.加鎖
加鎖實際上就是在redis中,給Key鍵設置一個值,為避免死鎖,并給定一個過期時間
SET lock_key random_value NX PX 5000
值得注意的是:
random_value 是客戶端生成的唯一的字符串(可以參考分布式id算法,像UUID,Snowflake等等)。
NX 代表只在鍵不存在時,才對鍵進行設置操作。
PX 5000 設置鍵的過期時間為5000毫秒。
這樣,如果上面的命令執行成功,則證明客戶端獲取到了鎖。
2.解鎖
解鎖的過程就是將Key鍵刪除。但也不能亂刪,不能說客戶端1的請求將客戶端2的鎖給刪除掉。這時候random_value的作用就體現出來。
為了保證解鎖操作的原子性,我們用LUA腳本完成這一操作。先判斷當前鎖的字符串是否與傳入的值相等,是的話就刪除Key,解鎖成功。
3.關鍵代碼
加鎖
解鎖
至于守護線程,可以參考JDK的ScheduleService,根據key的TTL創建定時任務去監測當前的業務的執行時間從而判斷是否決定鎖續約,當然還有其他很多方法,這只是一個參考。
總結:基于上面的實現,我們基本上實現了單機的redis分布式鎖,但它依然有個明顯的缺點,即不可重入,如果同一個進程進行多次加鎖,則顯得有點捉襟見肘了。其實,參看JDK的重入鎖,我們也可以輕松的設計出redis分布式鎖,下面我們來了解一下Redisson.
Redisson分布式鎖
Redisson為我們提供了更好的實現,幾門滿足了分布式鎖的所有特性。我們來了解一下
Redisson是架設在Redis基礎上的一個Java駐內存數據網格(In-Memory Data Grid)。充分的利用了Redis鍵值數據庫提供的一系列優勢,基于Java實用工具包中常用接口,為使用者提供了一系列具有分布式特性的常用工具類。使得原本作為協調單機多線程并發程序的工具包獲得了協調分布式多機多線程并發系統的能力,大大降低了設計和研發大規模分布式系統的難度。同時結合各富特色的分布式服務,更進一步簡化了分布式環境中程序相互之間的協作。
Redisson可謂是強大到不可思議,幾乎包含了我們想要的關于分布式鎖想要的一切特性,拆箱即用,非常方便,而且鎖的種類繁多,像可重入鎖,聯鎖,紅鎖,公平鎖,信號量,可過期信號量,閉鎖等等。本文就結合源碼大概講解一下redisson基于JDK重入鎖實現的分布式鎖,希望你能get到它的思想并運用到項目中。
先給一下Maven配置:
org.redisson redisson 3.11.6代碼簡單示例:
源碼解析:
1.加鎖
RLock lock = client.getLock("lock1"); 這句代碼就是為了獲取鎖的實例,然后我們可以看到它返回的是一個RedissonLock對象。加鎖的代碼都是lockInterruptibly 方法提供支持的,我們只分析這個方法
如上代碼,就是加鎖的全過程。先調用tryAcquire來獲取鎖,如果返回值ttl為空,則證明加鎖成功,返回;如果不為空,則證明加鎖失敗。這時候,它會訂閱這個鎖的Channel,等待鎖釋放的消息,然后重新嘗試獲取鎖。流程如下:
接下來就要看tryAcquire方法
在這里,它有兩種處理方式,一種是帶有過期時間的鎖,一種是不帶過期時間的鎖。接著往下看,tryLockInnerAsync方法是真正執行獲取鎖的邏輯,它是一段LUA腳本代碼。在這里,它使用的是hash數據結構。同時注意scheduleExpirationRenewal,redisson就是通過它進行所續約的。
這段LUA代碼看起來并不復雜,有三個判斷:
- 通過exists判斷,如果鎖不存在,則設置值和過期時間,加鎖成功
- 通過hexists判斷,如果鎖已存在,并且鎖的是當前線程,則證明是重入鎖,加鎖成功
- 如果鎖已存在,但鎖的不是當前線程,則證明有其他線程持有鎖。返回當前鎖的過期時間,加鎖失敗
加鎖成功后,在redis的內存數據中,就有一條hash結構的數據。Key為鎖的名稱;field為隨機字符串+線程ID;值為1。如果同一線程多次調用lock方法,值遞增1。這正好吻合了JDK重入鎖的設計思想。
2.解鎖
通過調用unlock方法來解鎖
然后我們再看unlockInnerAsync方法。這里也是一段LUA腳本代碼。
如上代碼,就是釋放鎖的邏輯。同樣的,它也是有三個判斷:
- 如果鎖已經不存在,通過publish發布鎖釋放的消息,解鎖成功
- 如果解鎖的線程和當前鎖的線程不是同一個,解鎖失敗,拋出異常
- 通過hincrby遞減1,先釋放一次鎖。若剩余次數還大于0,則證明當前鎖是重入鎖,刷新過期時間;若剩余次數小于0,刪除key并發布鎖釋放的消息,解鎖成功
這樣,關于redisson重入鎖的加鎖解鎖都已經分析完了。注意,redisson的鎖續約是采用了watchdog機制,其實它就是一個定時調度任務,如果你沒有設置鎖的過期時間,redisson會給一個默認值30s,這樣看門狗就會每10s進行一次續約,保證鎖一致持有在手中,當然它依然有自己的熔斷機制,假設持有鎖的線程進入了死循環,在幾次續約后便不再續約。同樣,在解鎖時,取消了鎖續約機制。
總結
1.上面介紹的均為單機模式下的分布式鎖實現方式,如果強行使用到集群模式下存在一定的風險,我們知道redis集群模式下使用的主從復制模式(異步),如果客戶端在加鎖成功的一剎那,master節點故障,導致slave節點沒有同步到對應的key值,可能存在多個客戶端獲取通一把鎖。至于集群模式下分布式鎖如何實現和原理細節,請參考http://redis.cn/topics/distlock.html。
2.同時對于redisson,可重入鎖只是其冰山一角,如果感興趣的話可以去官網了解其架構和底層原理,本文不宜過多講解。github地址:https://github.com/redisson/redisson。
附上redisson架構圖:
《新程序員》:云原生和全面數字化實踐50位技術專家共同創作,文字、視頻、音頻交互閱讀總結
以上是生活随笔為你收集整理的java如何保证redis设置过期时间的原子性_redis专题系列22 -- 如何优雅的基于redis实现分布式锁的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: python如何安装matplotlib
- 下一篇: ont维修使能工具_上海OTC机器人维修