目前開發過程當中,按照公司規範,須要依賴框架中的緩存組件。不得不說,作組件的大牛對CRUD操做的封裝,鏈接池、緩存路由、緩存安全性的管控都處理的無可挑剔。可是有一個小問題,該組件沒有對分佈式鎖作實現,那就要想辦法依靠緩存組件本身去實現一個分佈式鎖了。redis
什麼,爲啥要本身實現?有現成的開源組件直接拿過來用不就好了,好比Spring-Integration-Redis提供RedisLockRegistry,Redisson,不比本身去實現快的多。那我得聲明一下,本人也不喜歡重複造輪子。具體緣由呢,首先是項目中的緩存組件是不能替換的,鏈接池還可能沒有辦法複用,其次就是若是對開源組件實現原理不熟悉,那麼出了問題,維護起來又須要更多成本。spring
先說一下當前須要分佈式鎖的兩個場景,一個是微信端access_token刷新(分佈式鎖能夠保證access_token只刷新一次,刷新完成以後放入緩存,其餘請求直接從緩存讀取);一個是分佈式部署的定時任務(分佈式鎖能夠保證同一時刻只有一個節點的定時任務執行)。sql
在單機部署的狀況下,要想保證特定業務在順序執行,經過JDK提供的synchronized關鍵字、Semaphore、ReentrantLock,或者咱們也能夠基於AQS定製化鎖。單機部署的狀況下,鎖是在多線程之間共享的,可是分佈式部署的狀況下,鎖是多進程之間共享的。那麼分佈式鎖要保證鎖資源的惟一性,能夠在多進程之間共享。數據庫
RedisLockRegistry是spring-integration-redis中提供redis分佈式鎖實現類。主要是經過redis鎖+本地鎖雙重鎖的方式實現的一個比較好的鎖。小程序
OBTAIN_LOCK_SCRIPT是一個上鎖的lua腳本。KEYS[1]表明當前鎖的key值,ARGV[1]表明當前的客戶端標識,ARGV[2]表明過時時間。微信小程序
基本邏輯是:根據KEYS[1]從redis中拿到對應的客戶端標識,如已存在的客戶端標識和ARGV[1]相等,那麼重置過時時間爲ARGV[2];若是值不存在,設置KEYS[1]對應的值爲ARGV[1],而且過時時間是ARGV[2]。api
獲取鎖的過程也很簡單,首先經過本地鎖(localLock,對應的是ReentrantLock實例)獲取鎖,而後經過RedisTemplate執行OBTAIN_LOCK_SCRIPT腳本獲取redis鎖。緩存
爲何要使用本地鎖呢,首先是爲了鎖的可重入,其次是減輕redis服務壓力。安全
釋放鎖的過程也比較簡單,第一步經過本地鎖判斷當前線程是否持有鎖,第二步經過本地鎖判斷當前線程持有鎖的計數。微信
若是當前線程持有鎖的計數 > 1,說明本地鎖被當前線程屢次獲取,這時只釋放本地鎖(釋放以後當前線程持有鎖的計數-1)。
若是當前線程持有鎖的計數 = 1,釋放本地鎖和redis鎖。
RedisLockRegistry使用如上所示。
首先定義RedisLockRegistry對應的Bean,須要依賴redis的ConnectionFactory。
而後在服務層中注入RedisLockRegistry實例。
經過lock方法和unlock方法將業務邏輯包起來,須要注意的是unlock方法要寫在finally代碼塊中。
Redisson是架設在Redis基礎上的一個Java駐內存數據網格(In-Memory Data Grid)。
充分的利用了Redis鍵值數據庫提供的一系列優點,基於Java實用工具包中經常使用接口,爲使用者提供了一系列具備分佈式特性的經常使用工具類。
使得本來做爲協調單機多線程併發程序的工具包得到了協調分佈式多機多線程併發系統的能力,大大下降了設計和研發大規模分佈式系統的難度。
同時結合各富特點的分佈式服務,更進一步簡化了分佈式環境中程序相互之間的協做。
首先感覺一下經過Redisson Api使用redis分佈式鎖。
定義RedissonBuilder,經過redis集羣地址構建RedissonClient。
定義RedissonClient類型的Bean。
業務代碼裏,經過RedissonClient獲取分佈式鎖。
因爲對Redisson分佈式鎖實現原理了解的也不是很透徹,這裏推薦一篇文章:Redisson 分佈式鎖實現分析。
本地鎖(ReentrantLock)+ redis鎖
每個lock key對應惟一的一個本地鎖
分佈式環境下,每個線程對應一個惟一標識
經過JDK ConcurrentTaskScheduler完成定時任務執行,ScheduledFuture完成定時任務銷燬。其中taskId對應線程標識。
經過RedisLock註解實例lockInfo獲取到鎖key值、鎖過時時間信息。
定義測試類,測試方法註上@RedisLock註解,制定鎖的key值爲 "redis-lock-test",測試方法內隨機休眠。
開啓20個線程,同時調用測試方法。
多線程redis分佈式鎖測試結果以下。
定義可重入測試類,方法內獲取當前代理對象,遞歸調用測試方法。
測試方法中,調用可重入測試類注有@RedisLock的測試方法。
分佈式鎖可重入測試結果以下。
refreshAccessToken方法上標註@RedisLock註解,代表此方法在分佈式環境下會串行執行。
首先從緩存裏獲取access_token。
若是緩存裏的access_token爲空或者和失效的access_token相等,經過TokenAPI生成新的access_token並放入緩存。
若是緩存裏的access_token不爲空而且和失效的access_token不相等,直接返回緩存裏的access_token。
若是緩存中的access_token爲空,直接刷新access_token並放入緩存。
若是緩存中的access_token不爲空且和失效的access_token相等則刷新access_token並放入緩存,不然直接返回緩存中的access_token。
在分佈式環境下,涉及線程間併發問題和進程間併發問題都是能夠經過分佈式鎖解決的。若是是單節點線程之間共享資源的併發問題能夠經過JDK提供的線程鎖來解決,若是是多節點多線程之間共享資源的併發問題就須要藉助分佈式鎖。好比最多見的秒殺、搶紅包,後臺服務中涉及到庫存扣減、金額扣減、以及其餘高併發串行化場景的操做均可用分佈式鎖來解決問題。本文講述的例子主要是應用在微信公衆號和微信小程序access_token刷新、微信分享jsapi_ticket刷新,分佈式鎖能夠保證access_token和jsapi_ticket在高併發下只有一個線程去執行刷新動做,避免屢次刷新後access_token或者jsapi_ticket失效的問題。