天天看點

[跨資料庫、微服務] FreeSql 分布式事務 TCC/Saga 編排重要性

💻 前言

FreeSql 支援 MySql/SqlServer/PostgreSQL/Oracle/Sqlite/Firebird/達夢/Gbase/神通/人大金倉/翰高/Clickhouse/MsAccess Ado.net 資料庫,以及 Odbc 的專門實作包。

FreeSql.Cloud 為 FreeSql 提供跨資料庫通路,分布式事務TCC、SAGA解決方案,支援 .NET Core 2.1+, .NET Framework 4.0+.

本文主要講解從跨資料庫通路,到分布式事務落地,再更新到微服務服務編排探讨。寫下本文更多的成份是帶有疑問号,希望有微服務落地經驗的朋友指教一下。

TCC 事務特點:

  • Try 用于資源當機/預扣;
  • Try 全部環節通過,代表業務一定能完成,進入 Confirm 環節;
  • Try 任何環節失敗,代表業務失敗,進入 Cancel 環節;
  • Confirm 失敗會進行重試N次,直到傳遞成功,或者人工幹預;
  • Cancel 失敗會進行重試N次,直到取消成功,或者人工幹預;

SAGA 事務特點:

  • Commit 用于業務送出;
  • Commit 全部環節通過,代表業務傳遞成功;
  • Commit 任何環節失敗,代表業務失敗,進入 Cancel 環節;
  • Cancel 失敗會進行重試N次,直到取消成功,或者人工幹預;

由于 TCC/Saga 兩種流程有相似之處,是以本文主要以 Saga 為例講解應用代碼。本文講解的落地場景如下:

第一步:去 資料庫db1 扣除 user.Point - 10

第二步:去 資料庫db2 扣除 goods.Stock - 1

第三步:去 資料庫db3 建立訂單

第二步庫存不足時,整個流程怎麼執行?

⚡ 快速開始

dotnet add package FreeSql.Cloud

or

Install-Package FreeSql.Cloud
public enum DbEnum { db1, db2, db3 }

var fsql = new FreeSqlCloud<DbEnum>("app001"); //提示:泛型可以傳入 string
fsql.DistributeTrace = log => Console.WriteLine(log.Split('\n')[0].Trim());

fsql.Register(DbEnum.db1, () => new FreeSqlBuilder()
    .UseConnectionString(DataType.SqlServer, @"Data Source=...")
    .Build());

fsql.Register(DbEnum.db2, () => new FreeSqlBuilder()
    .UseConnectionString(DataType.MySql, @"Data Source=...")
    .Build());

fsql.Register(DbEnum.db3, () => new FreeSqlBuilder()
    .UseConnectionString(DataType.Oracle, @"Data Source=...")
    .Build());

services.AddSingleton<IFreeSql>(fsql);
services.AddSingleton(fsql);
//注入兩個類型,穩
           
FreeSqlCloud 必須定義成單例模式

🚀 關于分布式事務

FreeSqlCloud 提供 TCC/SAGA 分布式事務排程、失敗重試、持久化重新開機後重新喚醒事務單元、等管理功能。

// 測試資料
fsql.Use(DbEnum.db1).Insert(new User { Id = 1, Name = "testuser01", Point = 10 }).ExecuteAffrows();
fsql.Use(DbEnum.db2).Insert(new Goods { Id = 1, Title = "testgoods01", Stock = 0 }).ExecuteAffrows();

var orderId = Guid.NewGuid();
await DB.Cloud.StartSaga(orderId.ToString(), "支付購買SAGA事務",
    new SagaOptions
    {
        MaxRetryCount = 10, //重試次數
        RetryInterval = TimeSpan.FromSeconds(10) //重試間隔
    })
    .Then<Saga1>(DbEnum.db1, new SagaBuyState { UserId = 1, Point = 10, GoodsId = 1, OrderId = orderId })
    .Then<Saga2>(DbEnum.db2, new SagaBuyState { UserId = 1, Point = 10, GoodsId = 1, OrderId = orderId })
    .Then<Saga3>(DbEnum.db3, new SagaBuyState { UserId = 1, Point = 10, GoodsId = 1, OrderId = orderId })
    .ExecuteAsync();
           

由于商品庫存不足,測試結果如下:

2022-08-17 05:24:00 【app001】db1 注冊成功, 并存儲 TCC/SAGA 事務相關資料
2022-08-17 05:24:00 【app001】成功加載曆史未完成 TCC 事務 0 個
2022-08-17 05:24:00 【app001】成功加載曆史未完成 SAGA 事務 0 個
2022-08-17 05:24:00 【app001】SAGA(85a95966-d5b0-4371-b54b-07d079d9fd78, 支付購買SAGA事務) Created successful, retry count: 10, interval: 10S
2022-08-17 05:24:00 【app001】SAGA(85a95966-d5b0-4371-b54b-07d079d9fd78, 支付購買SAGA事務) Unit1(第1步:資料庫db1 扣除使用者積分) COMMIT successful
2022-08-17 05:24:00 【app001】資料庫使用[Use] db2
2022-08-17 05:24:00 【app001】SAGA(85a95966-d5b0-4371-b54b-07d079d9fd78, 支付購買SAGA事務) Unit2(第2步:資料庫db2 扣除庫存) COMMIT failed, ready to CANCEL, -ERR 扣除庫存失敗
2022-08-17 05:24:00 【app001】SAGA(85a95966-d5b0-4371-b54b-07d079d9fd78, 支付購買SAGA事務) Unit1(第1步:資料庫db1 扣除使用者積分) CANCEL successful
2022-08-17 05:24:00 【app001】SAGA(85a95966-d5b0-4371-b54b-07d079d9fd78, 支付購買SAGA事務) Completed, all units CANCEL successfully
           
  • Commit 用于業務送出;
  • Commit 全部環節通過,代表業務傳遞成功;
  • Commit 任何環節失敗,代表業務失敗,進入 Cancel 環節;
  • Cancel 失敗會進行重試N次,直到取消成功,或者人工幹預;

Saga1、Saga2、Saga3 的實作代碼如下:

[Description("第1步:資料庫db1 扣除使用者積分")]
class Saga1 : SagaUnit<SagaBuyState>
{
    public override async Task Commit()
    {
        var affrows = await Orm.Update<User>().Set(a => a.Point - State.Point)
            .Where(a => a.Id == State.UserId && a.Point >= State.Point)
            .ExecuteAffrowsAsync();
        if (affrows <= 0) throw new Exception("扣除積分失敗");
        //記錄積分變動日志?
    }
    public override async Task Cancel()
    {
        await Orm.Update<User>().Set(a => a.Point + State.Point)
            .Where(a => a.Id == State.UserId)
            .ExecuteAffrowsAsync(); //退還積分
        //記錄積分變動日志?
    }
}

[Description("第2步:資料庫db2 扣除庫存")]
class Saga2 : SagaUnit<SagaBuyState>
{
    public override async Task Commit()
    {
        var affrows = await Orm.Update<Goods>().Set(a => a.Stock - 1)
            .Where(a => a.Id == State.GoodsId && a.Stock >= 1)
            .ExecuteAffrowsAsync();
        if (affrows <= 0) throw new Exception("扣除庫存失敗");
    }
    public override async Task Cancel()
    {
        await Orm.Update<Goods>().Set(a => a.Stock + 1)
            .Where(a => a.Id == State.GoodsId)
            .ExecuteAffrowsAsync(); //退還庫存
    }
}

[Description("第3步:資料庫db3 建立訂單")]
class Saga3 : SagaUnit<SagaBuyState>
{
    public override async Task Commit()
    {
        await Orm.Insert(new Order { Id = State.OrderId, Status = Order.OrderStatus.Success })
            .ExecuteAffrowsAsync();
    }
    public override Task Cancel()
    {
        return Task.CompletedTask;
    }
}
class BuySagaState
{
    public int UserId { get; set; }
    public int Point { get; set; }
    public Guid BuyLogId { get; set; }
    public int GoodsId { get; set; }
    public Guid OrderId { get; set; }
}
           

📯 關于微服務

最近幾天在整理 FreeSql.Cloud 代碼及相關示例,發現 TCC/Saga 事務單元内不是隻能 CRUD 操作,它還可以調用遠端 webapi 甚至 gRPC 服務。

事務單元内調用遠端 webapi,同樣可以擷取失敗重試、持久化等特點。請看以下代碼示例:

// HTTP 服務編排??
var orderId = Guid.NewGuid();
await DB.Cloud.StartSaga(orderId.ToString(), "支付購買webapi(saga)",
    new SagaOptions
    {
        MaxRetryCount = 10,
        RetryInterval = TimeSpan.FromSeconds(10)
    })
    .Then<HttpSaga>(default, new HttpUnitState
    {
        Url = "https://192.168.1.100/saga/UserPoint",
        Data = "UserId=1&Point=10&GoodsId=1&OrderId=" + orderId
    })
    .Then<HttpSaga>(default, new HttpUnitState
    {
        Url = "https://192.168.1.100/saga/GoodsStock",
        Data = "UserId=1&Point=10&GoodsId=1&OrderId=" + orderId
    })
    .Then<HttpSaga>(default, new HttpUnitState
    {
        Url = "https://192.168.1.100/saga/OrderNew",
        Data = "UserId=1&Point=10&GoodsId=1&OrderId=" + orderId
    })
    .ExecuteAsync();

class HttpSaga : SagaUnit<HttpUnitState>
{
    public override Task Commit()
    {
        //Console.WriteLine("請求 webapi:" + State.Url + "/Commit" + State.Data);
        return Task.CompletedTask;
    }
    public override Task Cancel()
    {
        //Console.WriteLine("請求 webapi:" + State.Url + "/Cancel" + State.Data);
        return Task.CompletedTask;
    }
}
class HttpUnitState
{
    public string Url { get; set; }
    public string Data { get; set; }
}
           
2022-08-17 06:11:05 【app001】db1 注冊成功, 并存儲 TCC/SAGA 事務相關資料
2022-08-17 06:11:05 【app001】成功加載曆史未完成 TCC 事務 0 個
2022-08-17 06:11:05 【app001】成功加載曆史未完成 SAGA 事務 0 個
2022-08-17 06:11:06 【app001】SAGA(2cad53d3-6d7e-481e-ad9a-15d4773a5397, 支付購買webapi(saga)) Created successful, retry count: 10, interval: 10S
2022-08-17 06:11:06 【app001】SAGA(2cad53d3-6d7e-481e-ad9a-15d4773a5397, 支付購買webapi(saga)) Unit1 COMMIT successful
2022-08-17 06:11:06 【app001】SAGA(2cad53d3-6d7e-481e-ad9a-15d4773a5397, 支付購買webapi(saga)) Unit2 COMMIT successful
2022-08-17 06:11:06 【app001】SAGA(2cad53d3-6d7e-481e-ad9a-15d4773a5397, 支付購買webapi(saga)) Unit3 COMMIT successful
2022-08-17 06:11:06 【app001】SAGA(2cad53d3-6d7e-481e-ad9a-15d4773a5397, 支付購買webapi(saga)) Completed, all units COMMIT successfully
           

這段代碼是突然想出來的,由于沒正式落地過微服務項目,故攜帶代碼及類似的場景在 Natasha 技術大牛群裡提出來讨論。

讨論原文:

微服務這些業務編排的,比如支付購買業務,用微服務怎麼做。

第一步:去 server1 扣除 user.Point - 10

第二步:去 server2 扣除 goods.Stock - 1

第三步:去 server3 建立訂單

第二步扣庫存失敗,怎麼辦?

很多人會回複消息隊列,業務複雜了,不編排很難維護消息隊列的。編排後的代碼,讓維護者更加直覺。

感謝

dongfo

提供的參考方案:https://dtm.pub/app/order.html

DTM 解決方案也是使用的 saga 業務流程,看來 FreeSql.Cloud 沒有走偏,做跨資料庫事務可行,用來做 webapi 編排也不錯。

我仍然好奇,很多 .net 微服務文章介紹

服務編排

的少之又少,希望有微服務落地經驗的朋友多多指教。

問:是不是缺少了條件鍊路呢?A條件走A,B條件走B。

答:這種應該整個判斷,在分支做條件會複雜很多,直覺性會變差。

if (場景A)
   StartSaga(...) 流程1
if (場景B)
   StartSaga(...) 流程2
           

⛳ 結束語

FreeSql 支援很多資料庫,功能強大、穩定性好,有好的想法可以一起讨論。

希望這篇文章能幫助大家輕松了解并熟練掌握 TCC/Saga 事務,為企業的項目研發貢獻力量。

開源位址:https://github.com/dotnetcore/FreeSql

作者是什麼人?

作者是一個入行 18年的老批,他目前寫的.net 開源項目有:

開源項目 描述 開源位址 開源協定
ImCore 架構最簡單,擴充性最強的聊天系統架構 https://github.com/2881099/im 最寬松的 MIT 協定,可商用
FreeRedis 最簡單的 RediscClient https://github.com/2881099/FreeRedis 最寬松的 MIT 協定,可商用
csredis https://github.com/2881099/csredis 最寬松的 MIT 協定,可商用
FightLandlord 鬥地主單機或網絡版 https://github.com/2881099/FightLandlord 最寬松的 MIT 協定,學習用途
FreeScheduler 定時任務 https://github.com/2881099/FreeScheduler 最寬松的 MIT 協定,可商用
IdleBus 空閑容器 https://github.com/2881099/IdleBus 最寬松的 MIT 協定,可商用
FreeSql 國産最好用的 ORM https://github.com/dotnetcore/FreeSql 最寬松的 MIT 協定,可商用
FreeSql.Cloud 分布式事務tcc/saga https://github.com/2881099/FreeSql.Cloud 最寬松的 MIT 協定,可商用
FreeSql.AdminLTE 低代碼背景管理項目生成 https://github.com/2881099/FreeSql.AdminLTE 最寬松的 MIT 協定,可商用
FreeSql.DynamicProxy 動态代理 https://github.com/2881099/FreeSql.DynamicProxy 最寬松的 MIT 協定,學習用途

需要的請拿走,這些都是最近幾年的開源作品,以前更早寫的就不發了。

QQ群:4336577(已滿)、8578575(線上)、52508226(線上)

繼續閱讀