企業實戰之分散式鎖方案一步步的演變歷程!

阿風的架構筆記發表於2021-04-21

前言

在我們學習多執行緒開發的時候,線上程同時針對同一個資源進行操作的時候都需要加鎖;一般會用到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方案(推薦)。可以根據不通業務去選擇,即在同一個平臺中,可實現不同的方案。

看完三件事❤️


如果你覺得這篇內容對你還蠻有幫助,我想邀請你幫我三個小忙:

  1. 點贊,轉發,有你們的 『點贊和評論』,才是我創造的動力。
  2. 關注公眾號 『 阿風的架構筆記 』,不定期分享原創知識。
  3. 同時可以期待後續文章ing?
  4. 關注後回覆【666】掃碼即可獲取架構進階學習資料包

相關文章