持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第6天,点击查看活动详情
背景
商城系统,一般订单的完成都必须需经过三个有序的步骤:订单创建、订单付款、订单生成。系统需产生了订单创建、订单付款、订单生成产生三条消息,消息消费需要按照上述顺序消费消息才能保证业务的正确性。
消息有序性分类
RocketMQ能严格保证消息的有序性,将顺序消息分为全局顺序消息与分区顺序消息。
- 全局顺序:对于指定的一个 Topic,所有消息按照严格的先入先出(FIFO)的顺序进行发布和消费。
- 分区顺序:对于指定的一个 Topic,所有消息根据hashKey进行区块分区。 同一个分区内的消息按照严格的 FIFO 顺序进行发布和消费。
RocketMQ的一个topic默认有4个消息队列,普通消息的发送,是通过轮询的方式选择队列,然后将消息发送出去的,消息之间是没有顺序的。要保证RocketMQ消息全局有序,那么一个topic有且只能一个消息队列,所有的消息都是严格按照先进先出的顺序进行发布和消费。要保证分区有序,将消息根据hashKey进行区分,然后将同一个分区的消息严格按照先进先出的顺序进行发布和消息,也就说将同一分区的消息发送到一个队列中去。
需要注意:发送全局顺序消息,性能比较差,只能用一个队列进行消息的发送。大多数场景都是保证消息分区顺序就行了。
如何保证消息的有序性
RocketMQ实现消息的有序性,需要从三个方面一起保证消息的有序性:
- 生产者生产顺序消息
- Broker保存顺序消息
- 消费者顺序消费消息
生产者生产顺序消息
Producer端确保消息顺序唯一要做的事情就是将消息路由到特定的分区,在RocketMQ中,通过MessageQueueSelector来实现分区的选择,具体的实现如下:
Broker保存顺序消息
只要生产者发送过来的消息是有序的,Broker按照生产者发送过来的消息保存起来就保证消息在Broker服务器的有序性,当消息保存成功以后,才给生产者发送ack确认,生产者才会发送下一条消息。即使消息重复发送,也是没有关系的,只要在消费者端做幂等,将重复的消息去重就行了。
消费者顺序消费消息
RocketMQ消费端有两种类型:MQPullConsumer和MQPushConsumer。
-
MQPullConsumer由用户控制线程,主动从服务端获取消息,每次获取到的是一个MessageQueue中的消息。PullResult中的List和存储顺序一致,只需要自己保证消费的顺序。
-
MQPushConsumer由用户注册MessageListener来消费消息,在客户端中需要保证调用MessageListener时消息的顺序性。
实现流程如下:
具体实现代码
生成者发送顺序消息
@Component
public class OrderProduce
{
private Logger logger = LoggerFactory.getLogger(getClass());
@Autowired
private RocketMQTemplate rocketMQTemplate;
public void sendOrderMessage(String hashKey)
{
logger.info("start sendOrderMessage hashKey:{}",hashKey);
String message="mesageid";
for(int i=0;i<5;i++)
{
rocketMQTemplate.syncSendOrderly("order-topic", message+i,hashKey);
}
}
}
说明:hashKey 通常可以采用订单OrderId或者商品的ProductId等。
消费者顺序消费
@Component
@RocketMQMessageListener(consumerGroup="test-orderrocketmq-group",topic="order-topic",consumeMode=ConsumeMode.ORDERLY)
public class OrderConsumer implements RocketMQListener<Object>
{
private Logger logger =LoggerFactory.getLogger(getClass());
@Override
public void onMessage(Object o)
{
String msg=JSON.toJSONString(o);
logger.info("send msg succss content is:{}", msg);
}
}
需要注意:RocketMQMessageListener的consumeMode属性默认为ConsumeMode.CONCURRENTLY,实现顺序消息需要将类型需改为ConsumeMode.ORDERLY。
测试示例代码
@RequestMapping("/sendOrderMsg")
public String sendOrderMsg()
{
String orderId="98456231";
orderProduce.sendOrderMessage(orderId);
return "success";
}
测试结果
从测试的结果来看,消费者是按照顺序进行消息消费。
总结
本文讲解RocketMQ发送顺序消息需要三个方面共同实现有序才能保证消息的有序性,我们需要结合相应的场景来选择合适的方式实现消息有序性。