做者:踩刀詩人html
www.cnblogs.com/chopper-poet/p/10802242.html面試
前言redis
提到數據一致性、操做原子性,諸如此類的一些與併發有關的詞彙時不知道你第一時間會聯想到什麼呢?我相信大多數人可能會想到「鎖」,爲何是鎖呢,這個我很少說,你們內心應該都明白。在單體應用時代,咱們使用jvm提供的鎖就能夠很好的工做,可是到了分佈式應用時代,jvm提供的鎖就行不通了,那麼勢必要藉助一些跨jvm的臨界資源來支持鎖的相關語義,好比redis,zookeeper等。數據庫
步入正題安全
我今天就來分享下我司基於redis來實現的分佈式鎖,2013年投入使用,也算是久經沙場。可是也存在一些設計上的缺陷,這個我後面也會提到,但願你們秉着互相學習的態度文明交流,別一上來就說這不行那不行,仍是那句話「適合本身的纔是最好的」。網絡
加鎖過程分析併發
我第一次讀代碼的時候,有這麼幾個疑惑:框架
Q1:爲何不使用 SET key value [expiration EX seconds|PX milliseconds] [NX|XX] 這個指令來實現key的自動過時呢,反而放到應用代碼判斷key是否過時?jvm
A1:咱們的分佈式鎖開發的時候SET命令還不支持NX、PX,因此纔想出這種辦法來實現key過時,NX、PX在2.6.12之後開始支持;分佈式
Q2:已經判斷了當前key對應的時間戳已通過期了,爲何還要使用getset再獲取一次呢,直接使用set指令覆蓋不能夠嗎?
A2:這裏其實牽扯到併發的一些事情,若是直接使用set,那有可能多個客戶端會同時獲取到鎖,若是使用getset而後判斷舊值是否過時就不會有這個問題,設想一下以下場景:
C1加鎖成功,不巧的是,這時C1意外的奔潰了,天然就不會釋放鎖;
C2,C3嘗試加鎖,這時key已存在,因此C2,C3去判斷key是否已過時,這裏假設key已通過期了,因此C2,C3使用set指令去設置值,那兩個都會加鎖成功,這就闖大禍了;若是使用getset指令,而後判斷下返回值是否過時就能夠避免這種問題,假如C2跑的快,那C3判斷返回的時間戳已通過期,天然就加鎖失敗;
釋放鎖過程分析
Q1:爲何釋放鎖時還須要判斷key是否過時呢,直接del不是性能更高嗎?
A1:考慮這樣一種場景:
C1獲取鎖成功,開始執行本身的操做,不幸的是C1這時被阻塞了;
C2這時來獲取鎖,因爲C1被阻塞了很長時間,因此key對應的value已通過期了,這時C2經過getset加鎖成功;
C1塵封了過久終於被再次喚醒,對於釋放鎖這件事它但是認真的,伴隨着一波del操做,悲劇即將發生;
C3來獲取鎖,好傢伙,竟然一下就成功了,接着就是一波操做猛如虎,接着就是一堆的客訴過來了;
爲何會這樣呢?回想C1被喚醒之後的事情,竟然敢直接del,C2活都沒幹完呢,鎖就被C1給釋放了,這時C3來直接就加鎖成功,因此爲了安全起見C3釋放鎖時得分紅兩步:1.判斷value是否已通過期 2.若是已過時直接忽略,若是沒過時就執行del。這樣就真的安全了嗎?安全了嗎?安全了嗎?假如第一步和第二步之間相隔了好久是否是也會出現鎖被其餘人釋放的問題呢?是吧?是的!有沒有別的解決辦法呢?據說藉助lua就能夠解決這個問題了。
正視本身的缺點
Q1:Redis鎖的過時時間小於業務的執行時間該如何續期?
A1:這個暫時沒有實現,聽說有一個叫Redisson的傢伙解決了這個問題,咱們也有部分業務在使用,將來有可能會切換到Redisson。
Q2:怎麼實現的高可用?
A2:咱們採用Failover機制,初始化redis鎖的時候會維護一個redis鏈接池,加鎖或者釋放鎖的時候採用多寫的方式來保障一致性,若是某個節點不可用的時候會自動切換到其餘節點,可是這種機制可能會致使多個客戶端同時獲取到鎖的狀況,考慮這種狀況:
C1去redis1加鎖,加鎖成功後會寫到redis2,redis3;
C2也去redis1加鎖,可是此時C2到redis1的網絡出現問題,這時C2切換到redis2去加鎖,因爲第一步中的redis多寫並非原子的,全部就有可能致使C2也獲取鎖成功;
針對這種狀況,目前有些業務方是經過數據庫惟一索引的方式來規避的,將來會修復這個bug,具體方案目前尚未。
【推薦閱讀】
[技術]:面試官問你MySQL的優化,看這篇文章就夠了
[技術]:IntelliJ IDEA快捷鍵終極大全,速度收藏!