「跨数据库、微服务」 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+.

前言

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 事务,为企业的项目研发贡献力量。


相关文章
|
10月前
|
监控 Java API
Spring Boot 3.2 结合 Spring Cloud 微服务架构实操指南 现代分布式应用系统构建实战教程
Spring Boot 3.2 + Spring Cloud 2023.0 微服务架构实践摘要 本文基于Spring Boot 3.2.5和Spring Cloud 2023.0.1最新稳定版本,演示现代微服务架构的构建过程。主要内容包括: 技术栈选择:采用Spring Cloud Netflix Eureka 4.1.0作为服务注册中心,Resilience4j 2.1.0替代Hystrix实现熔断机制,配合OpenFeign和Gateway等组件。 核心实操步骤: 搭建Eureka注册中心服务 构建商品
1411 3
|
9月前
|
存储 安全 Java
管理 Spring 微服务中的分布式会话
在微服务架构中,管理分布式会话是确保用户体验一致性和系统可扩展性的关键挑战。本文探讨了在 Spring 框架下实现分布式会话管理的多种方法,包括集中式会话存储和客户端会话存储(如 Cookie),并分析了它们的优缺点。同时,文章还涵盖了与分布式会话相关的安全考虑,如数据加密、令牌验证、安全 Cookie 政策以及服务间身份验证。此外,文中强调了分布式会话在提升系统可扩展性、增强可用性、实现数据一致性及优化资源利用方面的显著优势。通过合理选择会话管理策略,结合 Spring 提供的强大工具,开发人员可以在保证系统鲁棒性的同时,提供无缝的用户体验。
193 0
|
11月前
|
存储 关系型数据库 分布式数据库
喜报|阿里云PolarDB数据库(分布式版)荣获国内首台(套)产品奖项
阿里云PolarDB数据库管理软件(分布式版)荣获「2024年度国内首版次软件」称号,并跻身《2024年度浙江省首台(套)推广应用典型案例》。
|
人工智能 安全 Java
智慧工地源码,Java语言开发,微服务架构,支持分布式和集群部署,多端覆盖
智慧工地是“互联网+建筑工地”的创新模式,基于物联网、移动互联网、BIM、大数据、人工智能等技术,实现对施工现场人员、设备、材料、安全等环节的智能化管理。其解决方案涵盖数据大屏、移动APP和PC管理端,采用高性能Java微服务架构,支持分布式与集群部署,结合Redis、消息队列等技术确保系统稳定高效。通过大数据驱动决策、物联网实时监测预警及AI智能视频监控,消除数据孤岛,提升项目可控性与安全性。智慧工地提供专家级远程管理服务,助力施工质量和安全管理升级,同时依托可扩展平台、多端应用和丰富设备接口,满足多样化需求,推动建筑行业数字化转型。
437 5
|
Cloud Native 关系型数据库 分布式数据库
登顶TPC-C|云原生数据库PolarDB技术揭秘:Limitless集群和分布式扩展篇
阿里云PolarDB云原生数据库在TPC-C基准测试中以20.55亿tpmC的成绩刷新世界纪录,展现卓越性能与性价比。其轻量版满足国产化需求,兼具高性能与低成本,适用于多种场景,推动数据库技术革新与发展。
|
11月前
|
存储 NoSQL MongoDB
MongoDB数据库详解-针对大型分布式项目采用的原因以及基础原理和发展-卓伊凡|贝贝|莉莉
MongoDB数据库详解-针对大型分布式项目采用的原因以及基础原理和发展-卓伊凡|贝贝|莉莉
412 8
MongoDB数据库详解-针对大型分布式项目采用的原因以及基础原理和发展-卓伊凡|贝贝|莉莉
|
12月前
|
中间件 关系型数据库 Go
Go语言数据库编程:数据迁移与事务控制
本文介绍了《Go语言实战指南》中关于数据库编程的核心内容,涵盖使用 GORM 进行数据迁移与事务控制。主要内容包括:AutoMigrate 方法自动创建或更新表结构;事务控制的自动与手动实现方式;事务隔离级别的设置;以及在 Gin 框架中统一管理事务的实践建议。适合开发阶段的数据库结构管理和事务性操作需求。
|
Cloud Native 关系型数据库 分布式数据库
登顶TPC-C|云原生数据库PolarDB技术揭秘:Limitless集群和分布式扩展篇
云原生数据库PolarDB技术揭秘:Limitless集群和分布式扩展篇
|
人工智能 数据可视化 JavaScript
颠覆开发效率!国内首个微服务编排框架Juggle开源啦!
Juggle是国内首个开源的微服务编排框架,专注于解决企业微服务进程中接口重复开发、系统对接复杂等问题。它提供零代码、低代码和AI增强功能,通过可视化拖拽快速组装简单API为复杂接口,支持多协议、多语言脚本和流程多版本管理。相比国外框架如Conductor,Juggle更贴合国内需求,具备高效开发、企业级可靠性及信创适配等优势,助力企业实现敏捷创新与数字化转型。
1083 0
颠覆开发效率!国内首个微服务编排框架Juggle开源啦!
|
SQL 存储 分布式数据库
分布式存储数据恢复—hbase和hive数据库数据恢复案例
分布式存储数据恢复环境: 16台某品牌R730xd服务器节点,每台服务器节点上有数台虚拟机。 虚拟机上部署Hbase和Hive数据库。 分布式存储故障: 数据库底层文件被误删除,数据库不能使用。要求恢复hbase和hive数据库。
528 12