RocketMQ事务性消息及持久化
TransactionProducer(事务消息):
在分布式系统中,我们时常会遇到分布式事务的问题,除了常规的解决方案之外,我们还可以利用RocketMQ的事务性消息来解决分布式事务的问题。RocketMQ和其他消息中间件最大的一个区别是支持了事务消息,这也是分布式事务里面的基于消息的最终一致性方案。
RocketMQ消息的事务架构设计:
- 生产者执行本地事务,修改订单支付状态,并且提交事务
- 生产者发送事务消息到broker上,消息发送到broker上在没有确认之前,消息对于consumer是不可见状态
- 生产者确认事务消息,使得发送到broker上的事务消息对于消费者可见
- 消费者获取到消息进行消费,消费完之后执行ack进行确认
这里可能会存在一个问题,生产者本地事务成功后,发送事务确认消息到broker上失败了怎么办?这个时候意味着消费者无法正常消费到这个消息。所以RocketMQ提供了消息回查机制,如果事务消息一直处于中间状态,broker会发起重试去查询broker上这个事务的处理状态。一旦发现事务处理成功,则把当前这条消息设置为可见。

RocketMQ事务消息的实践:
生产者producer:
public class TransactionProducer {
public static void main(String[] args) throws MQClientException, UnsupportedEncodingException, InterruptedException {
TransactionMQProducer transactionMQProducer=new
TransactionMQProducer("tx_producer");
transactionMQProducer.setNamesrvAddr("192.168.1.101:9876;192.168.1.102:9876");
ExecutorService executorService= Executors.newFixedThreadPool();
transactionMQProducer.setExecutorService(executorService);
transactionMQProducer.setTransactionListener(new TransactionListenerLocal()); //本地事务的监听
transactionMQProducer.start();
for(int i=;i<;i++){
String orderId= UUID.randomUUID().toString();
String body="{'operation':'doOrder','orderId':'"+orderId+"'}";
Message message=new Message("testTopic2",
null,orderId,body.getBytes(RemotingHelper.DEFAULT_CHARSET));
transactionMQProducer.sendMessageInTransaction(message,orderId);
Thread.sleep();
}
}
}
TransactionListenerLocal:
public class TransactionListenerLocal implements TransactionListener {
private Map<String,Boolean> results=new ConcurrentHashMap<>();
//执行本地事务
@Override
public LocalTransactionState executeLocalTransaction(Message message, Object o) {
System.out.println("开始执行本地事务:"+o.toString()); //o
String orderId=o.toString();
//模拟数据库保存(成功/失败)
boolean result=Math.abs(Objects.hash(orderId))%==;
if(!result) {
results.put(orderId, result); //
}
return result? LocalTransactionState.COMMIT_MESSAGE: LocalTransactionState.UNKNOW;
}
//提供给事务执行状态检查的回调方法,给broker用的(异步回调)
//如果回查失败,消息就丢弃
@Override
public LocalTransactionState checkLocalTransaction(MessageExt messageExt) {
String orderId=messageExt.getKeys();
System.out.println("执行事务回调检查: orderId:"+orderId);
if(results.size()==){
return LocalTransactionState.COMMIT_MESSAGE;
}
return LocalTransactionState.COMMIT_MESSAGE;
}
}
消费端 consumer:
public class TransactionConsumer {
//rocketMQ 除了在同一个组和不同组之间的消费者的特性和kafka相同之外
//RocketMQ可以支持广播消息,就意味着,同一个group的每个消费者都可以消费同一个消息
public static void main(String[] args) throws MQClientException {
DefaultMQPushConsumer defaultMQPushConsumer=
new DefaultMQPushConsumer("tx_consumer");
defaultMQPushConsumer.setNamesrvAddr("192.168.1.101:9876;192.168.1.102:9876");
defaultMQPushConsumer.setConsumeFromWhere(ConsumeFromWhere.CONSUME_FROM_FIRST_OFFSET);
//subExpression 可以支持sql的表达式. or and a=? ,,,
defaultMQPushConsumer.subscribe("testTopic2","*");
defaultMQPushConsumer.registerMessageListener(new MessageListenerConcurrently() {
@Override
public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> list, ConsumeConcurrentlyContext consumeConcurrentlyContext) {
list.stream().forEach(message->{
System.out.println("开始业务处理逻辑:消息体:"+new String(message.getBody())+"->key:"+message.getKeys());
});
return ConsumeConcurrentlyStatus.CONSUME_SUCCESS; //签收
}
});
defaultMQPushConsumer.start();
}
}
RocketMQ事务消息的三种状态:
- ROLLBACK_MESSAGE:回滚事务
- COMMIT_MESSAGE: 提交事务
- UNKNOW: broker会定时的回查Producer消息状态,直到彻底成功或失败。
当executeLocalTransaction方法返回ROLLBACK_MESSAGE时,表示直接回滚事务,当返回COMMIT_MESSAGE提交事务当返回UNKNOW时,Broker会在一段时间之后回查checkLocalTransaction,根据checkLocalTransaction返回状态执行事务的操作(回滚或提交),如示例中,当返回ROLLBACK_MESSAGE时消费者不会收到消息,且不会调用回查函数,当返回COMMIT_MESSAGE时事务提交,消费者收到消息,当返回UNKNOW时,在一段时间之后调用回查函数,并根据status判断返回提交或回滚状态,返回提交状态的消息将会被消费者消费,所以此时消费者可以消费部分消息
消息的存储和发送:
由于分布式消息队列对于可靠性的要求比较高,所以需要保证生产者将消息发送到broker之后,保证消息是不出现丢失的,因此消息队列就少不了对于可靠性存储的要求
从主流的几种MQ消息队列采用的存储方式来看,主要会有三种
- 分布式KV存储,比如ActiveMQ中采用的levelDB、Redis, 这种存储方式对于消息读写能力要求不高的情况下可以使用
- 文件系统存储,常见的比如kafka、RocketMQ、RabbitMQ都是采用消息刷盘到所部署的机器上的文件系统来做持久化,这种方案适合对于有高吞吐量要求的消息中间件,因为消息刷盘是一种高效率,高可靠、高性能的持久化方式,除非磁盘出现故障,否则一般是不会出现无法持久化的问题
- 关系型数据库,比如ActiveMQ可以采用mysql作为消息存储,关系型数据库在单表数据量达到千万级的情况下IO性能会出现瓶颈,所以ActiveMQ并不适合于高吞吐量的消息队列场景。总的来说,对于存储效率,文件系统要优于分布式KV存储,分布式KV存储要优于关系型数据库
消息的存储结构:
RocketMQ就是采用文件系统的方式来存储消息,消息的存储是由ConsumeQueue和CommitLog配合完成的。CommitLog是消息真正的物理存储文件。ConsumeQueue是消息的逻辑队列,有点类似于数据库的索引文件,里面存储的是指向CommitLog文件中消息存储的地址。每个Topic下的每个Message Queue都会对应一个ConsumeQueue文件,文件的地址是:${store_home}/consumequeue/${topicNmae}/${queueId}/${filename}, 默认路径: /root/store在rocketMQ的文件存储目录下,可以看到这样一个结构的的而文件。

CommitLog:
CommitLog是用来存放消息的物理文件,每个broker上的commitLog本当前机器上的所有consumerQueue共享,不做任何的区分。CommitLog中的文件默认大小为1G,可以动态配置; 当一个文件写满以后,会生成一个新的commitlog文件。所有的Topic数据是顺序写入在CommitLog文件中的。文件名的长度为20位,左边补0,剩余未起始偏移量,比如00000000000000000000 表示第一个文件, 文件大小为102410241024,当第一个文件写满之后,生成第二个文件000000000001073741824 表示第二个文件,起始偏移量为1073741824。
ConsumeQueue:
consumeQueue表示消息消费的逻辑队列,这里面包含MessageQueue在commitlog中的其实物理位置偏移量offset,消息实体内容的大小和Message Tag的hash值。对于实际物理存储来说,consumeQueue对应每个topic和queueid下的文件,每个consumeQueue类型的文件也是有大小,每个文件默认大小约为600W个字节,如果文件满了后会也会生成一个新的文件。
IndexFile:
索引文件,如果一个消息包含Key值的话,会使用IndexFile存储消息索引。Index索引文件提供了对CommitLog进行数据检索,提供了一种通过key或者时间区间来查找CommitLog中的消息的方法。在物理存储中,文件名是以创建的时间戳明明,固定的单个IndexFile大小大概为400M,一个IndexFile可以保存2000W个索引。
abort:
broker在启动的时候会创建一个空的名为abort的文件,并在shutdown时将其删除,用于标识进程是否正常退出,如果不正常退出,会在启动时做故障恢复。
Config:

可以看到这个里面保存了 消费端consumer的偏移量:

以及topic的一些配置信息:

消息存储的整体结构:

RocketMQ的消息存储采用的是混合型的存储结构,也就是Broker单个实例下的所有队列公用一个日志数据文件CommitLog。这个是和Kafka又一个不同之处。为什么不采用kafka的设计,针对不同的partition存储一个独立的物理文件呢?这是因为在kafka的设计中,一旦kafka中Topic的Partition数量过多,队列文件会过多,那么会给磁盘的IO读写造成比较大的压力,也就造成了性能瓶颈。所以RocketMQ进行了优化,消息主题统一存储在CommitLog中。当然它也有它的优缺点
- 优点在于:由于消息主题都是通过CommitLog来进行读写,ConsumerQueue中只存储很少的数据,所以队列更加轻量化。对于磁盘的访问是串行化从而避免了磁盘的竞争
- 缺点在于:消息写入磁盘虽然是基于顺序写,但是读的过程确是随机的。读取一条消息会先读取ConsumeQueue,再读CommitLog,会降低消息读的效率。
消息发送到消息接收的整体流程:
1. Producer将消息发送到Broker后,Broker会采用同步或者异步的方式把消息写入到CommitLog。RocketMQ所有的消息都会存放在CommitLog中,为了保证消息存储不发生混乱,对CommitLog写之前会加锁,同时也可以使得消息能够被顺序写入到CommitLog,只要消息被持久化到磁盘文件CommitLog,那么就可以保证Producer发送的消息不会丢失。

2. commitLog持久化后,会把里面的消息Dispatch到对应的Consume Queue上,Consume Queue相当于kafka中的partition,是一个逻辑队列,存储了这个Queue在CommiLog中的起始offset,log大小和MessageTag的hashCode。

3. 当消费者进行消息消费时,会先读取consumerQueue , 逻辑消费队列ConsumeQueue保存了指定Topic下的队列消息在CommitLog中的起始物理偏移量Offset,消息大小、和消息Tag的HashCode值

4. 直接从consumequeue中读取消息是没有数据的,真正的消息主体在commitlog中,所以还需要从commitlog中读取消息

什么时候清理物理消息文件?那消息文件到底删不删,什么时候删?
消息存储在CommitLog之后,的确是会被清理的,但是这个清理只会在以下任一条件成立才会批量删除消息文件(CommitLog):
- 消息文件过期(默认48小时),且到达清理时点(默认是凌晨4点),删除过期文件。
- 消息文件过期(默认48小时),且磁盘空间达到了水位线(默认75%),删除过期文件。
- 磁盘已经达到必须释放的上限(85%水位线)的时候,则开始批量清理文件(无论是否过期),直到空间充足。
注:若磁盘空间达到危险水位线(默认90%),出于保护自身的目的,broker会拒绝写入服务。
RocketMQ事务性消息及持久化的更多相关文章
- RocketMQ事务性消息
mq事务介绍 mq事务消息流程 生产者发送消息到mq,消息状态为:SEND_OK.此消息是消费者不可见(消费者无法消费此条消息) 执行本地任务:成功则返回COMMIT_MESSAGE,此时消费者可消费 ...
- rocketmq总结(消息的高可用、中间件选型)
rocketmq总结(消息的高可用.中间件选型) 参考: https://blog.csdn.net/meilong_whpu/article/details/76922456 http://blog ...
- RocketMQ源码 — 九、 RocketMQ延时消息
上一节消息重试里面提到了重试的消息可以被延时消费,其实除此之外,用户发送的消息也可以指定延时时间(更准确的说是延时等级),然后在指定延时时间之后投递消息,然后被consumer消费.阿里云的ons还支 ...
- 针对缓存在Redis中的聊天消息的持久化方案分析
选型依据 数据库的选型主要考虑一下几个方面: 数据库本身是否收费 数据库后期维护成本 是否支持水平及垂直扩展,及扩展的容易程度 业务数据本身特性 使用此数据库的开发成本 由于此数据库主要用来存储缓存在 ...
- RocketMQ源码分析之RocketMQ事务消息实现原理上篇(二阶段提交)
在阅读本文前,若您对RocketMQ技术感兴趣,请加入 RocketMQ技术交流群 根据上文的描述,发送事务消息的入口为: TransactionMQProducer#sendMessageInTra ...
- RocketMQ之消息幂等
幂等(idempotent.idempotence)是一个数学与计算机学概念,常见于抽象代数中. 在编程中一个幂等操作的特点是其任意多次执行所产生的影响均与一次执行的影响相同. 首先我们了解一下什么是 ...
- rocketmq总结(消息的顺序、重复、事务、消费模式)
rocketmq总结(消息的顺序.重复.事务.消费模式) 参考: http://www.cnblogs.com/wxd0108/p/6038543.html https://www.cnblogs.c ...
- rocketmq,zookeeper,redis分别持久化的方式
1.rocketmq持久化: RocketMQ 的所有消息都是持久化的, 先写入系统 PAGECACHE, 然后刷盘, 可以保证内存与磁盘都有一份数据,访问时,直接从内存读取. RocketMQ 的所 ...
- rocketmq事务消息入门介绍
说明 周五的时候发了篇:Rocketmq4.3支持事务啦!!!,趁着周末的时候把相关内容看了下,下面的主要内容就是关于RocketMQ事务相关内容介绍了. 说明: 今天这篇仅仅是入门介绍,并没有涉及到 ...
随机推荐
- 解析binlog生成MySQL回滚脚本
如果数据库误操作想恢复数据.可以试试下面这个脚本.前提是执行DML操作. #!/bin/env python #coding:utf-8 #Author: Hogan #Descript : 解析bi ...
- 在postman中请求的接口有csrf怎么办
今天在写项目的时候,写了一个post接口,为了防止crsf攻击,config.defalut.js文件中加了如下代码: exports.security = { csrf: { ignoreJSON: ...
- ArrayList的contains()和HashSet的contains()效率比较
ArrayList的contains(Object o)方法内部只有一行代码:判断indexOf(0)是否大于等于0.而indexOf(o)内部会从头遍历数组,直到某位置的元素等于o,极端情况下,要把 ...
- mac安装指定版本的openjdk
先安装SDKMAN,教程在https://sdkman.io/install 然后 sdk list java sdk install java 11.0.5.hs-adpt
- 以Emacs Org mode为核心的任务管理方案
前言 如今用于任务管理的方法与工具越来越多,如纸笔系统.日历与任务列表.Emacs Org mode系统,以及移动设备上的诸多应用.这些解决方案各具特色,在一定程度上能够形成互补作用.但是,它们彼此之 ...
- 【C++】关键字struct
网址连接 https://www.cnblogs.com/zhengfa-af/p/8144786.html 主要内容: 1. C语言中,结构体的3中不同声明和定义方式: 2. struct在C和C+ ...
- BZOJ 1069 Luogu P4166 最大土地面积 (凸包)
题目链接: (bzoj)https://www.lydsy.com/JudgeOnline/problem.php?id=1069 (luogu)https://www.luogu.org/probl ...
- 5.Python注释(多行注释和单行注释)用法详解
Python单行注释 Python 中使用井号(‘#’)作为单行注释的符号,语法格式为: # 注释内容 也就是说,从符号‘#’处开始,直到换行处结束,此部分内容都作为注释的内容,当程序执行时,这部分内 ...
- Oracle-SYSTEM表空间突然持续爆满
一般情况下,我们建完数据库后,都会给数据库指定一个新的默认表空间,不然会占用数据库系统表空间资源,导致数据库性能下降. 我们可以同过SQL语句找出改表空间占用空间前10的对象 SELECT * FRO ...
- architecture 20190628
https://abp.io/ --ABP v2 官网 https://grpc.io/ --gRPC官网 https://devblogs.microsoft.com/dotnet/introdu ...