消息中间件选型和关于RocketMQ的事务处理
文章目录
市面上比较流行的消息中间件以及如何选型
关于消息中间件的几个思考
为什么需要消息中间件
消息队列之所以作为高并发系统下面核心组件之一,主要具有削峰填谷、异步操作、系统解耦、提高性能、蓄流压测等优势。
消息中间件有哪些缺点
- 系统的复杂性提高
- 系统的健壮性降低,需要考虑中间件宕机带来的影响
如何保证消息队列的高可用
通过集群来保证,Master、Slave等
如何保证消息的不重复消费
消息在生产和消费的过程中增加幂等性的保证
如何保证消息的不丢失
生产过程丢失:同步确认模式
消息中间件存储丢失:及时放入持久化的队列
如何保证消息的顺序性
通过算法放入同一个消息队列
目前市面上比较常见的消息中间件有RocketMq、Kafka、RabbitMq。
我们先来介绍一下三种中间件的优缺点:
消息中间件 | 优点 | 缺点 |
---|---|---|
kafak | 1. 高吞吐量,单机可以抗住十几万的QPS 2. 性能高,发送消息毫秒级 3. 高可用,支持集群部署,部分宕机不影响使用 | 1. 因为消息不是直接写入磁盘,而是写入磁盘缓冲区,所以可能会造成数据的丢失 2. 功能单一,只支持订阅-发布 |
RabbitMQ | 1. 保证消息不丢失 2. 高可用,部分宕机不影响使用 3. 功能丰富,消息重试、死信队列 | 1. 吞吐量低,大概每秒几万QPS 2. 开发语言是erlang,很难进行改造 |
RocketMQ | 1. 吞吐量高,单机器每秒十几万QPS 2. 高可用、高性能 3. 保证数据不丢失 4. 支持大规模集群 5. 功能丰富,如延迟消息、消息回溯 6. java开发,易改造 | 1. 社区活跃度一般 2. 在MQ核心中没有实现JMS接口,因此对某些系统迁移需要修改大量代码 |
选型建议:
首先我们可以看一下社区的活跃度 Kafka > RocketMQ(阿里团队) > RabbitMQ > ActiveMQ
kafka: 主要特点是消费消息,高吞吐量。所以如果我们对高吞吐量、高性能有要求的话,可以考虑kafka,但是kafka功能比较单一。综合来看,kafka目前很适合对于大量日志的收集。
RabbitMQ: 社区活跃度高,较为稳定,不利于开发和维护,适合数据量小,稳定的小公司选型。
RocketMQ: 业务中如果存在高并发的场景或者需要符合很多的业务场景,建议选择RocketMQ,毕竟经历过双十一的磨砺。
RocketMQ
RocketMQ的官网地址
功能特性:
- 订阅-发布
- 消息顺序: 按照发送的顺序来消费,例如订单的结算。顺序消息也分为全局顺序消息和分区顺序消息,全局顺序是指某个Topic下的所有消息都要保证顺序;部分顺序消息只要保证每一组消息被顺序消费即可。
- 消息过滤: 消费者根据tag对于消息效率,减少无用的网络传输
- 消息可靠性
- 至少一次: 每个消息至少发送一次。消费者在消费之后手动提交ack消息
- 回溯消息: 消息在消费之后会保存一段时间,可以重新消费
- 事务消息(*): 通过事务消息达到分布式事务的最终一致
- 定时消息: 消息发送到broker之后,不会被立即消费,会在特定的时候投递给指定的topic。比如电商里,提交了一个订单就可以发送一个延时消息,1h后去检查这个订单的状态,如果还是未付款就取消订单释放库存。
- 消息重试: Rocket为每个消费者组设置一个重试队列
- 消息重投: 在消息量大,网络抖动、消息重复的情况下,消息重投会导致消息重复的问题
- 流量控制
- 死信队列: 在消息重试之后达到重试的最大次数之后,会被放入另外一个队列被存储。
架构
BrokerServer: 主要负责处理来自客户端的请求,维护topic信息等功能。Broker分为Master和Slave,通过制定小童的BrokerName,不同的BrokerId来定义。BrokerId=0为Master,非0位Slave。目前只有BrokerID=1的从服务器才会参与消息的读负载
NameServer: 是一个topic的路由中心,支持broker的动态注册和发现;路由信息动态管理,每个NameServer上面保存了一个完整的路由信息
Producer: 消息发布角色
Consumer: 消息消费的角色,支持push推、pull拉两种方式进行消费。定期从NameServer中拉去路由信息,并和Broker的Master、Slave建立长连接。
工作流程:
- 启动NameServer,等待broker、producer、consumer连接上来,相当于注册中心
- Broker启动,与NameServer建立连接,并发送心跳包(包括broker的信息和topic的信息)
- Producer发送消息时通过NameServer查看topic在哪一台broker上面
- Consumer消费消息
消息事务
主要分为以下几个步骤:
- 生产者发送办消息到MQ Server,暂时不能被投递和消费
- MQ Server返回成功接收
- 生产者执行本地事务
- 生产者向MQ Server发送提交还是回滚
- 如果MQ Server没有收到反馈,则主动询问生产者
- 生产者检查本地事务状态
- 如果提交,Consumer消费
- 如果回滚,Consumer不消费
生产者:
@RequestMapping("sendTransaction")
public Object sendTransaction(@RequestParam(value = "msg",required = false)String msg){
String transactionId = UUID.randomUUID().toString();
org.springframework.messaging.Message<String> message = MessageBuilder.withPayload(msg)
.setHeader(RocketMQHeaders.TRANSACTION_ID, transactionId).build();
//发送半消息
TransactionSendResult sendResult = rocketMQTemplate.sendMessageInTransaction("topic03",message,transactionId);
//执行本地事务
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
return sendResult;
}
//监听发送
@RocketMQTransactionListener
public class MQLocalListener implements RocketMQLocalTransactionListener {
/**
* 用于执行本地事务
* @param msg
* @param arg
* @return
*/
@Override
public RocketMQLocalTransactionState executeLocalTransaction(Message msg, Object arg) {
MessageHeaders headers = msg.getHeaders();
String transactionId = (String) headers.get(RocketMQHeaders.TRANSACTION_ID);
log.info("[执行本地事务]消息体参数: transactionId={}", transactionId);
//执行本地方法
try {
Thread.sleep(2000);
String payload = new String((byte[]) msg.getPayload());
if ("zzz".equals(payload)){
log.info("[回滚本地事务]");
return RocketMQLocalTransactionState.ROLLBACK;
}
} catch (InterruptedException e) {
e.printStackTrace();
//异常,发送回滚信号
return RocketMQLocalTransactionState.ROLLBACK;
}
//正常,发送提交信号
return RocketMQLocalTransactionState.COMMIT;
}
/**
* 回查本地事务执行结果
* @param msg
* @return
*/
@Override
public RocketMQLocalTransactionState checkLocalTransaction(Message msg) {
MessageHeaders headers = msg.getHeaders();
String transactionId = headers.get(RocketMQHeaders.TRANSACTION_ID, String.class);
log.info("【回查本地事务】transactionId={}", transactionId);
//查询日志记录
// 如果存在则返回 RocketMQLocalTransactionState.COMMIT
// 如果失败则返回 RocketMQLocalTransactionState.ROLLBACK
return RocketMQLocalTransactionState.COMMIT;
}
}
消费者:
public static void main(String[] args) throws MQClientException {
int i=0;
DefaultMQPushConsumer consumer = new DefaultMQPushConsumer();
consumer.setNamesrvAddr("127.0.0.1:9876");
consumer.subscribe("topic03","*");
consumer.setConsumerGroup("topic03-group");
consumer.registerMessageListener(new MessageListenerConcurrently() {
@Override
public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> msgs, ConsumeConcurrentlyContext context) {
String msg = new String(msgs.get(0).getBody());
System.out.println("收到消息:"+msg);
// 标记该消息已经被成功消费
return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
}
});
consumer.start();
}
消费者消费失败如何处理?
- 正常消费,会返回 ConsumeConcurrentlyStatus.CONSUME_SUCCESS
- 异常消费,会触发重试机制 ConsumeConcurrentlyStatus.RECONSUME_LATER
如何进行重试?
重试的消息会进入一个"%RETRY%"+ConsumeGroup
的重试队列中
然后RocketMQ默认允许每条消息最多重试16次,每次重试的间隔时间与延迟消息的延迟级别是对应的。不过取的是延迟级别的后16级别。如果消息重试16次后仍然失败,消息将不再投递。转为进入死信队列。另外一条消息无论重试多少次,这些重试消息的MessageId始终都是一样的。
如何保证消息消费的幂等性?
首先,我们要了解消息幂有三种情况:
- at most once 最多一次:每条消息最多被消费一次,rocketMQ通过异步发送、sendOneWay等方式保证
- at least once 至少一次:每条消息至少消费一次,rocketMQ通过同步发送、事务消息等方式保证
- exactly once 刚好一次:每条消息只会消费一次,也是MQ中最难保证的一种。需要业务系统自行保证消息的幂等性,比如每条消息新建一个唯一的编号。
根据上面的事务消息的例子,我们可以讨论一下为什么rocket可以解决分布式事务的问题?
首先,我们需要了解为什么需要分布式事务。
有一个大家熟悉的例子,A账号向B账号转账100元,如果两个账号的操作在同一个事务中操作,要么一起成功,要么一起失败,我们就不用操心了。
但是存在一个问题,如果A银行账号和B银行账号不在同一个事务,那么就会存在3个问题。
- A扣除了100块,然后发送消息给B账号失败
- A先发送消息给B账号,然而A账号扣钱失败
- A消息和操作都成功了,B账号加钱失败
我们现在来看一下RocketMQ是怎么解决这个问题的:
RocketMQ采用的是二阶段提交,分为:准备阶段和确认阶段
准备阶段:将消息放在新的队列,消费者无法感知
确认阶段:Committe和RollBack
那么,现在就有几个动作可能会失败了,按照执行顺序来说: 发送准备消息、执行事务、发送确认消息,根据这几个动作失败分为3个异常
- 发送准备消息失败,不影响后面的操作
- 发送准备消息成功,执行事务失败,消费者无法感知消息,所以不影响
- 发送准备消息和执行事务成功,发送确认消息因网络故障失败,这个会影响到消息的消费
那么,Rocket为了解决异常3,采用如果没有收到确认消息,会回调生产者的方法来查看事务的状态。
其中,如果生产者没有问题,而消费者消费数据时出问题了,会进行重试,再超过重试次数之后交由人工处理。或者是消费数据出问题之后,业务上实现逻辑进行生产者数据的回滚。因为RocketMQ是保证最终的一致性,消费者的失败并不会回滚生产者。
如果喜欢博主,可以关注我的公众号哈