分布式事务二:基于RocketMQ最终消息一致分布式事务实现

声明: 本篇主要对所用到的技术做了归纳总结,对源码讲解较少,如果有基础的朋友可直接下载源码结合时序图更能容易理解;基础比较弱的朋友建议先看看资料自看源码这样更容易理解。这里的部分资料来源于网络,所以这里对那些资料提供者表达衷心的感谢。

方案:

  • 业务流程:Tss库向Saas转移库存,order为记录表。
  • 技术栈:Springboot+RocketMQ+mysq+postgreSQL
  • 代码地址:https://github.com/bao17634/SpringBoot-RocketMQ-Demo.git
1、方案时序图:

1.1、正常流程
在这里插入图片描述

  • 1)produce先向MQ server发送消息。
  • 2)MQ server将消息持久化成功后,向produce发送ACK消息,确认消息是否发送成功,此时消息为半消息
  • 3)如消息发送成功,则produce开始执行本地事务;如消息接收失败,后续也不执行任何操作。
  • 4) 如produce本地事务执行成功,produce向MQ server发送的commit进行确认,此时消息就会标记为可投递的消息;如MQ server收到Rollback的确认,就会删除半消息,consumer也不会接收到此消息,后面也无任何操作。
  • 5)MQ server将已标记的消息发送给consumer,consumer在接收到消息后就会执行相关业务操作。

1.2、MQ长时间按未收到produce的消息
在这里插入图片描述

  • 1)执行过程中MQ server长时间未收到produce的消息,就会调用checkLocalTransaction()方法进行回查。
  • 2)回查本地事务是否执行成功,如果执行成功则会让MQ server重新提交二次确认消息(Commit或RollBack),后续操作就会按照流程1.1中的步骤4和步骤5来执行。

1.3、消费端消息消费失败
在这里插入图片描述

  • 1)消费消费RocketMQ定义的有两种状态码:CONSUME_SUCCESSRECONSUME_LATER;如果消费端返回RECONSUME_LATER或出现异常则表示消息消费失败。
  • 2)MQ在消费端消息消费失败时,其内部的核心组件Broker在一段时间内(两小时内)会重新向消费端发送消息,最大可能的保证消息消费成功。
    MQ消息重试次数与时间关系如下图所示:
    [外链图片转存失败(img-7LDt89YD-1569380180085)(en-resource://database/1504:1)]
    可以看到,RocketMQ采用了“时间衰减策略”进行消息的重复投递,即重试次数越多,消息消费成功的可能性越小。

1.4 补充说明:

  • checkLocalTransaction()方法其实是MQ包中TransactionListener接口中方法的实现,所以在MQ回调checkLocalTransaction()是在MQ内部回调。但是在本方案我主要是检查本地事是否执行成功,跟执行本地事务的方法写在一起,所以我在时序图中将checkLocalTransaction()方法画在produce上,也为了理解方便。
  • 消费成功的消息MQ不会立即删除,还会继续存储在MQ的CommitLog文件中,在消费端消费完消息之后,在响应体中,broker会返回下一次应该拉取的位置,PushConsumer通过这一个位置,更新自己下一次的pull请求。这样就保证了正常情况下,消息只会被投递一次(消费端拉取消息的方式有两种:pushpull)。
  • 消息存储在CommitLog之后,清理只会在以下任一条件成立才会批量删除消息CommitLog文件。
    1. 消息文件过期(默认72小时),且到达清理时点(默认是凌晨4点),删除过期文件。
    2. 消息文件过期(默认72小时),且磁盘空间达到了水位线(默认75%),删除过期文件。
    3. 磁盘已经达到必须释放的上限(85%水位线)的时候,则开始批量清理文件(无论是否过期),直到空间充足。
    4. 若磁盘空间达到危险水位线(默认90%),出于保护自身的目的,broker会拒绝写入服务。
    5. 详解请点击
1.5 部分代码如下:

(1)produce配置

@Override
public void afterPropertiesSet() throws Exception {   
    producer.setNamesrvAddr("127.0.0.1:9876");    
    ExecutorService executorService = new ThreadPoolExecutor(2, 5, 100, TimeUnit.SECONDS, new ArrayBlockingQueue<Runnable>(2000), new ThreadFactory() {        
@Override        
public Thread newThread(Runnable r){            
    Thread thread = new Thread(r);            
    thread.setName("client-transaction-message-check-thread");            
    return thread;       
}   
});    
    //开启多线程,用于回查    
    producer.setExecutorService(executorService);    
    //设置回调事务检查监听器 
    producer.setTransactionListener(transactionListener);   
try {        
producer.start();   
} catch (MQClientException e) {       
    e.printStackTrace();   
}
}

(2)向MQ发送消息

Message msg = new Message("orderMessage", "order",orderDTO.getOrder().getOrderCode(),
                    JSON.toJSONString(orderDTO).getBytes(RemotingHelper.DEFAULT_CHARSET));
 //生产者通过sendMessageInTransaction向RocketMQ发送事务消息
SendResult sendResult = producer.sendMessageInTransaction(msg, null);

(3)配置consumer

DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("orderDemo");
consumer.setNamesrvAddr("127.0.0.1:9876");
consumer.setConsumeFromWhere(ConsumeFromWhere.CONSUME_FROM_FIRST_OFFSET);
consumer.subscribe("orderMessage", "*");

(4)consumer接收消息

consumer.registerMessageListener(new MessageListenerConcurrently() {    
@Override    
public ConsumeConcurrentlyStatus 
    consumeMessage(List<MessageExt> message,  ConsumeConcurrentlyContext context) {        
    ConsumeConcurrentlyStatus concurrentlyStatus = consumerService.consumeOrderMessage(message);       
    return concurrentlyStatus;   
}
});

(5)异常回调方法

@Override
public LocalTransactionState checkLocalTransaction(MessageExt msg) {   
LocalTransactionState state = LocalTransactionState.UNKNOW;    
    try {        
         OrderDTO orderDTO = JSON.parseObject(msg.getBody(), OrderDTO.class);       
    if (orderDTO != null) {
        state = LocalTransactionState.ROLLBACK_MESSAGE; 
    }            
         boolean isCommit = tssHouseService.checkTransferStatus(msg.getTransactionId());          
} catch (Exception e) { 
e.printStackTrace();  
}    
return state;
}
2、RocketMQ

RocketMQ 是一个分布式消息传递和流媒体平台,具有低延迟,高性能和可靠性,万亿级容量和灵活的可扩展性。
在这里插入图片描述
rocketMQ特点:

  • 是一个队列模型的消息中间件,具有高性能、高可靠、高实时、分布式特点。
  • Producer、Consumer队列都可以分布式。
  • Producer向一些队列轮流发送消息,队列集合称为Topic,Consumer如果做广播消费,则一个consumer实例消费这个Topic对应的所有队列,如果做集群消费,则多个Consumer实例平均消费这个topic对应的队列集合。
  • 能够保证严格的消息顺序
  • 提供丰富的消息拉取模式
  • 高效的订阅者水平扩展能力
  • 实时的消息订阅机制
  • 亿级消息堆积能力
  • 较少的依赖
2.1、RocketMQ、ActiveMQ、Kafka性能对比
消息中间件客户端SDK协议规范消息订购预定消息批量留言广播消息邮件过滤触发服务器重新发送消息消息存储消息追溯消息优先级高可用和故障转移消息跟踪组态管理和操作工具
ActiveMQjava、.NET、C++推模型、支持OPenWire、STOMP、AMQP、AMQP、MQTT、JMS独家消费者或独家排队可以确保订购支持不支持支持支持不支持使用JDBC和高性能日志(如levelDB,kahaDB)支持支持根据存储,如果使用kahadb,则需要ZooKeeper服务器不支持默认配置为低级别,用户需要优化配置参数支持
kafkaJava,Scala拉模型,支持TCP确保在分区内对消息进行排序不支持支持,使用异步生产者不支持支持,您可以使用Kafka Streams过滤邮件不支持高性能文件存储支持的偏移量表示不支持支持,需要ZooKeeper服务器不支持Kafka使用键值对格式进行配置。可以从文件或以编程方式提供这些值支持,使用terminal命令查看消息的信息
RocketMQJava,C ++,Go拉模型,支持TCP,JMS,OpenMessaging确保严格的消息排序,并可以优雅地扩展支持支持,使用同步模式以避免消息丢失支持支持的基于SQL92的属性过滤器表达式支持高性能和低延迟的文件存储支持的时间戳和偏移量2表示不支持支持的Master-Slave模型,没有其他套件支持开箱即用,用户只需要注意一些配置支持Web和终端命令,用于消息信息的展示
2.2、RocketMQ 核心组件图

RocketMQ是开源的消息中间件,它主要由NameServer,Producer,Broker,Consumer四部分构成。在这里插入图片描述
NameServer
NameServer主要负责Topic和路由信息的管理。
Producer
消息生产者,负责产生消息,一般由业务系统负责产生消息。
Broker
消息中转角色,负责存储消息,转发消息。
Consumer
消息消费者,负责消息消费,一般是后台系统负责异步消费。

2.3、RokcetMQ 物理部署图在这里插入图片描述

NameServer
NameServer是一个几乎无状态节点,可集群部署,节点之间无任何信息同步。
Broker
Broker分为Master与Slave,一个Master可以对应多个Slave,但是一个Slave只能对应一个Master,Master与Slave的对应关系通过指定相同的BrokerName,不同的BrokerId来定义,BrokerId为0表示Master,非0表示Slave。Master也可以部署多个。每个Broker与Name Server集群中的所有节点建立长连接,定时注册Topic信息到所有Name Server。
Producer
Producer与Name Server集群中的其中一个节点(随机选择)建立长连接,定期从Name Server取Topic路由信息,并向提供Topic服务的Master建立长连接,且定时向Master发送心跳。Producer完全无状态,可集群部署。
Consumer
Consumer与Name Server集群中的其中一个节点(随机选择)建立长连接,定期从Name Server取Topic路由信息,并向提供Topic服务的Master、Slave建立长连接,且定时向Master、Slave发送心跳。Consumer既可以从Master订阅消息,也可以从Slave订阅消息,订阅规则由Broker配置决定。

2.4、NameServer 路由注册、删除机制在这里插入图片描述
  • Broker每30秒向NameServer发送心跳包,心跳包中包含topic的路由信息
  • NarneServer 收到 Broker 心跳包后 更新 brokerLiveTable 中的信息, 特别记录心跳时间 lastUpdateTime。
  • NarneServer 每隔 10s 扫描 brokerLiveTable, 检 测表中上次收到心跳包的时间,比较当前时间 与上一次时间,如果超过120s,则认为 broker 不可用,移除路由表中与该 broker相关的所有信息。
  • 消息生产者拉取主题的路由信息,即消息生产者并不会立即感知 Broker 服务器的新增与删除。
2.5、RocketMQ的消息领域模型图在这里插入图片描述

Topic

  • Topic表示消息的第一级类型,比如一个电商系统的消息可以分为:交易消息、物流消息等。一条消息必须有一个Topic。
  • 最细粒度的订阅单位,一个Group可以订阅多个Topic的消息。

Tag
Tag表示消息的第二级类型,比如交易消息又可以分为:交易创建消息,交易完成消息等。RocketMQ提供2级消息分类,方便灵活控制。
Group
Group组,一个组可以订阅多个Topic。
Message Queue
消息的物理管理单位。一个Topic下可以有多个Queue,Queue的引入使得消息的存储可以分布式集群化,具有了水平扩展能力。在 RocketMQ 中,所有消息队列都是持久化,长度无限的数据结构,所谓长度无限是指队列中的每个存储单元都是定长,访问其中的存储单元使用 Offset 来访问,offset 为 java long 类型,64 位,理论上在 100年内不会溢出,所以认为是长度无限。也可以认为 Message Queue 是一个长度无限的数组,Offset 就是下标。

2.6、RocketMQ 消息存储设计原理图

在这里插入图片描述
CommitLog
消息存储文件,所有消息主题的消息都存储在 CommitLog 文件中。 Commitlog 文件存储的逻辑视图如图所示
在这里插入图片描述
ConsumeQueue
消息消费队列,消息到达 CommitLog 文件后,将异步转发到消息 消费队列,供消息消费者消费。ConsumeQueue存储格式如下:
在这里插入图片描述

  • 单个 ConsumeQueue 文件中默认包含 30 万个条目,单个文件的长度为 30w × 20 字节, 单个 ConsumeQueue 文件可以看出是一个 ConsumeQueue 条目的数组,其下标为 ConsumeQueue 的逻辑偏移量,消息消费进度存储的偏移量 即逻辑偏移量。
  • ConsumeQueue 即为 Commitlog 文件的索引文件, 其构建机制是当消息到达 Commitlog 文件后, 由专门的线程 产生消息转发任务,从而构建消息消费队列文件与下文提到的索引文件。
2.7 安装RocketMQ的步骤

注:下载最新版的rocketMQ版本,官方要求JDK版本1.8+,但是在用JDK11测试的时候 报一些不知名的错误后面就用JDK1.8,所以此处推荐用JDK1.8

3. 此方案总结

本方案基于支持事务消息的MQ(RocketMQ),其支持事务消息的方式采用类似于二阶段提交。此方案其实也是对本地消息表的封装,将本地消息表基于 MQ 内部,其他方面的协议基本与本地消息表一致。

优点:

  • 消息数据独立存储,降低业务系统与消息系统之间的耦合。
  • 吞吐量优于本地消息表方案。
  • 实现最终一致性,不需要依赖本地数据库事务。

缺点:

  • 一次消息发送需要两次网络请求(half消息 + commit/rollback)。
  • 需要实现消息回查接口。
  • 实现难度大,主流的MQ不支持,仅支持MQ也只有RocketMQ且事务消息部分代码也未开源。
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值