关于.net:跨数据库微服务-FreeSql-分布式事务-TCCSaga-编排重要性

98次阅读

共计 7427 个字符,预计需要花费 19 分钟才能阅读完成。

💻 前言

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 必须定义成单例模式

new FreeSqlCloud\<DbEnum\>() 多连贯治理

new FreeSqlCloud\<DbEnum\>(“app001”) 开启 TCC/SAGA 事务失效


🚀 对于分布式事务

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, CreateTime = DateTime.Now})
            .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/Fr… 最宽松的 MIT 协定,可商用
csredis https://github.com/2881099/cs… 最宽松的 MIT 协定,可商用
FightLandlord 斗地主单机或网络版 https://github.com/2881099/Fi… 最宽松的 MIT 协定,学习用处
FreeScheduler 定时工作 https://github.com/2881099/Fr… 最宽松的 MIT 协定,可商用
IdleBus 闲暇容器 https://github.com/2881099/Id… 最宽松的 MIT 协定,可商用
FreeSql 国产最好用的 ORM https://github.com/dotnetcore… 最宽松的 MIT 协定,可商用
FreeSql.Cloud 分布式事务 tcc/saga https://github.com/2881099/Fr… 最宽松的 MIT 协定,可商用
FreeSql.AdminLTE 低代码后盾治理我的项目生成 https://github.com/2881099/Fr… 最宽松的 MIT 协定,可商用
FreeSql.DynamicProxy 动静代理 https://github.com/2881099/Fr… 最宽松的 MIT 协定,学习用处

须要的请拿走,这些都是最近几年的开源作品,以前更早写的就不发了。

QQ 群:4336577(已满)、8578575(在线)、52508226(在线)

正文完
 0