【分布式事务框架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字段进行版本隔离

posted @ 2021-01-28 20:38  .Neterr  阅读(671)  评论(0编辑  收藏  举报