分布式事務最終一致性-CAP框架輕松搞定


前言

對於分布式事務,常用的解決方案根據一致性的程度可以進行如下划分:

  • 強一致性(2PC、3PC):數據庫層面的實現,通過鎖定資源,犧牲可用性,保證數據的強一致性,效率相對比較低。
  • 弱一致性(TCC):業務層面的實現,通過預留或鎖定部分資源,最后通過確認或取消操作完成事務的處理。比如A向B轉款500元,A賬號會凍結500元,其他操作正常,B接收轉款時,也不能直接入賬,而是將500元放到預留空間,只有經過確認之后,A才正式扣錢,B才正式入賬; 如果取消把A的500塊解凍,B也不會入賬。
  • 最終一致性(本地消息表):不管經過多少個服務節點,最終數據一致就行。比如下單成功之后,需要庫存服務扣減庫存,如果庫存扣減失敗,不管是重試,還是最后人工處理,最后確保訂單和庫存數據能對上就行;為保證用戶體驗,及時通過中間狀態的形式反饋給用戶,比如常見的出票中、數據處理中等。

對於強一致性和弱一致性的解決方案一般針對數據一致性和時效性要求特別高的業務場景,通常會犧牲暫時的可用性來滿足一致性的要求;由於為保證一致性,會鎖定資源,在高並發的業務場景不是最佳選擇,所以很多系統在業務需求允許的情況下,基本上都會采用最終一致性方案。

正文

1.1 最終一致性簡述

顧名思義就是保證數據最后的一致性就行了。如果中間節點發生失敗,系統為了減少代價,一般不會自動回滾,而是通過重試機制和人工參與的方式對失敗數據進行處理,從而保證系統高並發場景下高可用數據一致性需求。

1.2 解決方案

目前用得最多的方案是結合本地消息表進行實現,再加上后台任務、消息隊列中間件就可以更好的實現分布式事務的處理。

解決方案流程

本地消息表:就是在對應業務數據庫中增加的一張消息表;這張表存儲業務產生的消息,通過本地事務保證業務數據和消息數據的一致性。在消息表中通過一個狀態來標識業務是否執行成功,如果失敗,后台任務就進行重試。

1.2.2 CAP框架簡介

CAP 是一個EventBus(事件總線),同時也是一個在微服務或者SOA系統中解決分布式事務問題的一個框架,基於CAP理論思想進行封裝的。采用模塊化設計,具有高度的可擴展性,可靠並且易於更改。

對於分布式事務的處理,CAP 框架采用的是“異步確保”這種方案,即本地消息表。官方支持的數據存儲方式有SQL Server、MySQL、PostgreSql、MongoDB、In-Memory(內存),由於是開源項目,社區大佬也提供了其他數據存儲支持,如:Oracle、SQLite、SmartSql等。

在分布式系統,各節點需要進行消息傳輸,CAP框架提供以下幾種方式RabbitMQ、Kafka、Redis Streams(Redis 5.0支持)、Azure Service Bus、Amazon SQS、In-Memory Queue,使用方式都差不多。

CAP的架構圖如下:

架構圖

上圖簡要說明:

  • 有兩個微服務,服務A和服務B;
  • 服務A中通過本地事務的方式,將事件消息和業務邏輯進行事務保存(事件消息保存在本地消息表中),保證業務邏輯和消息的一致性和可靠性;關於消息的處理和保存CAP已經封裝在內部;
  • CAP內部定時調度任務將消息發布到消息隊列中;
  • 服務B訂閱到消息,將其保存到服務B的本地消息表中,CAP已經封裝好,只需按照說明使用即可;
  • 如果業務處理失敗,服務B中集成的CAP會根據配置的定時任務策略進行重試,直到處理成功為止;

主要的理論就說那么多,更多詳細內容,請進下方傳送門:

接下來就到擼碼時刻,CAP由於封裝比較好,所以使用起來比較簡單。

1.3 擼碼實踐

以下的業務場景是為了案例演示,目的是體現CAP的實踐,所以業務邏輯都只是模擬,切勿當真。

1.3.1 環境准備

演示中要用到RabbitMQ,為了安裝方便,這里使用Docker的方式,直接通過鏡像運行,簡單,快速方便。關於Docker的實踐,后續會專門出系列文章。這里就先總結一下Docker的安裝和RabbitMQ在Docker中的運行步驟,采用的主機環境是我之前買的阿里雲服務器(CentOS 7);演示用的數據庫是SqlServer。

  • Docker安裝

    1、移除移動舊版本

    sudo yum remove docker \
                      docker-client \
                      docker-client-latest \
                      docker-common \
                      docker-latest \
                      docker-latest-logrotate \
                      docker-logrotate \
                      docker-engine
    

    2、安裝需要的依賴包

    sudo yum install -y yum-utils
    

    3、設置鏡像倉庫

    sudo yum-config-manager \
        --add-repo \
        https://download.docker.com/linux/centos/docker-ce.repo
    

    4、更新Yum軟件包索引

    sudo yum makecache fast # 提高安裝速度
    

    5、開始安裝Docker

    sudo yum install docker-ce docker-ce-cli containerd.io
    

    6、啟動Docker

    sudo systemctl start docker
    

    7、測試Docker

    sudo docker run hello-world # 運行Hello-world
    

    安裝成功

  • RabbitMQ在Docker中安裝和運行

    1、一行命令直接指定鏡像運行,如果本地找不到鏡像,會去遠程倉儲里去找。

    docker run -d --hostname my-rabbit --name cap-rabbit -p 8888:15672 -p 5672:5672 -p 5671:5671 -p 1883:1883 rabbitmq:3-management
    

    這里先不細說命令了,后續聊Docker的時候好好說說。命令需要注意的是主機端口和容器端口的映射。

    2、運行成功后就可以訪問啦,默認用戶名和密碼:guest/guest;

    這里訪問的地址端口是8888,那是在啟動容器的時候將主機端口8888和容器端口15672進行了映射。

這就是選擇Dokcer安裝的原因,超級快;如果用傳統的方式,還得安裝語言環境,還得配置,最后才能安裝;Docker通過鏡像的方式直接運行即可。

如果小伙伴新增用戶之后不能訪問,或者程序連接報錯,可以排查是否有權限訪問,如下:

注:如果小伙伴用的是雲服務器,需要配置安全組,允許端口訪問;另外如果程序和RabbitMq所在的主機不是同一台機器,主機防火牆也需要放開對應的端口。

1.3.2 開始擼碼
  • 項目准備

    這里模擬兩個服務,一個是訂單服務,一個是庫存服務,兩都用到EF(Code First),如果小伙伴對EF入門還不熟,<<跟我一起學.NetCore之EF Core 實戰入門,一看就會>>這篇文章超詳細,肯定能幫到你; 所以接下來就上幾張關鍵的圖就行啦。

    項目結構:

    OrderDbContext:

    Startup中注冊服務:

    庫存服務的代碼和這個類似。

    通過遷移並更新到數據庫時,會生成如下數據庫和表:

  • 集成CAP

    這里因為用的是RabbitMQ、SqlServer,所以需要引入以下幾個包;如果用其他消息隊列或數據庫,可以引入對應的包。

    因為訂單服務是在Respository層使用CAP,所以對應的包就在這層引用;

    庫存服務是直接在Controller那層引用,這里就不重復截圖啦。

    訂單服務和庫存服務都是在各自項目的Startup文件中注冊CAP相關服務,並配置相關信息,如下圖:

    集成完畢之后,啟動項目(不需要手動自己遷移),在各自業務數據庫中就自動生成兩個消息表,用於后續消息的存儲,如下:

  • 編寫業務代碼

    訂單服務,在訂單生成成功之后,向庫存服務發送消息,業務邏輯如下:

    圖中用到的_capPublisher是通過構造函數注入的。訂單服務其他層的代碼就不用截圖了,就是簡單調用,源碼地址在文末。

    庫存服務直接訂閱就行,演示案例中是直接在StockController中進行訂閱,如下:

    // 標記為不實Action
    [NonAction]
    // 訂閱消息,參數和發布時指定的參數一致
    [CapSubscribe("Order.Create.Success")]
    public void UpdateStock(OrderEntity order)
    {
        //throw new Exception("扣減庫存異常了~~~");
        // 為了測試,庫存里面沒有數據的話,先模擬一條數據
        bool bHaveData = _stockDbContext.Stock.Any();
        if(!bHaveData)
        {
            StockEntity stock = new StockEntity
            {
                Id = Guid.NewGuid(),
                ProductNo = "Product001",
                StockCount = 100,
                UpdateDate = DateTime.Now
            };
            _stockDbContext.Stock.Add(stock);
            _stockDbContext.SaveChanges();
        }
        // 模擬扣減庫存
        using var trans = _stockDbContext.Database.BeginTransaction(_capPublisher, autoCommit: false);
        try
        {
            // 根據產品編號找到產品
            var product = _stockDbContext.Stock.Where(s => s.ProductNo == order.ProductNo).FirstOrDefault();
            // 扣減庫存之后保存
            product.StockCount = product.StockCount - order.Count;
            _stockDbContext.Update(product);
            _stockDbContext.SaveChanges();
            // 可以繼續向下發布流程,比如庫存扣減成功,下一步到物流服務進行相關處理,可以繼續發布消息
            // _capPublisher.Publish();
            trans.Commit();
            Console.WriteLine(order.OrderNo);
        }
        catch (Exception ex)
        {
            trans.Rollback();
        }
    }
    

    可以看到,訂閱很簡單,直接標上[CapSubscribe("Order.Create.Success")]這個Attribute就行了,如果消息狀態為失敗,后續CAP的定時任務會根據定時策略調用此方法。

1.3.3 運行看效果
  • 正常流程,下單成功,扣減庫存成功

    將訂單服務(端口5000)和庫存服務(端口6000)都啟動起來。

    訂單服務中增加了OrderController,里面有一個GenerateOrder的接口,直接調用即可:

    這里使用Postman工具進行測試,如下:

    庫存服務就會訂閱到信息,如下:

    業務流程完成之后,訂單和庫存數據整體一致了,回過頭來看看消息表,看看里面有什么消息,如下:

  • 異常流程模擬,下單成功,扣減庫存失敗

    在扣減服務邏輯方法中手動拋出異常,代碼如下:

    然后啟動項目重新測試,再下一個訂單試試; 操作后,先來看看消息表,如下:

    注:CAP在默認情況下,發送和消費消息的過程中失敗會立即重試 3 次,在 3 次以后將進入重試輪詢;重試將在發送和消費消息失敗的 4分鍾后 開始,這是為了避免設置消息狀態延遲導致可能出現的問題;后續就會每隔1分鍾之后重試一次,默認的最高重試次數為50次,當達到50次時,就不會重試了。

    現在知道問題了,優化代碼,重新啟動,即把拋異常的代碼注釋掉,看看會不會自動處理,如下:

    如上圖,稍等一會,消息就自動處理了,業務數據符合預期,保證一致性。 這個是CAP內部定時讀取消息表,根據狀態不斷重試業務邏輯,直到成功為止。 CAP的全自動是不是感覺比較便捷,寫最少的代碼,解決了最難搞的分布式事務。

  • 修改默認的配置

    在實際業務場景中,默認配置可能不太實用,可以在注冊服務時進行默認配置更改,如下:

    配置修改之后的測試這里就不截圖了,留給小伙伴們動手試試吧。

案例代碼地址:https://gitee.com/CodeZoe/microservies-demo/tree/main/CapDemo

總結

關於分布式事務的實操,把最常用的最終一致性方案簡單分享了一下,小伙伴可以根據自己的業務場景,趕緊動手試試吧;

其他方案會在后續的文章中加上,主要還是以實用為主,已經不咋用的就沒必要再說啦。

文章中提及到Docker和RabbitMQ,我已經在着手准備這塊的文章了,關注“Code綜藝圈”,和我一起學習吧;

圖片


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM