一、概述
二階段訊息是DTM
新提出的,可以完美代替現有的事務訊息和本地訊息表架構。無論從複雜度、效能、便利性還是程式碼量都是完勝現有的方案。
相比現有的訊息架構藉助於各種訊息中介軟體比如RocketMQ
等,DTM
自己實現了無需額外的學習成本。它能夠保證本地事務的提交和全域性事務提交是“原子的”,適合解決不需要回滾的分散式事務場景。
二階段訊息保證提交的原子性和如何保證業務成功執行如下時序圖:
二階段訊息主要是指Prepare
和Submit
兩個階段,主程式向DTM
服務傳送Prepare
訊息,成功後執行本地事務,完成本地事務後傳送Submit
訊息至DTM
服務,之後DTM
會呼叫分支事件執行其他服務,最後完成全域性事務。
當傳送了Prepare
但是Submit
沒有提交的話,會進行回撥請求來確認訊息的情況,具體工作過程如下:
1、在處理本地事務時,會將gid
插入到barrier
表中,同時帶上插入原因為committed
。該表有一個唯一索引,主要欄位為gid
。
2、當進行回查時,二階段訊息的操作不是直接查gid
是否存在,而是再insert ignore
一條帶有相同gid
的資料,同時帶上插入原因為rollbacked
。此時如果表中如果已有gid
的記錄,那麼新的插入操作就會被ignore
,否則資料會被插入。
3、然後再用gid
查詢表中的記錄,如果查到記錄的reason
為committed
,那麼說明本地事務已提交;如果查到記錄的reason
為rollbacked
,那麼說明本地事務已回滾。
二、安裝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
,這個其實就是為了等價mysql
的insert 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" }
DtmUrl
:DTM
的監聽地址,http
的是36789
,grpc
的是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
:表示使用的資料庫型別
BarrierSqlTableName
:Barrier
表的名字
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儲存引擎的該功能還在開發中)修改為當前時間,就會在數秒內被定時輪詢,事務就會繼續往前執行。