分布式事务系列--消息表+MQ

本地消息表+MQ(无事务)

其他网址

分布式事务之本地消息表_大数据_weixin_34327223的博客-CSDN博客
分布式事务-本地消息表 | Echo Blog
分布式事务--本地消息表(定时轮询扫描)_Java_王卫东的博客-CSDN博客
分布式事务探讨系列(三):本地消息表和MQ等可靠消息解决方案_数据库_lsblsb的专栏-CSDN博客

简介

说明
项目来源 源于eBay经典的BASE方案。ebay的完整方案https://queue.acm.org/detail.cfm?id=1394128
基本设计思想 将远程分布式事务拆分成一系列的本地事务
使用的重要技术 消息队列和消息应用状态表
优缺点 优点:基本避免了分布式事务,实现了“最终一致性”;开发简单
缺点:需设计DB消息表,同时还需要一个后台任务,不断扫描本地消息。导致消息的处理和业务逻辑耦合额外增加业务方的负担。
适用场景 适用于对一致性要求不高的非高并发场景。(本地消息队列是BASE理论,是最终一致模型)。

以跨行转账为例

  • 第一步:扣款 1万
  • 第二步:通知对方银行账户上加 1万。

 伪代码:

public void trans(){
    try{
        //操作数据库
        boolean result = dao.update(model);    //操作数据库失败会抛出异常
        
        //如果前一步成功,则投递消息
        if(result){
            mq.append(model);        //若失败,会抛出异常
        }
    }catch(Exception e){
        rollback();    //发生异常,则回滚
    }
}

根据上述代码及注释,我们来分析下可能的情况:

  1. 操作数据库成功,向 MQ 中投递消息也成功,皆大欢喜(没问题)
  2. 操作数据库成功,但是向 MQ 中投递消息时失败,向外抛出了异常,刚刚执行的更新数据库的操作将被回滚(没有问题)
  3. 操作数据库失败,不会向 MQ 中投递消息了(没问题)
  4. 操作数据库成功,result是true,这个时候服务挂了,没有投递消息给MQ。(有问题)最终结果是数据库操作成功,事件未被投递。
  5. 操作数据库成功,mq发送成功,但是返回响应的时候网络异常,导致append操作抛出异常。(有问题)最终结果是事件被投递,数据库却被回滚。

解决方法

法1:使用RabbitMq的消息确认机制

见《RabbitMQ实战指南》=> 4.8.2 发送方确认机制

将信道设为确认模式。异步操作:发送完之后通过回调函数获得结果(成功或失败),若结果是失败,则再次投递。

注意:此法仍然未解决上边的第4种情况。

法2:引入本地消息表(下边以此法示例)

分布式事务系列--消息表+MQ_第1张图片

 优化后的伪代码

public void trans(){
    boolean isSuccess = false;
    try{
        //1. 业务处理
        dao.update(model);      //操作数据库失败会抛出异常
        
        //2. 记录事件
        dao.insert(eventModel);  //操作数据库失败会抛出异常
        
        isSuccess = true;
    }catch(Exception e){
        rollback();    //发生异常,则回滚
        isSuccess = false;
    }
    
    //3. 发送消息事件,若失败,会有定时任务等从eventModel表中取出事件重新发送消息
    if(isSuccess){
        mq.append(model);
    }
}

 最终方案

  1. 扣款 1万
  2. 通过本地事务将事务消息(包括本地事务id、支付账户、收款账户、金额、状态等)插入至消息表。
  3. 通知对方银行账户上加 1万。
  4. 准备一个后台定时程序,源源不断的把消息表中(未经确认的)的message传送给消息中间件。失败了,不断重试重传。允许消息重复,但消息不会丢,顺序也不会打乱。

实例流程

我只是下了个订单,鬼知道我在微服务里经历了什么 - 简书

说明:这种分布式事务适合前边服务弱依赖于后边的服务的场景,比如付款成功增积分:积分的多少不影响付款。但对于强依赖的情况不合适,如下单减库存操作:是否能下单由库存是否充足而定,而下单是否成功由减库存是否成功而定;此时,用此法最合适:订单模块直接调用减库存的微服务接口。

本处以购物时的付款成功增积分为例说明。

分布式事务系列--消息表+MQ_第2张图片

原图网址:https://www.processon.com/diagraming/5ee053b7e0b34d4dba2f48f7
(参考https://processon.com/view/5b8655cae4b08faf8c3aaeeb)

错误处理

  • 步骤:1,2:任意一个出错,整个事务回滚,不会出错
  • 步骤3:投递消息成功,结果接收MQ响应时网络出错。不会出错
  • 步骤4:MQ宕机(解决方法:MQ一般支持持久化)。消费者消费失败(解决方法:重试3次)
  • 步骤4-8:4-8任意一个失败次数超过重试次数。解决方法:发送报警,让人工处理。
            从工程实践角度讲,这种整个流程自动回滚的代价是非常巨大的,不但实现复杂,还会引入新的问题。
    比如自动回滚失败,又怎么处理?对应这种极低概率的case,采取人工处理,会比实现一个高复杂的自动化回滚系统,更加可靠,也更加简单。
  • 步骤6:增积分完成之后,断网,导致增积分成功的消息没有发布出去。解决方法:定时任务会再次将消息投递,步骤5会判断到增积分已完成,直接跳到步骤8:发布消息
  • 一个原子性问题:如果保证消息消费 + insert message到判重表这2个操作的原子性?消费成功,但insert判重表失败,怎么办?关于这个,在Kafka的源码分析系列,第1篇,exactly once问题的时候,有过讨论。

MQ(有事务)

简介

其他网址

RocketMQ使用及分布式事务解决思路_数据库_易水寒的博客-CSDN博客
分布式事务-本地消息表 | Echo Blog        

        方案2的一个缺点:需要设计DB消息表,同时还需要一个后台任务,不断扫描本地消息。导致消息的处理和业务逻辑耦合额外增加业务方的负担。

        为了能解决该问题,同时又不和业务耦合,RocketMQ提出了“事务消息”的概念。阿里巴巴的RocketMQ中实现了分布式事务,实际上其实是对本地消息表的一个封装,将本地消息表移动到了MQ内部

        对比方案3和方案2,RocketMQ最大的改变,其实就是把“扫描消息表”这个事情,不让业务方做,而是消息中间件帮着做了。至于消息表,其实还是没有省掉。因为消息中间件要询问发送方,事物是否执行成功,还是需要一个“变相的本地消息表”,记录事物执行状态。

具体来说,就是把消息的发送分成了2个阶段:Prepare阶段和确认阶段。
具体来说,上面的2个步骤,被分解成3个步骤:

  • 第一步:生产者:向RocketMQ发送Prepared消息,生产者会拿到消息的地址。
  • 第二步:生产者:执行本地事务(修改数据库的数据)。
  • 第三步:生产者:
      若第二步执行成功,用第一步拿到的地址去访问消息,并修改状态(Confirm),消息接收者就能使用这个消息。
      若第二步执行失败,用第一步拿到的地址去访问消息,并取消Prepared消息,消息接收者就得不到这个消息。

分布式事务系列--消息表+MQ_第3张图片

1. Producer向broker端发送消息
2. 服务端将消息持久化成功之后,向发送方ACK确认消息已经发送成功,此时消息为半消息
3. 发送方开始执行本地事务逻辑
4. 发送方根据本地事务执行结果向服务端提交二次确认(Commit或者Rollback)。服务端收到Commit状态则将半消息标记为可投递,订阅方最终将收到该消息;服务端收到Rollback状态则删除半消息,订阅方将不会接受该消息。
5. 在断网或者是应用重启等特殊情况下,上述步骤4提交的二次确认最终未到达服务端,RocketMQ 会定期扫描消息集群中的事物消息,若发现未经确认的Prepared 消息,会对该消息发起消息回查
6. 发送方收到消息回查后,需要检查对应消息的本地事务执行的最终结果
7. 发送方根据检查得到的本地事务的最终状态再次提交二次确认,服务端仍按照4对半消息进行操作

错误处理

消费方重试全部失败怎么办?

解决方法:发送报警,让人工处理。
        从工程实践角度讲,这种整个流程自动回滚的代价是非常巨大的,不但实现复杂,还会引入新的问题。
比如自动回滚失败,又怎么处理?对应这种极低概率的case,采取人工处理,会比实现一个高复杂的自动化回滚系统,更加可靠,也更加简单。

实例

其他网址

RocketMQ分布式事务消息_盲流子的博客-CSDN博客
RocketMQ使用及分布式事务解决思路_数据库_易水寒的博客-CSDN博客

官方Springboot RocketMQ例子:https://github.com/ThierrySquirrel/rocketmq-spring-boot-starter

你可能感兴趣的:(分布式)