RocketMQ

RocketMQ

概念

中文文档;这篇笔记主要内容都来源于这个文档

RocketMQ主要由producer(生产消息)、broker(存储消息,一般对应一台服务器)、consumer(消费消息)三部分组成;每个broker可以存储多个topic的消息,每个topic消息也可以分片存储在不同的broker;消息队列(message queue)是用于存储消息的物理地址,每个topic中的消息存储于多个message queue中。consumer group(消费者组)由多个consumer构成;其主要构成部分如下:

  • 生产者(producer):生产消息,把消息发送到broker服务器,主要的发送方式有:同步、异步、顺序、单向等四种发送方式,其中同步和异步需要broker返回确认,单向则不需要;
  • 消费者(consumer):从broker拉取消息,rocketmq提供两种消费方式:
    • 拉取式消费:应用主动调用拉取消息的方法拉取消息;
    • 推动式消费:broker收到消息后主动推送给消费端(实际就是对拉取式的封装,即拉取后定时继续从broker拉取消息);
  • 主题(topic):一类消息的集合,每个消息只能属于一个主题,是消息订阅的基本单位;
  • broker server:负责查询、存储、转发消息以及服务高可用,也保存消息相关的原数据(如:消费者组、消费进度偏移、主题、消息队列等),它包括五个重要的子模块:
    • remoting module:处理来自客户端的请求;
    • client manager:管理客户端和维护消费者的topic订阅信息;
    • store service:提供api接口用于消息的存储和查询;
    • HA service:高可用服务,提供master和slave之间的数据同步功能;
    • index service:根据特定的key对消息进行索引,以便更快的查询;
  • name server:topic路由注册中心,一般是集群部署,多个name server之间互不通讯;
    • 管理broker,接受broker集群的注册信息,提供心跳检测;
    • 路由信息管理:保存broker的路由信息,用于客户端查询队列信息,即客户端通过name server查找topic对应的broker;
  • 生产者组(producer group):发送同一类消息且发送逻辑一致的一组生产者;
  • 消费者组(consumer group):消费同一类消息且消费逻辑一致的一组消费者;
  • 集群消费(Clustering):相同消费者组的每个消费者平均分摊消息;
  • 广播消息(broadcasting):相同消费者组的每个消费者都接受全量消息;
  • 普通顺序消费(normal ordered message):消费者通过同一消息队列收到的消息是有序的;
  • 严格顺序消费(strictly ordered message):消费者收到的所有消息均是有序的;
  • 消息(message):传输信息的载体,生产消费的最小单位,每条消息必属于一个主题,每个消息拥有唯一的messageID;
  • 标签(tag):用于区分同一主题下不同类型的消息;消费者可以根据不同的tag实现不同的消费逻辑;

安装

dockerhub镜像地址

使用方法:

  • 下载:docker pull foxiswho/rocketmq:4.8.0
  • 启动server
docker run -d -v $(pwd)/logs:/home/rocketmq/logs --name rmqnamesrv --net net1 --net-alias rmqserver -e "JAVA_OPT_EXT=-Xms512M -Xmx512M -Xmn128m" -p 9876:9876 foxiswho/rocketmq:4.8.0 sh mqnamesrv
  • 启动broker
docker run -d -v $(pwd)/logs:/home/rocketmq/logs -v $(pwd)/store:/home/rocketmq/store -v $(pwd)/conf:/home/rocketmq/conf --name rmqbroker --net net1 --net-alias rmqbroker -e "NAMESRV_ADDR=rmqserver:9876" -e "JAVA_OPT_EXT=-Xms512M -Xmx512M -Xmn128m" -p 10911:10911 -p 10912:10912 -p 10909:10909 foxiswho/rocketmq:4.8.0 sh mqbroker -c /home/rocketmq/rocketmq-4.8.0/conf/broker.conf
  • 下载console:docker pull styletang/rocketmq-console-ng
  • 启动console:
docker run --name rmqconsole --net net1 --net-alias rmqconsole --link rmqserver:rmqserver -e "JAVA_OPTS=-Drocketmq.namesrv.addr=rmqserver:9876 -Dcom.rocketmq.sendMessageWithVIPChannel=false" -p 8010:8080 -d styletang/rocketmq-console-ng
  • 访问:localhost:8010

我这里使用单机部署测试,启动rocketmq的命令可以用这个:

# 1.启动nameServer,绑定相关端口(net1是自建的网卡);2.修改broker配置(ip、端口、允许创建topic、允许使用bysql过滤消息);3.启动broker
docker run --name rocketmq --net net1 --net-alias rocketmq -p 9876:9876 -p 9911:9911 -p 9909:9909 -p 9912:9912 -e "JAVA_OPT_EXT=-Xms256M -Xmx256M -Xmn128m" -e "NAMESRV_ADDR=rocketmq:9876" -d foxiswho/rocketmq:4.8.0 sh mqnamesrv \
&& docker exec -d rocketmq sed -i '$a listenPort=9911\nbrokerIP1=192.168.56.102\nautoCreateTopicEnable=true\nenablePropertyFilter=true\nmapedFileSizeCommitLog=1024*1024*10' /home/rocketmq/rocketmq-4.8.0/conf/broker.conf \
&& docker exec -d rocketmq sh mqbroker -c /home/rocketmq/rocketmq-4.8.0/conf/broker.conf

docker run --name rmqconsole --net net1 --net-alias rmqconsole --link rocketmq:rocketmq -e "JAVA_OPTS=-Drocketmq.namesrv.addr=rocketmq:9876 -Dcom.rocketmq.sendMessageWithVIPChannel=false" -p 8010:8080 -d styletang/rocketmq-console-ng

特性

订阅与发布:发布是指某个生产者向某个topic发送消息,订阅是指某个消费者关注了某个topic中带有某些tag的消息,进而消费数据;

消息顺序:指一类消息消费时需要按照发送的顺序来消费,其由分为:

  • 全局顺序:对于指定的topic所有消息严格按照先入先出的顺序发布和消费;
  • 分区顺序:所有消息根据sharding key进行区块分区,同一分区内的消息严格按照FIFO的顺序发布和消费;

消息过滤:broker支持根据tag或者自定义属性进行消息过滤;这样减少了无用消息的网络传输,但也增加了broker的负担,并且实现复杂;

可靠性:支持消息高可靠,(但设备损坏还是会导致单点故障,对此,如有必要,可以使用同步双写技术,但是这样会影响性能)

至少一次:消息至少投递一次,即需要收到消费者的ack,才算消费完成;

回溯消费:指因业务需要,消费过的消息需要重新消费;rocketmq支持按照时间维度(精确到毫秒)回溯消费

定时消息:即延迟队列,rocketMQ可以通过配置broker的messageDelayLevel属性实现延时,其默认值有:1s 5s 10s 30s 1m 2m 3m 4m 5m 6m 7m 8m 9m 10m 20m 30m 1h 2h,18个level,level==0代表非延迟,1~18代表对应的延时时间,大于18为2h;定时消息实际是根据延时级别存在名为SCHEDULE_TOPIC_XXXX的topic中,这保证了相同延迟的消息能够顺序消费,broker会调度地消费延时队列的消息,将其写入真实的topic;

消息重试:rocketmq处理重试消息的方式是先保存至上面提到的延迟队列中,然后重新保存至一个topic名为%RETRY%+consumerGroup的重试队列(这个topic是针对消费者组的),重试次数越多,对应的延时队列级别越高,延时也就越大;对于消息消费失败一般有两种情况:

  • 消息本身问题:即数据本身无法处理,跳过这条消息就好了,这种一般就使用mq的重试机制;
  • 下游应用有问题:即应用本身现在无法处理消息,也就是跳过这条消息,其他消息同样无法处理,这种情况最好是将应用休眠一段时间,以减轻broker的重试压力;

消息重投:一般情况下,消息重投是小概率事件,但是当消息量大、网络抖动等情况下,则是大概率事件;rocketMQ支持以下重试策略:

  • retryTimesWhenSendFailed:同步发送失败重投次数,默认2,每次失败会尝试向其他broker发送,超过重试次数则抛出异常;(出现RemotingExceptionMQClientException和部分MQBrokerException异常时会重投);
  • retryTimesWhenSendAsyncFailed:异步发送失败重试次数,异步重试不会选择其他broker,仅在同一个broker上重试;
  • retryAnotherBrokerWhenNotStoreOK:消息刷盘超时或slave不可用,是否尝试发送到其他broker,默认false,重要的消息可以开启;

流量控制

  • 生产者流控:
    • commitLog文件被锁时间超过osPageCacheBusyTimeOutMills时,返回流控,默认1000ms;
    • transientStorePoolEnable == true且broker为异步刷盘的主机,且transientStorePool资源不足则拒绝当前send,返回流控;
    • broker每隔10ms检查send请求队列头部请求的等待时间,超过waitTimeMillsInSendQueue返回流控,默认200ms;
  • 消费者流控:
    • 本地缓存消息数量超过pullThresholdForQueue时,默认1000;
    • 本地缓存消息大小超过pullThresholdSizeForQueue时,默认100MB;
    • 本地缓存消息跨度超过consumeConcurrentlyMaxSpan时,默认2000;

死信队列:用于处理无法被正常消费的消息(达到最大重试次数的消息);死信队列中的消息可以通过console控制台来进行重发;

设计

消息存储

rocketmq中主要由三个跟消息存储相关的文件:

  • commitLog:消息以及元数据存储的主体,大小默认1G,文件名20位,左边补0,剩余为起始偏移量,即下一个文件的文件名是上一个文件的文件大小+偏移量;
  • consumeQueue:消费队列,保存了指定topic下的队列消息在commitLog文件中的起始物理偏移量offset(8byte)、消息大小(4byte)和tag的哈希值(8byte),可以看作基于topic的commitLog的索引文件,可以提高消息消费的性能;这个文件就在$HOME/store/consumequeue/{topic}/{queueId}/{fileName},单个文件由30W个条目组成,可以像数组一样随机访问;
  • IndexFile:索引文件,可以通过key或时间区间来查询消息,其实现就是设计在文件系统中的hashmap结构(key为4byte哈希+8byte偏移量+4byte时间戳+4byte下一个index的偏移量,这个用于解决哈希冲突),文件大小固定;

在rocketmq中,consumeQueue队列存储的数据较少,并且都是顺序读取,在pagecache的机制下,其性能接近于内存,而对于commitLog文件则随机访问较多一些,需要选择合适的调度算法;另外也使用mappedByteBuffer对文件进行读写(利用NIO中的FileChannel模型,这也是rocketmq的文件采用定长存储的原因)

消息刷盘

  • 同步刷盘:消息要真正持久化到磁盘后才返回ack。该方式可靠性高,但影响性能,一般适用于金融业务;
  • 异步刷盘:只要消息写入pageCache即可返回ack,消息刷盘则采用后台异步线程提交的方式进行,这降低了读写延迟,提高了性能和吞吐量;

通信机制

通讯流程:

  • broker启动后将自己注册到nameServer,随后每隔30s定时上报topic路由信息;
  • 生产者发送消息时,先根据topic从本地缓存的topicPublishInfoTable中获取路由信息,如果没有则从nameServer更新路由信息,同时每30s从nameServer拉取一次;
  • 生产者根据路由信息选择一个队列发送消息,broker接收消息并落盘存储;
  • 消费者也从nameServer中获取路由信息,并在客户端负载均衡后选择一个或者几个消息队列拉取消息;

过滤与负载均衡

消息过滤

rocketMQ自消息过滤是在消费端处理的,其具体消费过程是通过consumeQueue拿到消息索引,然后从commitLog读取真正的消息;rocketMQ主要支持两种过滤方式:

  • tag过滤(如果有多个tag,可以用||分割):broker从consumeQueue读取记录时,会利用tag的hash值做过滤,然后消费端在拉取到消息后会与原始tag字符串做对比,过滤消息;
  • SQL92过滤:

负载均衡

负载均衡主要是在客户端完成的;

  • 生产端的负载均衡:发送消息时,会先根据topic找到指定的TopicPublishInfo路由信息,从其中选择一个队列发送消息;(如果关闭sendLatencyFaultEnable,则采用递增取模的方式选择,若开启,则在递增取模的基础上过滤掉不可达的broker,即按上次请求超时时间确定多少时间内不再选择这个broker)
  • 消费端的负载均衡:一个消息消费队列同一时间只允许被同一个消费者组内的一个消费者消费,一个消息消费者能同时消费多个消息队列

过程:

  • 心跳检测:定时向集群中所有broker发送心跳检测(包含消费分组名、订阅关系、通讯模式、客户端id等信息),broker会将这些信息维护在consumerManager的本地缓存变量(consumerTable);同时将封装后的客户端通讯信息保存在本地缓存变量channelInfoTable中;
  • 消费端启动时,会完成负载均衡服务线程的启动(20s一次),其会根据消费者通讯类型(广播模式和集群模式)做不同的处理逻辑;这里主要看集群模式;
  • 从本地缓存变量中获取该topic下的消息消费队列集合;
  • 向broker端发送获取该消费者组下消费id列表的请求;
  • 对topic下的消息消费队列、消费者id排序,然后用分配策略算法(默认平均分配,即按排序结果将消息平均分配给各个消费者)计算出待拉取的消息队列;
  • 将分配到但是消息队列集合与processQueueTable做过滤比对;将过滤后的消息队列集合创建为processQueue放入processQueueTable,然后调用相关方法获取消息和下一个进度消费值offset,并将offset放入下一个请求中,并添加到请求列表(后续都是调用这里的方法获取消息,这里有点没太看懂);

事务消息

在4.3.0版本开始支持分布式事务消息,其采用2PC的思想实现,同时增加了一个补偿逻辑来处理二阶段超时或者失败的消息;

事务消息发送及提交流程:

  • 发送消息(half消息,该消息会存入名为RMQ_SYS_TRANS_HALF_TOPIC的topic,由于消费者并没有订阅这个topic,所以不会消费);
  • 服务端响应消息,写入结果;
  • 根据发送结果执行本地事务(发送失败则不执行);
  • 根据本地事务执行状态提交或者回滚事务(MQ会定时从RMQ_SYS_TRANS_HALF_TOPIC拉取消息,并根据生产者提交的状态提交或回滚消息);
    • 回滚:用Op消息标识消息为一个已经确定的状态;
    • 提交:在Op消息创建前将消息的topic和queue替换为真正的目标topic和queue;

补偿流程:

  • 对没有提交或回滚的事务消息,服务端会发起一次回查(默认15次,超过则默认回滚消息);
  • 生产者收到回查消息,回检查消息对应的本地事务状态;
  • 根据本地事务状态重新提交或者回滚;

消息查询

根据messageId查询:messageId长16字节,包含了主机地址、offset;具体查询过程是客户端根据messageId解析出broker地址和commitLog的偏移量然后封装到请求中并发送给broker,broker根据其中的offset和size读取消息并返回;

根据messageKey查询:该方式是基于IndexFile索引文件实现的,即利用topic和key找到索引文件中的一条记录,然后根据索引数据读取真实消息;

broker.conf

# 集群名称
brokerClusterName = DefaultCluster
# 节点名称
brokerName = broker-a
# 0代表master,正整数代表slave
brokerId = 0
# 什么时间删除超过预留时间的commitLog
deleteWhen = 04
# commitLog保留的小时数
fileReservedTime = 48
# SYNC_MASTER/ASYNC_MASTER/SLAVE
brokerRole = ASYNC_MASTER
# SYNC_FLUSH/ASYNC_FLUSH
flushDiskType = ASYNC_FLUSH
# 端口,修改这个端口会关联的修改10909、10912两个端口,其值为这个端口对应的值-2和+1
listenPort=10911
# 一般要配置为客户端可以访问的ip
brokerIP1=47.106.153.98
# 允许broker创建topic,生产环境下不应该开启
autoCreateTopicEnable=true
# commitlog文件映射到内存的大小,默认1G
mapedFileSizeCommitLog=1024*1024*1024
# 允许使用bysql过滤
enablePropertyFilter=true
# name server服务器地址及端口,可以是多个,分号隔开
namesrvAddr=192.168.56.102:9876

使用示例

创建生产者

private String nameServer = "192.168.56.102:9876";
private String topic = "testTopic01";
/**
 * 普通生产者
 */
@Bean
public DefaultMQProducer producer() throws MQClientException {
    DefaultMQProducer producer = new DefaultMQProducer("producerGroup01");
    // 设置NameServer的地址
    producer.setNamesrvAddr(nameServer);
    // 启动Producer实例
    producer.start();
    // 启动时创建主题
    producer.createTopic("TBW102", topic, 8, 0);
    return producer;
}
/**
 * 事务消息生产者
 */
@Bean
public TransactionMQProducer transProducer() throws MQClientException {
    TransactionMQProducer producer = new TransactionMQProducer("producerGroup02");
    producer.setNamesrvAddr(nameServer);
    producer.start();
    return producer;
}

创建消费者

/**
 * 普通消费者
 */
@Bean
public DefaultMQPushConsumer filterConsumer() throws Exception {
    DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("consumerGroup03");
    consumer.setNamesrvAddr(nameServer);
    consumer.subscribe(topic, "*");
    /// 订阅指定tag的消息多个可以用 || 分割
    /// consumer.subscribe(topic, "tagA");
    /// 使用sql表达式过滤消息,可用的属性都是来源于生产者设置的:msg.putUserProperty(k, v)
    /// consumer.subscribe(topic, MessageSelector.bySql("a = 1"));
    consumer.registerMessageListener((MessageListenerConcurrently) (msgs, context) -> {
        System.out.printf("过滤消费者%s收到消息: %s %n", Thread.currentThread().getName(), msgs);
        return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
    });
    // 启动消费者实例
    consumer.start();
    return consumer;
}
/**
 * 顺序消费者
 */
@Bean
public DefaultMQPushConsumer sortConsumer() throws Exception {
    DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("consumerGroup02");
    consumer.setNamesrvAddr(nameServer);
    // 设置第一次启动时从头开始消费
    consumer.setConsumeFromWhere(ConsumeFromWhere.CONSUME_FROM_FIRST_OFFSET);
    consumer.subscribe(topic, "*");
    consumer.registerMessageListener(new MessageListenerOrderly() {
        @Override
        public ConsumeOrderlyStatus consumeMessage(List<MessageExt> msgs, ConsumeOrderlyContext context) {
            context.setAutoCommit(true);
            for (MessageExt msg : msgs) {
                // 可以看到每个queue有唯一的consume线程来消费, 消息对每个queue(分区)有序
                System.out.printf("顺序消费者%s从对队列 %s 收到消息: %s %n", Thread.currentThread().getName(), msg.getQueueId(), new String(msg.getBody()));
            }
            return ConsumeOrderlyStatus.SUCCESS;
        }
    });
    consumer.start();
    return consumer;
}

发送同步消息

@Autowired
private DefaultMQProducer producer;
@Autowired
private TransactionMQProducer transProducer;

private String topic = "testTopic01";
private String tags = "TagA";

@GetMapping("/send")
@ApiOperation("发送同步消息")
public R<SendResult> send(String body) throws Exception {
    // 创建消息,并指定Topic,Tag和消息体
    Message msg = new Message(topic, tags, body.getBytes(RemotingHelper.DEFAULT_CHARSET));
    // 发送消息到一个Broker
    SendResult sendResult = producer.send(msg);
    // 通过sendResult返回消息是否成功送达
    return R.success(sendResult);
}

发送异步消息

@GetMapping("/sendAsync")
@ApiOperation("发送异步消息")
public R sendAsync(String body) throws Exception {
    Message msg = new Message(topic, tags, body.getBytes(RemotingHelper.DEFAULT_CHARSET));
    // SendCallback接收异步返回结果的回调
    producer.send(msg, new SendCallback() {
        @Override
        public void onSuccess(SendResult sendResult) {
            System.out.printf("异步发送消息:%s %n", sendResult);
        }

        @Override
        public void onException(Throwable e) {
            System.out.println("出错了。。。。");
            e.printStackTrace();
        }
    });
    return R.success();
}

发送单向消息

@GetMapping("/sendOneWay")
@ApiOperation("发送单向消息")
public R sendOneWay(String body) throws Exception {
    Message msg = new Message(topic, tags, body.getBytes(RemotingHelper.DEFAULT_CHARSET));
    // 发送单向消息,没有任何返回结果
    producer.sendOneway(msg);
    return R.success();
}

发送顺序消息

@GetMapping("/sortSend")
@ApiOperation("发送顺序消息")
public R<List<SendResult>> sortSend(String body) throws Exception {
    // 生成10条消息,分成奇数偶数两组,组内都按顺序发送
    int num = 10;
    List<SendResult> sendResults = new ArrayList<>(num);
    for (int i = 1; i <= num; i++) {
        Message msg = new Message(topic, tags, (body + "_" + i).getBytes());
        sendResults.add(producer.send(msg, new MessageQueueSelector() {
            @Override
            public MessageQueue select(List<MessageQueue> mqs, Message msg, Object arg) {
                // 从这里可以看出分组数量不能超过mqs的大小
                return mqs.get((int) arg % 2);
            }
        }, i));
    }
    return R.success(sendResults);
}

发送延时消息

@GetMapping("/delaySend")
@ApiOperation("发送延时消息(rocketMQ只支持固定的几个时间,详情看:delayTimeLevel)")
public R<SendResult> delaySend(String body) throws Exception {
    Message message = new Message(topic, body.getBytes());
    // 设置延时等级3, 即10s之后发送
    message.setDelayTimeLevel(3);
    SendResult sendResult = producer.send(message);
    return R.success(sendResult);
}

批量发送消息

@GetMapping("/batchSend")
@ApiOperation("批量发送消息(单次最多发送4M的消息,超过这个大小需要分割消息)")
public R<List<SendResult>> batchSend(String body) throws Exception {
    int num = 10;
    List<Message> msgs = new ArrayList<>(num);
    for (int i = 1; i <= num; i++) {
        msgs.add(new Message(topic, tags, (body + "_" + i).getBytes()));
    }
    // 将消息按4M的大小分割(如果消息大小不超过4M就不需要这一步)
    ListSplitterBySize<Message> msgIterator = new ListSplitterBySize<Message>(msgs, 4 * 1024 * 1024) {
        @Override
        protected int calcSize(Message message) {
            // 计算一条消息的大小(topic + body + properties + log)
            int tmpSize = message.getTopic().length() + message.getBody().length;
            Map<String, String> properties = message.getProperties();
            for (Map.Entry<String, String> entry : properties.entrySet()) {
                tmpSize += entry.getKey().length() + entry.getValue().length();
            }
            // 增加⽇日志的开销20字节
            tmpSize = tmpSize + 20;
            System.out.println(tmpSize);
            return tmpSize;
        }
    };
    List<SendResult> results = new ArrayList<>();
    while (msgIterator.hasNext()) {
        results.add(producer.send(msgIterator.next()));
    }
    return R.success(results);
}

一个按大小切分list的工具类

public abstract class ListSplitterBySize<T> implements Iterator<List<T>> {

    private List<T> list;
    private int size;
    private int currIndex = 0;

    protected ListSplitterBySize(List<T> list, int size) {
        this.list = list;
        this.size = size;
    }

    @Override
    public boolean hasNext() {
        return currIndex < list.size();
    }

    @Override
    public List<T> next() {
        int startIndex = this.currIndex;
        int tmpSize = 0;
        do {
            tmpSize += calcSize(this.list.get(this.currIndex));
            this.currIndex++;
        } while (tmpSize < size && this.currIndex < list.size());
        if (tmpSize > size) {
            this.currIndex--;
        }
        if (startIndex == this.currIndex) {
            throw new ServiceException("无法分割list,因为单个对象的大小就已经超过限制的大小了");
        }
        return this.list.subList(startIndex, this.currIndex);
    }

    /**
     * 计算泛型大小
     */
    protected abstract int calcSize(T t);

}

带过滤属性的消息

@GetMapping("/filterSend")
@ApiOperation("发送过滤消息")
public R<SendResult> filterSend(String body) throws Exception {
    Message message = new Message(topic, tags, body.getBytes());
    // 设置过滤属性
    message.putUserProperty("a", "1");
    SendResult sendResult = producer.send(message);
    return R.success(sendResult);
}

发送事务消息

@GetMapping("/transSend")
@ApiOperation("发送事务消息")
public R<SendResult> transSend(String body) throws Exception {
    transProducer.setExecutorService(Executors.newFixedThreadPool(4));
    transProducer.setTransactionListener(new TransactionListenerImpl());
    Message msg = new Message(topic, tags, body.getBytes(RemotingHelper.DEFAULT_CHARSET));
    SendResult sendResult = transProducer.sendMessageInTransaction(msg, null);
    return R.success(sendResult);
}

public static class TransactionListenerImpl implements TransactionListener {
    private ConcurrentHashMap<String, LocalTransactionState> localTrans = new ConcurrentHashMap<>();

    @Override
    public LocalTransactionState executeLocalTransaction(Message msg, Object arg) {
        // 这里执行本地事务(这个方法执行前,消息就已经发送到RMQ_SYS_TRANS_HALF_TOPIC的topic了)
        System.out.println("本地事务执行成功");
        localTrans.put(msg.getTransactionId(), LocalTransactionState.COMMIT_MESSAGE);
        // 这里其实就可以返回成功,只是我这里为了测试检查事务转态而返回未知
        return LocalTransactionState.UNKNOW;
    }

    @Override
    public LocalTransactionState checkLocalTransaction(MessageExt msg) {
        System.out.println("检查事务状态");
        LocalTransactionState state = localTrans.get(msg.getTransactionId());
        return state == null ? LocalTransactionState.UNKNOW : state;
    }
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值