作者:踩刀詩人 www.cnblogs.com/chopper-poet/p/10802242.html
前言
提到資料一緻性、操作原子性,諸如此類的一些與并發有關的詞彙時不知道你第一時間會聯想到什麼呢?我相信大多數人可能會想到“鎖”,為什麼是鎖呢,這個我不多說,大家心裡應該都明白。
在單體應用時代,我們使用jvm提供的鎖就可以很好的工作,但是到了分布式應用時代,jvm提供的鎖就行不通了,那麼勢必要借助一些跨jvm的臨界資源來支援鎖的相關語義,比如redis,zookeeper等。
步入正題
我今天就來分享下我司基于redis來實作的分布式鎖,2013年投入使用,也算是久經沙場。
但是也存在一些設計上的缺陷,這個我後面也會提到,希望大家秉着互相學習的态度文明交流,别一上來就說這不行那不行,還是那句話“适合自己的才是最好的”。
加鎖過程分析
我第一次讀代碼的時候,有這麼幾個疑惑:
Q1:為什麼不使用 SET key value [expiration EX seconds|PX milliseconds] [NX|XX] 這個指令來實作key的自動過期呢,反而放到應用代碼判斷key是否過期?
A1:我們的分布式鎖開發的時候SET指令還不支援NX、PX,是以才想出這種辦法來實作key過期,NX、PX在2.6.12以後開始支援;
Q2:已經判斷了目前key對應的時間戳已經過期了,為什麼還要使用getset再擷取一次呢,直接使用set指令覆寫不可以嗎?
A2:這裡其實牽扯到并發的一些事情,如果直接使用set,那有可能多個用戶端會同時擷取到鎖,如果使用getset然後判斷舊值是否過期就不會有這個問題,設想一下如下場景:
1、C1加鎖成功,不巧的是,這時C1意外的奔潰了,自然就不會釋放鎖;
2、C2,C3嘗試加鎖,這時key已存在,是以C2,C3去判斷key是否已過期,這裡假設key已經過期了,是以C2,C3使用set指令去設定值,那兩個都會加鎖成功,這就闖大禍了;
如果使用getset指令,然後判斷下傳回值是否過期就可以避免這種問題,假如C2跑的快,那C3判斷傳回的時間戳已經過期,自然就加鎖失敗;
釋放鎖過程分析
Q1:為什麼釋放鎖時還需要判斷key是否過期呢,直接del不是性能更高嗎?
A1:考慮這樣一種場景:
1、C1擷取鎖成功,開始執行自己的操作,不幸的是C1這時被阻塞了;
2、C2這時來擷取鎖,由于C1被阻塞了很長時間,是以key對應的value已經過期了,這時C2通過getset加鎖成功;
3、C1塵封了太久終于被再次喚醒,對于釋放鎖這件事它可是認真的,伴随着一波del操作,悲劇即将發生;
4、C3來擷取鎖,好家夥,居然一下就成功了,接着就是一波操作猛如虎,接着就是一堆的客訴過來了;
為什麼會這樣呢?回想C1被喚醒以後的事情,居然敢直接del,C2活都沒幹完呢,鎖就被C1給釋放了,這時C3來直接就加鎖成功,是以為了安全起見C3釋放鎖時得分成兩步:
1.判斷value是否已經過期
2.如果已過期直接忽略,如果沒過期就執行del。
這樣就真的安全了嗎?安全了嗎?安全了嗎?
假如第一步和第二步之間相隔了很久是不是也會出現鎖被其他人釋放的問題呢?是吧?是的!
有沒有别的解決辦法呢?聽說借助lua就可以解決這個問題了。
正視自己的缺點
Q1:Redis鎖的過期時間小于業務的執行時間該如何續期?
A1:這個暫時沒有實作,據說有一個叫Redisson的家夥解決了這個問題,我們也有部分業務在使用,未來有可能會切換到Redisson。
Q2:怎麼實作的高可用?
A2:我們采用Failover機制,初始化redis鎖的時候會維護一個redis連接配接池,加鎖或者釋放鎖的時候采用多寫的方式來保障一緻性,如果某個節點不可用的時候會自動切換到其他節點,但是這種機制可能會導緻多個用戶端同時擷取到鎖的情況,考慮這種情況:
1、C1去redis1加鎖,加鎖成功後會寫到redis2,redis3;
2、C2也去redis1加鎖,但是此時C2到redis1的網絡出現問題,這時C2切換到redis2去加鎖,由于第一步中的redis多寫并不是原子的,所有就有可能導緻C2也擷取鎖成功;
針對這種情況,目前有些業務方是通過資料庫唯一索引的方式來規避的,未來會修複這個bug,具體方案目前還沒有。