第十六节:CAP框架异常处理、实现分布式事务(最终一致性) 及 其它用法
一. CAP框架异常处理
1. RabbitMQ宕机
(1).模拟场景
直接把RabbitMq服务关闭,然后发送5次请求,会发现Published表中多了5条数据!!!!Received表中没有数据;然后打开RabbitMq服务,观察现象,仔细观察Published表,有3条记录已经重试了3次,是Failed,另外两条打开服务后,重新发送成功。
(2).实现原理
当将RabbitMQ启动后,消息正常发送,CAP框架内部使用定时器轮询机制实现读取DB中的未成功的数据发送给MQ.
(3).问题
重试3次仍然失败的数据后续怎么办,后续怎么是否还重试呢?
答:大约4分钟后,失败的3条数据重新发送成功了.(这里可能有个bug,重试次数还是为3,没有加1)
2. 发布者执行业务成功,发送消息时宕机
(1).模拟场景
这里指的是调用publish的时候失败(不太好模拟),数据还是会存放在Published表中。
(2).实现原理
本地消息表、定时器轮询、幂等性
3. 订阅者接收消息失败(或者接收消息成功,执行业务失败)
(1).模拟场景1:接收消息失败
关闭订阅者服务,然后发送5次请求,发现Published表中多了5条数据,且都是发送成功的, 此时Received表中没有数据!!打开订阅者服务,发现很快多了5条数据,且为成功状态,还有订阅者的控制台收到了发送过来的5次请求。
(2).模拟场景2:接收消息成功,执行业务失败
有3种写法,只有直接异常不补获 或者 进入catch显式throw抛出异常, Cap框架才认为消费失败,,Received表中有数据,但状态是Failed!!
A. 直接异常不补获,Cap认为是失败的,会快速重试3次,然后4min中后,每隔FailedRetryInterval(自己配置),重试1次。
B. try-catch捕获异常,Cap认为是成功的
C. catch显式throw抛出异常, Cap认为是失败的,会快速重试3次,然后4min中后,每隔FailedRetryInterval(自己配置),重试1次。
补充:这里即使Cap认为是失败,MQ的中的消息也被删除了,这里CAP框架就是这么设计的,个人不太倾向失败也删除消息(个人倾向消息发送成功且本地业务也执行成功,再反馈ACK,MQ删除消息)。
代码分享:
/// <summary> /// 订阅者的方法 /// </summary> /// <param name="time"></param> [NonAction] [CapSubscribe("ypfkey1")] public void ReceiveMsg(DateTime time) { //1. 正常接收 { _log.LogInformation($"我是订阅者,收到的内容为:{time}"); } //2. 异常接收(Cap认为是失败的) //{ // int.Parse("dsfdsf"); //模拟业务报错 // _log.LogInformation($"我是订阅者,收到的内容为:{time}"); //} //3. 异常接收(Cap认为是成功的) //{ // try // { // int.Parse("dsfdsf");//模拟业务报错 // _log.LogInformation($"我是订阅者,收到的内容为:{time}"); // } // catch (Exception ex) // { // _log.LogInformation($"业务执行失败了:{ex.Message}"); // } //} //4. 异常接收(catch中显式抛异常) //{ // try // { // int.Parse("dsfdsf");//模拟业务报错 // _log.LogInformation($"我是订阅者,收到的内容为:{time}"); // } // catch (Exception ex) // { // _log.LogInformation($"业务执行失败了:{ex.Message}"); // throw new Exception($"业务执行失败了:{ex.Message}"); // } //} }
(2).实现原理:
A.使用本地消息表解决(思想:持久化操作);
B.定时器,消息重试;
C.幂等性 一个函数每次都是相同的结果,状态只有一个;
4. 订阅者消费重试后,还是消费失败
(1). 准备:
通过‘人工干预’的方式解决,通过Nuget安装给两个项目安装【DotNetCore.CAP.Dashboard 5.0.3】,然后在ConfigureService中的AddCap添加代码:x.UseDashboard(); 通过下面两个地址进行人工干预:
http://localhost:9001/cap 处理发布者-发送失败的消息
http://localhost:9011/cap 处理订阅者-接收失败的消息
(2). 模拟场景:
先制造一个订阅者接收消息成功,但执行业务失败的情况,登录后台cap,查看异常;此时把订阅者接口改成正常接收,在cap后台手动点击异常记录,进行重新消费,消费成功,DB的记录变为succeed。
如下图:
二. 基于Cap实现分布式事务
1. 什么是分布式事务
分布式系统会把一个应用系统拆分为可独立部署的多个服务,因此需要服务与服务之间远程协作才能完成事务操作,这种分布式系统环境下由不同的服务之间通过网络远程协作完成事务称之为分布式事务,例如用户注册送积分事务、创建订单减库存事务,银行转账事务等都是分布式事务。
简单的来说:接口A中除了要执行自身业务,还需要通过网络通信调用另外一个服务中的接口,自身业务+调用另外一个服务接口 就组成一个分布式事务。
2. Cap框架是处理分布式事务的?
CAP 不直接提供开箱即用的基于 DTC 或者 2PC 的分布式事务,相反我们提供一种可以用于解决在分布式事务遇到的问题的一种解决方案。在分布式环境中,由于涉及通讯的开销,使用基于2PC或DTC的分布式事务将非常昂贵,在性能方面也同样如此。另外由于基于2PC或DTC的分布式事务同样受**CAP定理**的约束,当发生网络分区时它将不得不放弃可用性(CAP中的A)。针对于分布式事务的处理,CAP 采用的是“异步确保”这种方案(也叫本地消息表),实现最终一致性。
下面实操的结论:
多个publish 和 本地业务DB操作,每个publish可能对应一项其它DB的操作,最后统一提交,
(1). 本地业务DB操作失败则整体失败,publish不会执行,本地published表中也不会存储消息记录(无论先业务DB操作,还是先publish操作,结果都一样)。
(2). publish向MQ中发送失败,本地DB业务执行成功,这种情况下本地published表中会存储消息发送记录,可以进行发送重试,然后订阅者那也有持久化功能,可以进行消费重试或者手动干预,最终保证都能成功。
3. 实操分析
详见:PubController中的SendMsg2方法,调用地址:http://localhost:9001/api/Pub/SendMsg2 进行测试.
(1).模拟1:DB操作和消息publish统一提交成功。
(2).模拟2:先消息publish发送,后DB操作,统一提交,模拟DB操作失败,则消息的publish也发送失败(本地表中没数据,根本不执行哦) 【本地业务执行失败,是必须要回滚的,所以不往消息表中插入数据】
(3).模拟3:先DB操作,后消息publish,统一提交,关闭RabbitMQ,模拟消息发送失败,但业务正常执行完,没有进入catch,DB操作成功,publish在本地表中插入数据,只不过数据状态是失败的。
(4).模拟4:事务自动提交,不需要调用trans.Commit(),提交成功。
(5).模拟5:多个publish统一提交(假设每个publish对应一个不同的DB),但这里必须使用手动事务提交,手动trans.Commit(),最终提交成功。
PS:特别注意以上2和3的对比,5要手动提交
/// <summary> /// 发布者调用的方法2(有事务) /// </summary> /// <returns></returns> public IActionResult SendMsg2() { try { #region 01-业务都执行成功 //using (var trans = _dbContext.Database.BeginTransaction(_capBus, autoCommit: false)) //{ // //业务1 // _dbContext.OrderInfor.Add(new OrderInfor() // { // id = Guid.NewGuid().ToString("N"), // orderNum = new Random().Next(99999, 999999).ToString(), // addTime = DateTime.Now // }); // //业务2 // var nowTime = DateTime.Now; // _capBus.Publish("ypfkey1", nowTime); // _log.LogInformation($"我是发布者,发布的内容为:{nowTime}"); // _dbContext.SaveChanges(); // trans.Commit(); //} #endregion #region 02-模拟DB插入失败 //using (var trans = _dbContext.Database.BeginTransaction(_capBus, autoCommit: false)) //{ // //业务1 // var nowTime = DateTime.Now; // _capBus.Publish("ypfkey1", nowTime); // _log.LogInformation($"我是发布者,发布的内容为:{nowTime}"); // //业务2 // _dbContext.OrderInfor.Add(new OrderInfor() // { // id = Guid.NewGuid().ToString("N"), // orderNum = new Random().Next(9999999, 999999999).ToString(), //模拟DB插入失败 // addTime = DateTime.Now // }); // _dbContext.SaveChanges(); // trans.Commit(); //} #endregion #region 03-模拟消息发送失败(关闭RabbitMq) //using (var trans = _dbContext.Database.BeginTransaction(_capBus, autoCommit: false)) //{ // //业务1 // _dbContext.OrderInfor.Add(new OrderInfor() // { // id = Guid.NewGuid().ToString("N"), // orderNum = new Random().Next(99999, 999999).ToString(), // addTime = DateTime.Now // }); // //业务2 // var nowTime = DateTime.Now; // _capBus.Publish("ypfkey1", nowTime); // _log.LogInformation($"我是发布者,发布的内容为:{nowTime}"); // _dbContext.SaveChanges(); // trans.Commit(); //} #endregion #region 04-业务都执行成功(事务自动提交) //using (var trans = _dbContext.Database.BeginTransaction(_capBus, autoCommit: true)) //{ // //业务1 // _dbContext.OrderInfor.Add(new OrderInfor() // { // id = Guid.NewGuid().ToString("N"), // orderNum = new Random().Next(99999, 999999).ToString(), // addTime = DateTime.Now // }); // //业务2 // var nowTime = DateTime.Now; // _capBus.Publish("ypfkey1", nowTime); // _log.LogInformation($"我是发布者,发布的内容为:{nowTime}"); // _dbContext.SaveChanges(); //} #endregion #region 05-多个publish //using (var trans = _dbContext.Database.BeginTransaction(_capBus, autoCommit: false)) //{ // var nowTime = DateTime.Now; // //业务1 // _capBus.Publish("ypfkey1", nowTime); // _log.LogInformation($"我是发布者,发布的内容为:{nowTime}"); // Thread.Sleep(5000); // //业务2 // _capBus.Publish("ypfkey1", nowTime.AddDays(1)); // _log.LogInformation($"我是发布者,发布的内容为:{nowTime.AddDays(1)}"); // Thread.Sleep(8000); // //业务3 // _capBus.Publish("ypfkey1", nowTime.AddDays(2)); // _log.LogInformation($"我是发布者,发布的内容为:{nowTime.AddDays(2)}"); // trans.Commit(); //} #endregion return Content("发送成功"); } catch (Exception ex) { return Content($"发送失败:{ex.Message}"); } }
三. Cap其它用法
1. 其它用法
(1).配置
x.FailedRetryInterval = 60; //重试间隔,4min后该值设置生效(默认快速重试3次)--服务于发送重试和消费重试
x.FailedRetryCount = 50; //重试的最大次数
x.ConsumerThreadCount = 1; //消费者线程并行处理消息的线程数,提高消费速度,但这个值大于1时,将不能保证消息执行的顺序。
x.SucceedMessageExpiredAfter = 24 * 3600; //成功消息的过期时间(过期则删除)
x.FailedThresholdCallback //重试阈值的失败回调
详见:https://cap.dotnetcore.xyz/user-guide/zh/cap/configuration/
(2).消息
事务补偿、header和body分离、消息重试(发送和消费)、消息数据清理
详见:https://cap.dotnetcore.xyz/user-guide/zh/cap/messaging/
(3).幂等性
详见:https://cap.dotnetcore.xyz/user-guide/zh/cap/idempotence/
2.事务补偿实操
(1).含义
某些情况下,消费者需要返回值以告诉发布者执行结果,以便于发布者实施一些动作,通常情况下这属于补偿范围。
(2).模拟场景
发布者中有两个方法SendMsg3和MarkStatus方法,MarkStatus用于接收消费者回传的信息,该方法需要标记 [CapSubscribe("mStatusKey")]特性,特性的值为publish方法的第三个参数声明,eg:_capBus.Publish("pOrderkey", orderNum, "mStatusKey");
(3).测试
请求 http://localhost:9001/api/Pub/SendMsg3 ,发布者的控制台除了发送成功外,还收到消费者消费成功的信息,说明测试通过。
发布者代码:
public IActionResult SendMsg3() { var orderNum = new Random().Next(99999, 999999); _capBus.Publish("pOrderkey", orderNum, "mStatusKey"); _log.LogInformation($"我是发布者,发布的内容为:{orderNum}"); return Content("发送成功"); } [NonAction] [CapSubscribe("mStatusKey")] public void MarkStatus(string msg) { _log.LogInformation($"我是发布者,接收到回传信息为:{msg}"); if (msg=="ok") { _log.LogInformation($"消费者消费成功了"); } else { _log.LogInformation($"消费者消费失败了"); } }
订阅者代码:
[NonAction] [CapSubscribe("pOrderkey")] public string ReceiveMsg2(string orderNum) { _log.LogInformation($"我是订阅者,收到的内容为:{orderNum}"); return "ok"; }
运行截图:
3.表头发送实操
请求 http://localhost:9001/api/Pub/SendMsg4,代码如下:
发布者:
public IActionResult SendMsg4() { var header = new Dictionary<string, string>() { ["my.header.first"] = "first", ["my.header.second"] = "second" }; var orderNum = new Random().Next(99999, 999999); _capBus.Publish("ypfkey2", orderNum, header); _log.LogInformation($"我是发布者,发布的内容为:{orderNum}"); return Content("发送成功"); }
订阅者:
[CapSubscribe("ypfkey2")] public void ReceiveMessage(string orderNum, [FromCap] CapHeader header) { Console.WriteLine($"我是订阅者,收到的内容为:{orderNum}"); Console.WriteLine("message firset header :" + header["my.header.first"]); Console.WriteLine("message second header :" + header["my.header.second"]); }
4.获取发送表和接收表的名称
未测试,基于关系型数据库存储的时候可以使用,比如基于:SQLServer存储和MySQL存储
参考:https://cap.dotnetcore.xyz/user-guide/zh/storage/sqlserver/
5.类中接收消息
未测试,参考:https://www.cnblogs.com/savorboard/p/cap.html
!
- 作 者 : Yaopengfei(姚鹏飞)
- 博客地址 : http://www.cnblogs.com/yaopengfei/
- 声 明1 : 如有错误,欢迎讨论,请勿谩骂^_^。
- 声 明2 : 原创博客请在转载时保留原文链接或在文章开头加上本人博客地址,否则保留追究法律责任的权利。