前言
在我們學習多執行緒開發的時候,線上程同時針對同一個資源進行操作的時候都需要加鎖;一般會用到reentrantLock和synchronized兩種鎖方案,至於他們之間的區別也是面試的時候經常問到的,小夥伴們可自行網補。這裡介紹企業經常用到的另一種鎖,分散式鎖。大家肯定聽說過,但是就不一定用對哦。今天就深入的介紹一下分散式鎖方案的演變。
常見用法
我們也不免俗套來舉個併發扣除庫存的例子
我們來看一下程式碼
//扣除商品庫存
//產品id: productId
//扣除數量: count
public void reduce(int productId,int count){
//步驟1 從資料庫獲得產品實體
Product product = getProduct(productId);
//步驟2 獲得當前庫存數量
int stockCount = product.getStock();
if(stockCount >= count){
//步驟3 扣除庫存
product.setStock(stockCount - count);
//步驟4 把產品實體更新到資料庫
productService.update(product);
log.info("購買成功!")
}else{
log.info("庫存不足,無法購買!")
}
}
購買場景
當前產品的庫存數為10
請求A買了2個產品,那應該扣除2
請求B買了3個產品,那應該再扣除3
那最終的庫存剩餘為5
上面程式碼在分散式環境中,只要稍微流量大點,這邊就會出現扣減庫存不是預期的情況。原因就是
兩個請求同時到來時,都同時執行了步驟1,在同一時刻都獲取到了同一個產品庫存當前庫存都為10;但在步驟3的時候都是用10減count值,那麼不管是請求A和請求B哪個先執行步驟4,庫存剩餘要麼剩餘是8或者7;都不是最終的5。
原因知道了,那怎麼解決?小夥伴想到的就是弄個鎖,而且還要分散式鎖。
分散式鎖登場
上面的問題很多小夥伴應該都知道要用分散式鎖,那用什麼技術方案呢?我相信很多小夥伴都會說用redis方案,很簡單setnx就行了。
setnx命令 是redis的一條原生命令大意為 set if not exists, 在指定的key不存在的情況下,命令執行成功,如果key存在就命令執行不成功。
這個方案是很多公司都這麼用的,那我們調整一下程式碼
需要考慮到一些業務異常,需要把鎖釋放掉,加上try/finally,這個千萬不要忘了
當是還是有一些問題,就是如果加鎖成功後,業務沒有完成。突然斷電或者運維人員用kill -9命令把執行緒刪除了;那就導致了鎖一直沒有釋放,因為不會執行finally裡面的程式碼了。
那怎麼辦呢?有經驗的小夥伴應該就知道解決方案了
優化分散式鎖
方案還是比較簡單的,加個過期時間就行了
這樣即使斷電,過了10秒鐘之後鎖也會自動過期,也就是失效;別的請求就可以正常請求了
現在到了這裡,就是很多公司應用分散式鎖的常用方案了。小夥伴們這樣就沒有問題了嗎
問題分析
我們來看看問題出現在哪裡?我們來調整一下業務程式碼
因為我們扣庫存的業務,不可能像寫的很簡單的業務;正式場景中業務是比較多的,不可能就這麼簡單;如果業務程式碼執行的時間超出了鎖的過期時間,那麼鎖到期失效了,但業務程式碼還沒有執行完;這種場景就會導致資料錯亂。
那這個問題怎麼解決呢?
解決思路
這個問題的本質是鎖在沒有執行完成業務時,到期失效了;那我們可以不讓他失效不就行了嗎?那怎麼不讓他失效呢?
方案很簡單
啟動一個後臺執行緒,可以每3秒或者5秒執行一次,找到這個鎖的key,延長這個鎖key的過期時間;這樣就達到了鎖過期時間續期的功能了。是不是很簡單?
我們自己寫程式碼去實現是沒有問題的,但是現在市面上已經有了輪子了,不需要我們自己再去寫這個程式碼了,直接用人家的輪子;這個就是大名鼎鼎的Redisson。
分散式鎖Redisson
redisson是針對redis分佈鎖的強大的工具包,他提供了自動續期的功能,以及重入鎖的功能,只需要引入
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson</artifactId>
<version>3.13.2</version>
</dependency>
然後例項化
@Bean
publicRedisson redisson(){
Config config = new Config();
config.useSingleServer().setAddress("redis://xxx:6379").setPassword("*
").setDatabase(0);
return (Redisson) Redisson.create(config);
}
修改程式碼
用法非常簡單。我們接下來分析他的原理以及原始碼,看看他是怎麼續期的,和實現可重入的
原始碼分析
我們先來看看他的加鎖流程圖
加鎖機制
1、執行緒去獲取鎖,獲取成功: 執行lua指令碼,儲存資料到redis資料庫。
2、執行緒去獲取鎖,獲取失敗: 一直通過while迴圈嘗試獲取鎖,獲取成功後,執行lua指令碼,儲存資料到redis資料庫。即會阻塞執行緒
redisson是執行的lua指令碼的核心程式碼tryLockInnerAsync如下
redis執行lua指令碼是原子性的,要麼都成功,要麼都失敗
下面的程式碼就是獲取鎖失敗,就自旋;一直嘗試獲取鎖
續期機制
續期的業務就是上面流程圖中看門狗做的,這裡需要注意的是,如果要讓看門狗有效果,就不要設定過期時間,如果設定了過期時間看門狗就不起作用了;看原始碼
續期的程式碼就是this.scheduleExpirationRenewal(threadId);
注意判斷條件,是if (leaseTime == -1L)的時候才生效,才會啟動一個續期任務執行緒
主從問題
小夥伴看到這裡,是不是覺得這樣實現分佈鎖應該就沒有問題了吧,那是不是這樣呢?
我們來看一個問題
上圖中我們發現,如果突然出現redis的主節點突然掛掉了,而且在設定鎖key的時候,還沒有來得及同步到Slave從節點中,
主節點掛了,從節點會頂上成為主節點;但是這個時候新的主節點是沒有這個鎖key的。
那問題就來了,其他的請求再起請求的鎖,就會獲取鎖;這樣就造成了業務的混亂。
當然這個情況還是蠻特殊的,蠻少見的。那這個問題怎麼去解決呢?
解決方案
我們來分析一下上面的問題,主要就是因為主從同步資料有延遲導致的。
我們先來了解一下分散式系統中的CAP理論
C表示資料一致性,A表示高可用性,P表示分割槽容錯性
CAP 原則指的是,這三個要素最多隻能同時實現兩點,不可能三者兼顧。
那我們redis架構是AP原則,就是保證高可用性,犧牲了資料一致性;即主從資料有一定的延遲。
我們在來看看市面上還有另一種分散式鎖的方案,就是zookeeper;
zookeeper的架構原則是CP,就是保證了資料一致性,犧牲了高可用性,zookeeper的原理就是在寫入資料時候,要保證主節點寫入成功,而且還要保證大多數follow從節點也要寫入成功,都寫入成功,才會返回客戶端成功。這樣就保證了資料沒有延遲。
不過小夥伴們應該能夠發現,zookeeper的寫入效能是稍微低點的,因為要保證主從都要寫入成功才行。
總結
在整個主流方案中,如果一定要保證資料一致性,保證鎖不會有問題,可以選擇zk方案實現分散式鎖;但可以接受特殊情況下的鎖錯誤,那就用redis的redisson方案(推薦)。可以根據不通業務去選擇,即在同一個平臺中,可實現不同的方案。
看完三件事❤️
如果你覺得這篇內容對你還蠻有幫助,我想邀請你幫我三個小忙:
- 點贊,轉發,有你們的 『點贊和評論』,才是我創造的動力。
- 關注公眾號 『 阿風的架構筆記 』,不定期分享原創知識。
- 同時可以期待後續文章ing?
- 關注後回覆【666】掃碼即可獲取架構進階學習資料包