.NetCore中使用分散式事務DTM的二階段訊息

以往清泉發表於2023-04-01

一、概述

二階段訊息是DTM新提出的,可以完美代替現有的事務訊息和本地訊息表架構。無論從複雜度、效能、便利性還是程式碼量都是完勝現有的方案。

相比現有的訊息架構藉助於各種訊息中介軟體比如RocketMQ等,DTM自己實現了無需額外的學習成本。它能夠保證本地事務的提交和全域性事務提交是“原子的”,適合解決不需要回滾的分散式事務場景

二階段訊息保證提交的原子性和如何保證業務成功執行如下時序圖:

 

 二階段訊息主要是指PrepareSubmit兩個階段,主程式向DTM服務傳送Prepare訊息,成功後執行本地事務,完成本地事務後傳送Submit訊息至DTM服務,之後DTM會呼叫分支事件執行其他服務,最後完成全域性事務。

 當傳送了Prepare但是Submit沒有提交的話,會進行回撥請求來確認訊息的情況,具體工作過程如下:

 1、在處理本地事務時,會將gid插入到barrier表中,同時帶上插入原因為committed。該表有一個唯一索引,主要欄位為gid

 2、當進行回查時,二階段訊息的操作不是直接查gid是否存在,而是再insert ignore一條帶有相同gid的資料,同時帶上插入原因為rollbacked。此時如果表中如果已有gid的記錄,那麼新的插入操作就會被ignore,否則資料會被插入。

 3、然後再用gid查詢表中的記錄,如果查到記錄的reasoncommitted,那麼說明本地事務已提交;如果查到記錄的reasonrollbacked,那麼說明本地事務已回滾。

二、安裝DTM

 我使用二進位制包下載安裝地址,我是Window環境所以下載後解壓,點選dtm.exe進行執行即可,如下啟動成功

啟動成功後可以訪問http://localhost:36789,進入管理後臺

三、建立DTM所需的表

我們需要建立一個表處理訊息的回查,表裡儲存全域性事務ID,具體作用在後續說明,我這裡用的SqlServer資料庫,所以執行如下:

CREATE TABLE [dbo].[barrier]
(
    [id] bigint NOT NULL IDENTITY(1,1) PRIMARY KEY,
    [trans_type] varchar(45) NOT NULL DEFAULT(''),
    [gid] varchar(128) NOT NULL DEFAULT(''),
    [branch_id] varchar(128) NOT NULL DEFAULT(''),
    [op] varchar(45) NOT NULL DEFAULT(''),
    [barrier_id] varchar(45) NOT NULL DEFAULT(''),
    [reason] varchar(45) NOT NULL DEFAULT(''),
    [create_time] datetime NOT NULL DEFAULT(getdate()) ,
    [update_time] datetime NOT NULL DEFAULT(getdate())
)

GO

CREATE UNIQUE INDEX[ix_uniq_barrier] ON[dbo].[barrier]
        ([gid] ASC, [branch_id] ASC, [op] ASC, [barrier_id] ASC)
WITH(IGNORE_DUP_KEY = ON)

GO

這裡比較關鍵的是那個唯一索引,有一個IGNORE_DUP_KEY = ON,這個其實就是為了等價mysqlinsert ignore表示存在相關欄位的資訊則不插入,否則就插入資料

當然還支援很多其他的資料庫,建表語句可以從這裡檢視地址

 四、建立專案

 我們簡單的建立兩個.net core webapi專案進行測試,兩個專案都進行相同的如下操作:

 1、安裝Dtmcli和Microsoft.EntityFrameworkCore.SqlServer

 安裝Dtmcli是因為其中已經幫我們整合了DTM客戶端SDK HTTP版本,想要GRPC版本可以安裝Dtmgrpc

 安裝Microsoft.EntityFrameworkCore.SqlServer很顯然是為了處理資料庫。

Install-Package Dtmcli
Install-Package Microsoft.EntityFrameworkCore.SqlServer

2、配置

接下來我們配置服務,先在配置檔案appsetting.json中新增如下

  "AppSettings": {
    "DtmUrl": "http://localhost:36789",
    "BusiUrl": "http://localhost:5056",
    "QueryPreparedUrl": "http://localhost:5046",
    "BarrierConn": "Data Source=.;Initial Catalog=HTGL;TrustServerCertificate=True;;Integrated Security=True"
  }

 DtmUrlDTM的監聽地址,http的是36789grpc的是36790

 BusiUrl:訪問其他服務的地址

 QueryPreparedUrl:回查的地址

 BarrierConn:資料庫連線語句

 新增一個配置類:

    public class AppSettings
    {
        public string DtmUrl { get; set; }
        public string BusiUrl { get; set; }
        public string BarrierConn { get; set; }
        public string QueryPreparedUrl { get; set; }
    }

 之後注入服務如下:

builder.Services.AddDtmcli(dtm => { 
    dtm.DtmUrl = builder.Configuration.GetValue<string>("AppSettings:DtmUrl");
    dtm.SqlDbType = DtmCommon.Constant.Barrier.DBTYPE_SQLSERVER;
    dtm.BarrierSqlTableName = "[HTGL].[dbo].[barrier]";
});
builder.Services.Configure<AppSettings>(builder.Configuration.GetSection("AppSettings"));

SqlDbType:表示使用的資料庫型別

BarrierSqlTableNameBarrier表的名字

3、新增程式碼

我們在其中一個專案新增主程式程式碼如下:

    [ApiController]public class DtmController : ControllerBase
    {

        private readonly ILogger<DtmController> _logger;
        private readonly IDtmClient _dtmClient;
        private readonly IDtmTransFactory _transFactory;
        private readonly AppSettings _settings;
        private readonly IBranchBarrierFactory _factory;
        public DtmController(ILogger<DtmController> logger, IDtmClient dtmClient,IDtmTransFactory transFactory, IOptions<AppSettings> settings, IBranchBarrierFactory factory)
        {
            _logger = logger;
            _dtmClient = dtmClient;
            _transFactory = transFactory;
            _settings = settings.Value;
            _factory = factory;
        }
        private DbConnection GetConn() => new Microsoft.Data.SqlClient.SqlConnection(_settings.BarrierConn);
        [HttpPost("post-dtm-msg")]
        public async Task<IActionResult> Get(CancellationToken cancellationToken)
        {
            //1、建立gid
            var gid = await _dtmClient.GenGid(cancellationToken);
            //2、設定分支事務
            var msg = _transFactory.NewMsg(gid)
                .Add(_settings.BusiUrl + "/TransOut", new { id = 123 })
                .Add(_settings.BusiUrl + "/TransIn", new { id = 321 });//3、執行submit
            using (DbConnection conn = GetConn())
            {
                await msg.DoAndSubmitDB(_settings.QueryPreparedUrl + "/msg-queryprepared", conn, async tx =>
                {
                    //4、執行本地事務
                    await Task.CompletedTask;
                });
            }
            _logger.LogInformation("result gid is {0}", gid);
            return Content("SUCCESS");
        }
        [HttpGet("msg-queryprepared")]
        public async Task<IActionResult> QueryPrepared(CancellationToken cancellationToken)
        {
            var bb = _factory.CreateBranchBarrier(Request.Query);
            _logger.LogInformation("bb {0}", bb);
            using (DbConnection conn = GetConn())
            {
                //回撥查詢訊息狀態
                var res = await bb.QueryPrepared(conn);
                return Ok(new { dtm_result = res });
            }
        }
    }

然後我們向另一個服務專案新增如下程式碼,作為一個簡單的服務方法,沒有任何操作只是返回成功:

[ApiController]
    public class TransController : ControllerBase
    {
        private readonly ILogger<TransController> _logger;
        private readonly IBranchBarrierFactory _factory;
        private readonly AppSettings _settings;
        private DbConnection GetConn() => new Microsoft.Data.SqlClient.SqlConnection(_settings.BarrierConn);
        public TransController(ILogger<TransController> logger, IBranchBarrierFactory factory, IOptions<AppSettings> settings)
        {
            _logger = logger;
            _factory = factory;
            _settings = settings.Value;
        }
        [HttpPost("TransIn")]
        public async Task<IResult> In()
        {
            return Results.Ok(new { dtm_result = "SUCCESS" });
            //return Results.Ok(new { dtm_result = "FAILURE" });
        }
        [HttpPost("TransOut")]
        public async Task<IResult> Out()
        {
            return Results.Ok(new { dtm_result = "SUCCESS" });
        }
    }

五、執行檢視結果

我們正常執行,可以看到下面的動圖結果,在執行完本地事務後會訪問分支事務,然後資料庫表中新增了一條記錄

可以在管理後臺看到我們請求成功的資訊

 如果要演示失敗,需要做以下修改直接報錯,我們可以看到訪問了回撥方法,然後資料庫中看到rollback標記的訊息

using (DbConnection conn = GetConn())
            {
                await msg.DoAndSubmitDB(_settings.QueryPreparedUrl + "/msg-queryprepared", conn, async tx =>
                {
                    throw new Exception("報錯了");
                    //4、執行本地事務
                    await Task.CompletedTask;
                });
            }

 提交後再當機演示比較麻煩,我就不演示了,大家意會即可。

 如果分支事務返回的不是SUCCESS而是FAILURE會由DTM隔一段時間重新請求,dtm對每個事務的重試是指數退避策略,具體為間隔是每失敗一次,間隔加倍,避免過多的重試,導致系統負載異常上升。

 如果您經過長時間的的當機,因指數退避演算法導致要很久才會重試。如果您想要手動觸發立即重試,您可以手動把相應事務的next_cron_time(Redis儲存引擎的該功能還在開發中)修改為當前時間,就會在數秒內被定時輪詢,事務就會繼續往前執行。

 

相關文章