用CAP操作RabbitMQ 处理分布式事务的解决方案
一、在Nuget中引用以下包:
dotnetcore.cap DotNetCore.CAP.Dashboard DotNetCore.CAP.RabbitMQ DotNetCore.CAP.SqlServer
二、在Program.cs中注册服务
//配置CAP builder.Services.AddCap(x => { x.UseEntityFramework<AppDbContext>();//从AppDbContext获取数据库连接字符串 x.UseRabbitMQ("localhost");
x.DefaultGroupName = "cap.queue.app";//默认组名称 // Register Dashboard x.UseDashboard(); //使用仪表盘,默认网址:http://localhost:当前网站端口/cap //x.UseDashboard(opt => { opt.PathMatch = "/mycap"; });//配置仪表盘为新的网址 // Register to Consul x.UseDiscovery(d => { d.DiscoveryServerHostName = "localhost"; d.DiscoveryServerPort = 8500; d.CurrentNodeHostName = "localhost"; d.CurrentNodePort = 5800; d.NodeId = 1.ToString(); d.NodeName = "CAP No.1 Node"; }); });
三、发布消息
using CAPWebApplication.Entities; using DotNetCore.CAP; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Microsoft.Data.SqlClient; using Microsoft.EntityFrameworkCore; namespace CAPWebApplication.Controllers { [Route("api/[controller]")] [ApiController] public class PublishController : ControllerBase { private readonly ILogger<PublishController> _logger; private readonly ICapPublisher _capPublisher; private readonly AppDbContext _dbContext; public PublishController(ILogger<PublishController> logger, ICapPublisher capPublisher, AppDbContext dbContext) { _logger = logger; _capPublisher = capPublisher; _dbContext = dbContext; } //发送消息 [HttpGet("send")] public async Task<ActionResult> SendMessage() {//发布无事务的消息,消息重要性低时可以使用 await _capPublisher.PublishAsync("test.show.time", DateTime.Now); return Ok(); } [HttpGet("sendheader")] public async Task SendHeaderMessage() {//发布带有包头的消息 var header = new Dictionary<string, string> { ["my.header.first"] = "first", ["my.header.second"] = "second" }; _capPublisher.Publish("test.show.time", DateTime.Now, header); } [Route("adonet/transaction")] [HttpGet] public async Task AdonetWithTransaction() {//ADO.NET 事务处理 操作数据库并且发送消息 using (var conn = new SqlConnection(_dbContext.Database.GetConnectionString())) { using (var trans = conn.BeginTransaction(_capPublisher)) { await _capPublisher.PublishAsync("test.order", DateTime.Now); trans.Commit(); } } } [Route("efcore/transaction")] [HttpGet] public async Task EfcoreWithTransaction() {//EFCore 事件处理 操作数据库并且发送消息 using (var trans = _dbContext.Database.BeginTransaction(_capPublisher)) { Order order = new Order { Name = "赵六", Address = "湖南岳阳临湘" }; _dbContext.Add(order); await _dbContext.SaveChangesAsync(); await _capPublisher.PublishAsync("test.order", order); await trans.CommitAsync(); } } } }
四、订阅消息
4.1 在控制器中订阅消息
using CAPWebApplication.Entities; using DotNetCore.CAP; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Newtonsoft.Json; namespace CAPWebApplication.Controllers { [Route("api/[controller]")] [ApiController] public class ConsumerController : ControllerBase { //private readonly ILogger _logger; //public ConsumerController(ILogger logger) //{ // _logger = logger; //} //[NonAction] //[CapSubscribe("test.show.time")] //public async Task ReceiveMessage(DateTime time) //{ // Console.WriteLine($"接收到的时间 :{time}"); //} [NonAction] [CapSubscribe("test.show.time")] public async Task ReceiveHeaderMessage(DateTime time, [FromCap] CapHeader header) { Console.WriteLine($"接收到的时间:{time}"); Console.WriteLine($"第一个头部:{header["my.header.first"]}"); Console.WriteLine($"第二个头部:{header["my.header.second"]}"); } [NonAction] [CapSubscribe("test.order")] public async Task CheckReceivedMessage(Order order) { //string msg= System.Text.Json.JsonSerializer.Serialize(order); Console.WriteLine($"接受来自[test.order]的订单:{order}"); } } }
4.2 在业务逻辑订阅消息
如果你的订阅方法不在Controller中,那么你的订阅类需要实现ICapSubscribe
接口:
using CAPWebApplication.Entities; using DotNetCore.CAP; namespace CAPWebApplication.BusinessCode { public interface ISubscriberService { void CheckReceivedMessage(Order order); } public class SubscriberService : ISubscriberService, ICapSubscribe { [CapSubscribe("test.order")] public void CheckReceivedMessage(Order order) { Console.WriteLine($"来自业务层的订阅:{order}"); } } }
然后在Program.cs中注册ISubscriberService
builder.Services.AddTransient<ISubscriberService, SubscriberService>();
注意:注册必须放在配置CAP之前,即 builder.Services.AddCap() 之前
五、组订阅
同一消息在同一个组里面只有一订阅能收到消息,但在不同的组多个订阅都能收到消息。
订阅组的概念类似于 Kafka 中的消费者组。与消息队列中的广播方式相同,用于处理多个不同微服务实例之间的同一条消息。
CAP 启动时,会使用当前的程序集名称作为默认的组名,如果多个同组订阅者订阅同一个主题名,则只有一个订阅者可以接收到消息。相反,如果订阅者在不同的组中,他们都会收到消息。
在同一个应用程序中,您可以指定Group
属性以将订阅保存在不同的订阅组中:
using CAPWebApplication.Entities; using DotNetCore.CAP; namespace CAPWebApplication.BusinessCode { public interface ISubscriberService { void CheckReceivedMessage(Order order); void CheckReceivedMessage2(Order order); void CheckReceivedMessage3(Order order); } public class SubscriberService : ISubscriberService, ICapSubscribe { [CapSubscribe("test.order")] public void CheckReceivedMessage(Order order) { Console.WriteLine($"来自业务层的订阅:{order}"); } [CapSubscribe("test.order",Group ="group1")] public void CheckReceivedMessage2(Order order) { Console.WriteLine($"来自业务层group1的订阅:{order}"); } [CapSubscribe("test.order",Group ="group2")] public void CheckReceivedMessage3(Order order) { Console.WriteLine($"来自业务层group2的订阅:{order}"); } } }
三个订阅都能收到同一消息
六、主题订阅
将类上的订阅键+方法上的订阅键,最终组合在一起
实际的订阅键为:[CapSubscribe("test.order")]
[CapSubscribe("test")] [Route("api/[controller]")] [ApiController] public class ConsumerController : ControllerBase { [NonAction] [CapSubscribe("order")] public async Task CheckReceivedMessage(Order order) { //string msg= System.Text.Json.JsonSerializer.Serialize(order); Console.WriteLine($"来自控制器的订阅消息:{order}"); } }