在我寫這篇文章的時候,其實我還是挺糾結的,因為我這個方案本身也是雕蟲小技拿出來顯眼肯定會被貽笑大方,但是我最終還是拿出來與大家分享,我本著學習的態(tài)度和精神,希望大家能夠給與我指導和改進方案。
一、關于分布式鎖
關于分布式鎖,可能絕大部分人都會或多或少涉及到。 我舉二個例子:
場景一:從前端界面發(fā)起一筆支付請求,如果前端沒有做防重處理,那么可能在某一個時刻會有二筆一樣的單子同時到達系統(tǒng)后臺。
場景二:在App中下訂單的時候,點擊確認之后,沒反應,就又點擊了幾次。在這種情況下,如果無法保證該接口的冪等性,那么將會出現(xiàn)重復下單問題。 在接收消息的時候,消息推送重復。如果處理消息的接口無法保證冪等,那么重復消費消息產(chǎn)生的影響可能會非常大。
類似這種場景,我們有很多種方法,可以使用冪等操作,也可以使用鎖的操作。
我們先來解釋一下什么是冪等操作: 所謂冪等,簡單地說,就是對接口的多次調用所產(chǎn)生的結果和調用一次是一致的。擴展一下,這里的接口,可以理解為對外發(fā)布的HTTP接口或者Thrift接口,也可以是接收消息的內部接口,甚至是一個內部方法或操作。
在分布式環(huán)境中,網(wǎng)絡環(huán)境更加復雜, 因前端操作抖動、網(wǎng)絡故障、消息重復、響應速度慢等原因,對接口的重復調用概率會比集中式環(huán)境下更大,尤其是重復消息在分布式環(huán)境中很難避免。Tyler Treat也在《You Cannot Have Exactly-Once Delivery》一文中提到:
Within the context of a distributed system, you cannot have exactly-once message delivery.
分布式環(huán)境中,有些接口是天然保證冪等性的,如查詢操作。有些對數(shù)據(jù)的修改是一個常量,并且無其他記錄和操作,那也可以說是具有冪等性的。其他情況下,所有涉及對數(shù)據(jù)的修改、狀態(tài)的變更就都有必要防止重復性操作的發(fā)生。通過間接的實現(xiàn)接口的冪等性來防止重復操作所帶來的影響,成為了一種有效的解決方案。
于是我們根據(jù)以上內容就可以講一下使用分布式鎖的方法有哪些。
1、使用數(shù)據(jù)庫樂觀鎖,包括主鍵防重,版本號控制。但是這兩種方法各有利弊。
- 使用主鍵沖突的策略進行防重,在并發(fā)量非常高的情況下對數(shù)據(jù)庫性能會有影響,尤其是應用數(shù)據(jù)表和主鍵沖突表在一個庫的時候,表現(xiàn)更加明顯。其實針對是否會對數(shù)據(jù)庫性能產(chǎn)生影響這個話題,我也和一些專業(yè)的DBA同學討論過,普遍認可的是在mysql數(shù)據(jù)庫中采用主鍵沖突防重,在大并發(fā)情況下有可能會造成鎖表現(xiàn)象,比較好的辦法是在程序中生產(chǎn)主鍵進行防重。
- 使用版本號策略
這個策略源于mysql的mvcc機制,使用這個策略其實本身沒有什么問題,唯一的問題就是對數(shù)據(jù)表侵入較大,我們要為每個表設計一個版本號字段,然后寫一條判斷sql每次進行判斷。
2、Zookeeper防重策略
利用ZK確實是一個不錯的方案,流程如下:

以前的版本中普遍傳言說它的性能不好,但是后續(xù)的版本性能得到了較大提高,經(jīng)過系統(tǒng)壓測還是能夠支撐較大并發(fā)量的,經(jīng)過壓測三臺Zookeeper能搞住20000tps。
用zookeeper的優(yōu)點大概有:高可用、公平鎖、心跳保持鎖。
3、Redis防重策略
關于主從Redis方案最簡單的實現(xiàn)流程如下:

表面來看,這個方案似乎很管用,但是這里存在一個問題:在我們的系統(tǒng)架構里存在一個單點故障,如果Redis的master節(jié)點宕機了怎么辦呢?有人可能會說:加一個slave節(jié)點!在master宕機時用slave就行了!但是其實這個方案明顯是不可行的,因為這種方案無法保證第1個安全互斥屬性,因為Redis的復制是異步的。 總的來說,這個方案里有一個明顯的競爭條件(race condition),舉例來說:
- 客戶端A在master節(jié)點拿到了鎖。
- master節(jié)點在把A創(chuàng)建的key寫入slave之前宕機了。
- slave變成了master節(jié)點
- B也得到了和A還持有的相同的鎖(因為原來的slave里還沒有A持有鎖的信息)
于是我就在想,我該如何做才能讓Redis在分布式鎖這一塊能夠達到高可用呢?
于是基于Tedis的思想(http://www.oschina.net/p/tedis) 我自己寫了一套針對分布式鎖的雙寫Redis框架。
二、雙寫Redis的架構圖

說明:
組件名叫YeeRedisGroup,基本服務主要有四個,當數(shù)據(jù)到來的時候,會分別插入二個Redis服務,這二個Redis服務采用的是異地雙活的方案,當其中一個Redis服務掛了以后,會將這個Redis服務從可用隊列中摘除,放入重試隊列中,另一個Redis則會繼續(xù)使用。同樣讀取Redis的時候只會從可用隊列中讀取第一個Redis服務繼續(xù)讀取。
三、雙寫Redis的類圖結構

說明:這個圖其實沒什么可說的,大家自己看就可以了。
四、雙寫Redis的時序圖

說明:這個圖主要就是說明了整體系統(tǒng)交互流程是怎樣的。
五、故障容錯流程圖

六、故障重試流程圖

七、主動通知與主動查詢流程圖

八、Redis ????????????????????????可用隊列與重試隊列結構圖

九、項目闡述
上面項目的最終壓測結果還沒有出來,并且項目也還沒有最終開源,在此我是非常想和大家進行交流,我覺得可能的缺點是客戶端相對比較重,有大量的邏輯都在客戶端上面進行,從我個人是本著交流與學習的態(tài)度勇于接受各種批評與指正,謝謝。