【分佈式鎖的演化】分佈式鎖竟然還能用MySQL?

前言

以前的文章中經過電商場景中秒殺的例子和你們分享了單體架構中鎖的使用方式,可是如今不少應用系統都是至關龐大的,不少應用系統都是微服務的架構體系,那麼在這種跨jvm的場景下,咱們又該如何去解決併發。java

單體應用鎖的侷限性

在進入實戰以前簡單和你們粗略聊一下互聯網系統中的架構演進。mysql

架構簡單演化

在互聯網系統發展之初,消耗資源比較小,用戶量也比較小,咱們只部署一個tomcat應用就能夠知足需求。一個tomcat咱們能夠看作是一個jvm的進程,當大量的請求併發到達系統時,全部的請求都落在這惟一的一個tomcat上,若是某些請求方法是須要加鎖的,好比上篇文章中說起的秒殺扣減庫存的場景,是能夠知足需求的。可是隨着訪問量的增長,一個tomcat難以支撐,這時候咱們就須要集羣部署tomcat,使用多個tomcat支撐起系統。nginx

在上圖中簡單演化以後,咱們部署兩個Tomcat共同支撐系統。當一個請求到達系統的時候,首先會通過nginx,由nginx做爲負載均衡,它會根據本身的負載均衡配置策略將請求轉發到其中的一個tomcat上。當大量的請求併發訪問的時候,兩個tomcat共同承擔全部的訪問量。這以後咱們一樣進行秒殺扣減庫存的時候,使用單體應用鎖,還能知足需求麼?git

以前咱們所加的鎖是JDK提供的鎖,這種鎖在單個jvm下起做用,當存在兩個或者多個的時候,大量併發請求分散到不一樣tomcat,在每一個tomcat中均可以防止併發的產生,可是多個tomcat之間,每一個Tomcat中得到鎖這個請求,又產生了併發。從而扣減庫存的問題依舊存在。這就是單體應用鎖的侷限性。那咱們若是解決這個問題呢?接下來就要和你們分享分佈式鎖了。程序員

分佈式鎖

什麼是分佈式鎖?

那麼什麼是分佈式鎖呢,在說分佈式鎖以前咱們看到單體應用鎖的特色就是在一個jvm進行有效,可是沒法跨越jvm以及進程。因此咱們就能夠下一個不那麼官方的定義,分佈式鎖就是能夠跨越多個jvm,跨越多個進程的鎖,像這樣的鎖就是分佈式鎖。github

設計思路

分佈式鎖思路

因爲tomcat是java啓動的,因此每一個tomcat能夠當作一個jvm,jvm內部的鎖沒法跨越多個進程。因此咱們實現分佈式鎖,只能在這些jvm外去尋找,經過其餘的組件來實現分佈式鎖。redis

上圖兩個tomcat經過第三方的組件實現跨jvm,跨進程的分佈式鎖。這就是分佈式鎖的解決思路。sql

實現方式

那麼目前有哪些第三方組件來實現呢?目前比較流行的有如下幾種:數據庫

  • 數據庫,經過數據庫能夠實現分佈式鎖,可是高併發的狀況下對數據庫的壓力比較大,因此不多使用。
  • Redis,藉助redis能夠實現分佈式鎖,並且redis的java客戶端種類不少,因此使用方法也不盡相同。
  • Zookeeper,也能夠實現分佈式鎖,一樣zk也有不少java客戶端,使用方法也不一樣。

針對上述實現方式,老貓仍是經過具體的代碼例子來一一演示。tomcat

基於數據庫的分佈式鎖

思路:基於數據庫悲觀鎖去實現分佈式鎖,用的主要是select ... for update。select ... for update是爲了在查詢的時候就對查詢到的數據進行了加鎖處理。當用戶進行這種行爲操做的時候,其餘線程是禁止對這些數據進行修改或者刪除操做,必須等待上個線程操做完畢釋放以後才能進行操做,從而達到了鎖的效果。

實現:咱們仍是基於電商中超賣的例子和你們分享代碼。

我們仍是利用上次單體架構中的超賣的例子和你們分享,針對上次的代碼進行改造,咱們新鍵一張表,叫作distribute_lock,這張表的目的主要是爲了提供數據庫鎖,咱們來看一下這張表的狀況。
初始化訂單數據
因爲咱們這邊模擬的是訂單超賣的場景,因此在上圖中咱們有一條訂單的鎖數據。

咱們將上一篇中的代碼改造一下抽取出一個controller而後經過postman去請求調用,固然後臺是啓動兩個jvm進行操做,分別是8080端口以及8081端口。完成以後的代碼以下:

/**
 * @author kdaddy@163.com
 * @date 2021/1/3 10:48
 * @desc 公衆號「程序員老貓」
 */
@Service
@Slf4j
public class MySQLOrderService {
    @Resource
    private KdOrderMapper orderMapper;
    @Resource
    private KdOrderItemMapper orderItemMapper;
    @Resource
    private KdProductMapper productMapper;
    @Resource
    private DistributeLockMapper distributeLockMapper;
    //購買商品id
    private int purchaseProductId = 100100;
    //購買商品數量
    private int purchaseProductNum = 1;
    
    @Transactional(propagation = Propagation.REQUIRED)
    public  Integer createOrder() throws Exception{
        log.info("進入了方法");
        DistributeLock lock = distributeLockMapper.selectDistributeLock("order");
        if(lock == null) throw new Exception("該業務分佈式鎖未配置");
        log.info("拿到了鎖");
        //此處爲了手動演示併發,因此咱們暫時在這裏休眠1分鐘
        Thread.sleep(60000);

        KdProduct product = productMapper.selectByPrimaryKey(purchaseProductId);
        if (product==null){
            throw new Exception("購買商品:"+purchaseProductId+"不存在");
        }
        //商品當前庫存
        Integer currentCount = product.getCount();
        log.info(Thread.currentThread().getName()+"庫存數"+currentCount);
        //校驗庫存
        if (purchaseProductNum > currentCount){
            throw new Exception("商品"+purchaseProductId+"僅剩"+currentCount+"件,沒法購買");
        }

        //在數據庫中完成減量操做
        productMapper.updateProductCount(purchaseProductNum,"kd",new Date(),product.getId());
        //生成訂單
        ...次數省略,源代碼能夠到老貓的github下載:https://github.com/maoba/kd-distribute
        return order.getId();
    }
}

SQL的寫法以下:

select
   *
    from distribute_lock
    where business_code = #{business_code,jdbcType=VARCHAR}
    for update

以上爲主要實現邏輯,關於代碼中的注意點:

  • createOrder方法必需要有事務,由於只有在事務存在的狀況下才能觸發select for update的鎖。
  • 代碼中必需要對當前鎖的存在性進行判斷,若是爲空的狀況下,會報異常

咱們來看一下最終運行的效果,先看一下console日誌,

8080的console日誌狀況:

11:49:41  INFO 16360 --- [nio-8080-exec-2] c.k.d.service.MySQLOrderService          : 進入了方法
11:49:41  INFO 16360 --- [nio-8080-exec-2] c.k.d.service.MySQLOrderService          : 拿到了鎖

8081的console日誌狀況:

11:49:48  INFO 17640 --- [nio-8081-exec-2] c.k.d.service.MySQLOrderService          : 進入了方法

經過日誌狀況,兩個不一樣的jvm,因爲第一個到8080的請求優先拿到了鎖,因此8081的請求就處於等待鎖釋放纔會去執行,這說明咱們的分佈式鎖生效了。

再看一下完整執行以後的日誌狀況:

8080的請求:

11:58:01  INFO 15380 --- [nio-8080-exec-1] c.k.d.service.MySQLOrderService          : 進入了方法
11:58:01  INFO 15380 --- [nio-8080-exec-1] c.k.d.service.MySQLOrderService          : 拿到了鎖
11:58:07  INFO 15380 --- [nio-8080-exec-1] c.k.d.service.MySQLOrderService          : http-nio-8080-exec-1庫存數1

8081的請求:

11:58:03  INFO 16276 --- [nio-8081-exec-1] c.k.d.service.MySQLOrderService          : 進入了方法
11:58:08  INFO 16276 --- [nio-8081-exec-1] c.k.d.service.MySQLOrderService          : 拿到了鎖
11:58:14  INFO 16276 --- [nio-8081-exec-1] c.k.d.service.MySQLOrderService          : http-nio-8081-exec-1庫存數0
11:58:14 ERROR 16276 --- [nio-8081-exec-1] o.a.c.c.C.[.[.[/].[dispatcherServlet]    : Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Request processing failed; nested exception is java.lang.Exception: 商品100100僅剩0件,沒法購買] with root cause

java.lang.Exception: 商品100100僅剩0件,沒法購買
	at com.kd.distribute.service.MySQLOrderService.createOrder(MySQLOrderService.java:61) ~[classes/:na]

很明顯第二個請求因爲沒有庫存,致使最終購買失敗的狀況,固然這個場景也是符合咱們正常的業務場景的。最終咱們數據庫的狀況是這樣的:
訂單記錄

產品庫存記錄

很明顯,咱們到此數據庫的庫存和訂單數量也都正確了。到此咱們基於數據庫的分佈式鎖實戰演示完成,下面咱們來概括一下若是使用這種鎖,有哪些優勢以及缺點。

  • 優勢:簡單方便、易於理解、易於操做。
  • 缺點:併發量大的時候對數據庫的壓力會比較大。
  • 建議:做爲鎖的數據庫和業務數據庫分開。

寫在最後

對於上述數據庫分佈式鎖,其實在咱們的平常開發中用的也是比較少的。基於redis以及zk的鎖卻是用的比較多一些,原本老貓想把redis鎖以及zk鎖放在這一篇中一塊兒分享掉,可是再寫在同一篇上面的話,篇幅就顯得過長了,所以本篇就和你們分享這一種分佈式鎖。源碼你們能夠在老貓的github中下載到。地址是:https://github.com/maoba/kd-distribute,後面老貓會把redis鎖以及zk鎖都分享給你們,敬請期待,固然更多的乾貨分享,也歡迎你們關注公衆號「程序員老貓」。

相關文章
相關標籤/搜索