【分布式事务框架CAP】2、案例
CAP集成到项目
添加Package
数据库使用Sqlserver,消息队列使用RabbitMQ
<PackageReference Include="DotNetCore.CAP" Version="3.1.2" />
<PackageReference Include="DotNetCore.CAP.RabbitMQ" Version="3.1.2" />
<PackageReference Include="DotNetCore.CAP.SqlServer" Version="3.1.2" />
Startup添加配置
public void ConfigureServices(IServiceCollection services)
{
services.AddControllersWithViews();
services.AddCap(option =>
{
//如果你使用的 EF 进行数据操作,你需要添加如下配置:
//option.UseEntityFramework<AppDbContext>(); //可选项,你不需要再次配置 option.UseSqlServer 了
//如果你使用的ADO.NET,根据数据库选择进行配置:
option.UseSqlServer("Server=SC-202003151209\\SL1;Database=CAPDB;User=sa;Password=123456;");
//option.UseMySql("连接字符串")
//CAP支持 RabbitMQ、Kafka、AzureServiceBus 等作为MQ,根据使用选择配置:
option.UseRabbitMQ(rabbitOption => {
rabbitOption.HostName = "xxxx";
rabbitOption.Port = 5672;
rabbitOption.Password = "xxxx";
rabbitOption.UserName = "xxxx";
rabbitOption.VirtualHost = "xxxx";
});
//使用Dashboard,这是一个Cap的可视化管理界面;默认地址:http://localhost:端口/cap
//option.UseDashboard();
//默认分组名,此值不配置时,默认值为当前程序集的名称
//option.DefaultGroup = "m";
//失败后的重试次数,默认50次;在FailedRetryInterval默认60秒的情况下,即默认重试50*60秒(50分钟)之后放弃失败重试
//option.FailedRetryCount = 10;
//失败后的重试间隔,默认60秒
//option.FailedRetryInterval = 30;
//设置成功信息的删除时间默认24*3600秒
//option.SucceedMessageExpiredAfter = 60 * 60;
});
}
仪表盘
仪表盘可以显示订阅列表、发送、接收、失败消息,并且可以操作消息的重复消费
添加Package:
<PackageReference Include="DotNetCore.CAP.Dashboard" Version="3.1.2" />
UseDashboard:
services.AddCap(option =>
{
option.UseDashboard();
}
访问仪表盘:http://xxxx/cap
发布消息
简单推送,不使用事务:
public async Task<string> Publish1([FromServices] ICapPublisher capBus)
{
//第一个参数是消息队列的topic
await capBus.PublishAsync("Meshop.PayService.Refund", new RefundMessage { OrderID = 1, RefundPrice = 10M });
return "发送成功";
}
使用事务,自动提交:
public async Task<string> Publish2([FromServices] ICapPublisher capBus)
{
using (var conn = new MySqlConnection("Server=xxxx;Port=3306;Database=CAPDB; User=root;Password=123456;"))
{
using (var tran = conn.BeginTransaction(capBus, true))//true表示自动提交事务
{
var orderMaster = new OrderMaster {ID=1,TotalPrice=5M, OrderState = 1 };
long id = await conn.InsertAsync(orderMaster, (IDbTransaction)tran.DbTransaction);
//自动提交事务时,PublishAsync应放在最后面
await capBus.PublishAsync("Meshop.PayService.Refund", new RefundMessage { OrderID = 1, RefundPrice = 100M });
}
}
return "发送成功";
}
使用事务,手动提交事务:
public async Task<string> Publish3([FromServices] ICapPublisher capBus)
{
using (var conn = new MySqlConnection("Server=xxxx;Port=3306;Database=CAPDB; User=root;Password=123456;"))
{
using (var tran = conn.BeginTransaction(capBus, false))//false表示手动提交事务
{
var orderMaster = new OrderMaster { ID = 1, TotalPrice = 5M, OrderState = 1 };
long id = await conn.InsertAsync(orderMaster, (IDbTransaction)tran.DbTransaction);
//执行异步的分布式事务,推送必须在transaction.Commit()事务提交语句之前执行
await capBus.PublishAsync("Meshop.PayService.Refund", new RefundMessage { OrderID = 1, RefundPrice = 100M });
//事务提交:如果connectionn.BeginTransaction(_capBus, false)的autoCommit参数为false,则需要手动提交事务
await tran.CommitAsync();
}
}
return "发送成功";
}
使用事务:业务中的数据库操作与消息表在同一个事务中
订阅消息
如果是在Controller中,直接添加[CapSubscribe("xxxx")]
来订阅相关消息。
[CapSubscribe("Meshop.PayService.Refund")]
public Task Refund(RefundMessage message)
{
return Task.CompletedTask;
}
如果你的方法没有位于Controller 中,那么你订阅的类需要继承 ICapSubscribe
,然后添加[CapSubscribe("xxxx")]
标记:
namespace xxx.Service
{
public interface ISubscriberService
{
public void CheckReceivedMessage(DateTime time);
}
public class SubscriberService: ISubscriberService, ICapSubscribe
{
[CapSubscribe("xxxx.services.show.time")]
public void CheckReceivedMessage(DateTime time)
{
}
}
}
然后在 Startup.cs 中的 ConfigureServices()
中注入你的 ISubscriberService
类
public void ConfigureServices(IServiceCollection services)
{
services.AddTransient<ISubscriberService,SubscriberService>();
}
带消息头的消息
推送消息:
/// <summary>
/// 推送包含头信息的消息
/// </summary>
/// <param name="capBus"></param>
/// <returns></returns>
public async Task<string> Publish4([FromServices] ICapPublisher capBus)
{
//包含头信息的消息
IDictionary<string, string> headers = new Dictionary<string, string>
{
["my.header.first"] = "first",
["my.header.second"] = "second"
};
await capBus.PublishAsync("Meshop.PayService.Refund.Header", new RefundMessage { OrderID = 1, RefundPrice = 10M }, headers);
return "发送成功";
}
订阅消息:
/// <summary>
/// 接受包含头信息的消息
/// </summary>
/// <param name="message"></param>
/// <returns></returns>
[NonAction]
[CapSubscribe("Meshop.PayService.Refund.Header")]
public Task Refund1(RefundMessage message,[FromCap]CapHeader header)
{
Console.WriteLine($"=====================orderID:{message.OrderID},refundPrice:{message.RefundPrice},first:{header["my.header.first"]}");
return Task.CompletedTask;
}
订阅者组
用来处理不同微服务实例之间同时消费相同的消息,当CAP启动的时候,它将创建一个默认的消费者组,如果多个相同消费者组的消费者消费同一个Topic消息的时候,只会有一个消费者被执行。相反,如果消费者都位于不同的消费者组,则所有的消费者都会被执行
推送消息:
public async Task<string> Publish1([FromServices] ICapPublisher capBus)
{
await capBus.PublishAsync("Meshop.PayService.Refund", new RefundMessage { OrderID = 1, RefundPrice = 10M });
return "发送成功";
}
订阅消息:
[NonAction]
[CapSubscribe("Meshop.PayService.Refund",Group ="g1")]
public Task Refund3(RefundMessage message)
{
Console.WriteLine($"=====================g1,Refund3,orderID:{message.OrderID},refundPrice:{message.RefundPrice}");
return Task.CompletedTask;
}
[NonAction]
[CapSubscribe("Meshop.PayService.Refund", Group = "g1")]
public Task Refund4(RefundMessage message)
{
Console.WriteLine($"=====================g1,Refund4,orderID:{message.OrderID},refundPrice:{message.RefundPrice}");
return Task.CompletedTask;
}
[NonAction]
[CapSubscribe("Meshop.PayService.Refund", Group = "g2")]
public Task Refund5(RefundMessage message)
{
Console.WriteLine($"=====================g2,Refund5,orderID:{message.OrderID},refundPrice:{message.RefundPrice}");
return Task.CompletedTask;
}
有两个分组g1、g2,Refund3、Refund4订阅者属于g1分组,Refund5订阅者属于g2分组。最终只有Refund3、Refund5会执行
事务补偿
某些情况下,消费者需要返回值以告诉发布者执行结果,以便于发布者实施一些动作,通常情况下这属于补偿范围。
你可以在消费者执行的代码中通过重新发布一个新消息来通知上游,CAP
提供了一种简单的方式来做到这一点。 你可以在发送的时候指定callbackName
来得到消费者的执行结果,通常这仅适用于点对点的消费。以下是一个示例。
// ============= Publisher =================
public async Task<string> Publish5([FromServices] ICapPublisher capBus)
{
await capBus.PublishAsync("Meshop.PayService.Refund.Return", new RefundMessage { OrderID = 1, RefundPrice = 10M }, "Meshop.PayService.Refund.ReturnStatus");
return "发送成功";
}
[NonAction]
[CapSubscribe("Meshop.PayService.Refund.ReturnStatus")]
public void RefundReturnStatus(RefundMessage message)
{
System.Console.WriteLine($"================orderID:{message.OrderID}");
}
// ============= Consumer ===================
[NonAction]
[CapSubscribe("Meshop.PayService.Refund.Return")]
public object RefundReturn(RefundMessage message)
{
Console.WriteLine($"=====================Return,orderID:{message.OrderID},refundPrice:{message.RefundPrice}");
return message;
}
版本号
通过本地数据表的Version字段进行版本隔离