如何使用分布式事务
约 729 字大约 2 分钟
2025-02-12
示例
假设我们现在有两个微服务 ServiceA 和 ServiceB,如何能做到调用这两个服务,确保它们要么都成功,要么都失败呢?
要做到这一点,就需要使用分布式事务功能。
在代码中,可以使用Mirage.RemoteClient实例来控制事务。
var gateways = new NetAddress[] { new NetAddress("127.0.0.1" , 8912)};
using ( var rc = new Mirage.RemoteClient(gateways))
{
//打开分布式事务
rc.BeginTransaction();
var serviceA = await rc.GetMicroServiceAsync("ServiceA");
var serviceB = await rc.GetMicroServiceAsync("ServiceB");
//调用服务A的接口
var retA = await serviceA.InvokeAsync<string>("Amethod", "ParamA" , 1 , 2);
//调用服务B的接口
var retB = await serviceB.InvokeAsync<string>("Bmethod", "ParamB");
//提交分布式事务
await rc.CommitTransactionAsync();
}
也可以让多个服务同时执行,不用 await 等待结果
var gateways = new NetAddress[] { new NetAddress("127.0.0.1" , 8912)};
using ( var rc = new Mirage.RemoteClient(gateways))
{
//打开分布式事务
rc.BeginTransaction();
var serviceA = await rc.GetMicroServiceAsync("ServiceA");
var serviceB = await rc.GetMicroServiceAsync("ServiceB");
//同时异步调用A、B服务接口
serviceA.InvokeAsync("Amethod", "ParamA" , 1 , 2);
serviceB.InvokeAsync("Bmethod", "ParamB");
//等待所有服务执行完毕,并提交分布式事务
await rc.CommitTransactionAsync();
}
死锁案例
让我们看看一段调用微服务的代码:
using ( var rc = new Mirage.RemoteClient(gateways))
{
//打开分布式事务
rc.BeginTransaction();
var userService = await rc.GetMicroServiceAsync("UserInfoService");
//给userid=1的用户增加100积分
userService.InvokeAsync("AddPoints", 1 , 100);
//给userid=1的用户减少80积分
userService.InvokeAsync("ReducePoints", 1 , 80);
//等待所有服务执行完毕,并提交分布式事务
await rc.CommitTransactionAsync();
}
这段代码在一个分布式事务中,调用了两次 UserInfoService 服务。第一次是给用户 ID 为 1 的用户添加 100 积分,第二次是给用户 ID 为 1 的用户扣除 80 积分。代码在“扣除 80 积分”的位置上会被卡住,形成死锁。
为什么会这样呢? 首先每调用一次服务,都是一次网络请求,这是毋庸置疑的,而服务端在收到请求后,肯定是创建一个全新的数据库连接来进行业务处理。
第一次调用 UserInfoService 服务,在给用户 ID 为 1 的用户添加 100 积分的过程中,UserInfoService 服务开启了数据库事务并 update 了用户 ID 为 1 的积分数据(形成更新锁)。
第二次调用 UserInfoService 服务,要给用户 ID 为 1 的用户扣除 80 积分时,也会开启数据库事务并 update 用户 ID 为 1 的积分数据。但该数据已经被另一个请求线程锁定,因此该线程将永远等待,导致死锁的产生。
死锁解决方案
Mirage.ServiceProvider 升级到 >=1.0.1 (针对 Mirage 工程)
Mirage.ServiceProvider.AspNetCore 升级到 >=1.0.1 (针对 Asp.Net 工程)
Mirage.Invoker 升级到 >=1.0.1
微服务里使用的数据库对象务必使用 AddScoped 的依赖注入方式,这样才能确保在同一个分布式事务的周期里,多次请求同一个服务,所使用的数据库对象都是同一个。