• [跨数据库、微服务] 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("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(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(DbEnum.db1, new SagaBuyState { UserId = 1, Point = 10, GoodsId = 1, OrderId = orderId })
        .Then(DbEnum.db2, new SagaBuyState { UserId = 1, Point = 10, GoodsId = 1, OrderId = orderId })
        .Then(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
    {
        public override async Task Commit()
        {
            var affrows = await Orm.Update().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().Set(a => a.Point + State.Point)
                .Where(a => a.Id == State.UserId)
                .ExecuteAffrowsAsync(); //退还积分
            //记录积分变动日志?
        }
    }
    
    [Description("第2步:数据库db2 扣除库存")]
    class Saga2 : SagaUnit
    {
        public override async Task Commit()
        {
            var affrows = await Orm.Update().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().Set(a => a.Stock + 1)
                .Where(a => a.Id == State.GoodsId)
                .ExecuteAffrowsAsync(); //退还库存
        }
    }
    
    [Description("第3步:数据库db3 创建订单")]
    class Saga3 : SagaUnit
    {
        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(default, new HttpUnitState
        {
            Url = "https://192.168.1.100/saga/UserPoint",
            Data = "UserId=1&Point=10&GoodsId=1&OrderId=" + orderId
        })
        .Then(default, new HttpUnitState
        {
            Url = "https://192.168.1.100/saga/GoodsStock",
            Data = "UserId=1&Point=10&GoodsId=1&OrderId=" + orderId
        })
        .Then(default, new HttpUnitState
        {
            Url = "https://192.168.1.100/saga/OrderNew",
            Data = "UserId=1&Point=10&GoodsId=1&OrderId=" + orderId
        })
        .ExecuteAsync();
    
    class HttpSaga : SagaUnit
    {
        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(在线)

  • 相关阅读:
    JDK1.5 新特性【泛型】
    字节码学习之常见java语句的底层原理
    Promise
    Redis 知识总结
    MyBatis面试题(四)
    2.6 Windows驱动开发:使用IO与DPC定时器
    【MAPBOX基础功能】14、mapbox点击面图层高亮指定的面
    靶场上新:Openfire身份认证绕过
    堆--堆排序
    深入理解Java注解的实现原理以及前世今生
  • 原文地址:https://www.cnblogs.com/FreeSql/p/16594837.html