概述
RocketMQ按照顺序消费有两种顺序级别,一种是普通顺序消息。另外一种是更完全严格顺序
- 普通顺序消息指的是Producer将消息发送到相对应的消息队列上面
- 完全严格顺序:在普通顺序消息的基础上,Consumer严格进行顺序消费
在绝大部分情况下只需要用到普通顺序消息,大部分的应用都能容忍短暂的乱序;官方文档给出的说明中表示目前只有数据库的Binlog同步会强依赖完全严格顺序(要保证数据库事务的ACID特性)。
Comsumer严格顺序消费
Consumer在进行严格顺序消费的时候,需要利用到三把锁,Broker消息队列锁——>Consumer消息队列锁——>Consumer消息处理队列消费锁,锁粒度越来越细
- Broker消息队列锁是一个分布式锁,在集群模式下需要,在广播模式下不需要,集群模式中,只有获得该锁才能对Broker中的消息队列进行消息拉取操作
- Consumer消息队列锁是一个Broker本地锁,只有Consumer获得该锁后才能操作消息队列
- Consumer
- 消息处理队列消费锁是消费者客户端的一个本地锁,只有Consumer获得该锁后才能对消息处理队列进行消息的消费。
获取分布式Broker消息队列锁【RebalanceImpl.java#updateProcessQueueTableInRebalance】
在集群分布式模式下,Broker会不断接收消息队列的更新请求,Broker就会通过更新操作检查这个消息队列是不是属于自己,具体就是通过锁定这个分布式锁实现的,如果锁获取成功,表示这个消息队列是属于自己的,允许进行消息拉取,如果获取锁失败,则不允许进行消息拉取。
Broker消息队列分布式锁默认30s会过期,因此Consumer需要不断刷新该锁的过期时间,默认20s就会刷新一次
1: // ⬇️⬇️⬇️【ConsumeMessageOrderlyService.java】
2: public void start() {
3: if (MessageModel.CLUSTERING.equals(ConsumeMessageOrderlyService.this.defaultMQPushConsumerImpl.messageModel())) {
4: this.scheduledExecutorService.scheduleAtFixedRate(new Runnable() {
5: @Override
6: public void run() {
7: ConsumeMessageOrderlyService.this.lockMQPeriodically();
8: }
9: }, 1000 * 1, ProcessQueue.REBALANCE_LOCK_INTERVAL, TimeUnit.MILLISECONDS);
10: }
11: }
移除消息队列(已有Consumer对这个消息队列消费了,移除它就是告诉Broker这个消息队列不需要其他的Consumer来消费)RebalancePushImpl.java#removeUnnecessaryMessageQueue
在集群模式下,为了避免其它Consuer在获取分布式锁时和消息队列的消费冲突,如果获取锁失败,进行移除消息队列将会失败,等到下次重新分配消费队列的时候,再进行移除;如果在没有获取分布式锁的情况下就进行消息队列移除,那么可能会导致当前Consumer和其他的Consumer同时消费该消息队列,这样将恶恶法保证消息按照完全严格顺序消费。
在解锁Broker消息队列锁的时候,如果消息队列存在剩下没被拉取的消息,则进行延迟解锁【RebanlancePushImpl.java#unlockDelay】Broker消息队列锁。因为这样能保证还没被拉取的消息能被全部拉取进行消费,这样才能保证消息被完全严格顺序消费。
消费消息队列
消费消息时序图
// ⬇️⬇️⬇️【ConsumeMessageOrderlyService.java】
2: class ConsumeRequest implements Runnable {
3:
4: /**
5: * 消息处理队列
6: */
7: private final ProcessQueue processQueue;
8: /**
9: * 消息队列
10: */
11: private final MessageQueue messageQueue;
12:
13: @Override
14: public void run() {
15: if (this.processQueue.isDropped()) {
16: log.warn("run, the message queue not be able to consume, because it's dropped. {}", this.messageQueue);
17: return;
18: }
19:
20: // 获得 Consumer 消息队列锁
21: final Object objLock = messageQueueLock.fetchLockObject(this.messageQueue);
22: synchronized (objLock) {
23: // (广播模式) 或者 (集群模式 && Broker消息队列锁有效)
24: if (MessageModel.BROADCASTING.equals(ConsumeMessageOrderlyService.this.defaultMQPushConsumerImpl.messageModel())
25: || (this.processQueue.isLocked() && !this.processQueue.isLockExpired())) {
26: final long beginTime = System.currentTimeMillis();
27: // 循环
28: for (boolean continueConsume = true; continueConsume; ) {
29: if (this.processQueue.isDropped()) {
30: log.warn("the message queue not be able to consume, because it's dropped. {}", this.messageQueue);
31: break;
32: }
33:
34: // 消息队列分布式锁未锁定,提交延迟获得锁并消费请求
35: if (MessageModel.CLUSTERING.equals(ConsumeMessageOrderlyService.this.defaultMQPushConsumerImpl.messageModel())
36: && !this.processQueue.isLocked()) {
37: log.warn("the message queue not locked, so consume later, {}", this.messageQueue);
38: ConsumeMessageOrderlyService.this.tryLockLaterAndReconsume(this.messageQueue, this.processQueue, 10);
39: break;
40: }
41: // 消息队列分布式锁已经过期,提交延迟获得锁并消费请求
42: if (MessageModel.CLUSTERING.equals(ConsumeMessageOrderlyService.this.defaultMQPushConsumerImpl.messageModel())
43: && this.processQueue.isLockExpired()) {
44: log.warn("the message queue lock expired, so consume later, {}", this.messageQueue);
45: ConsumeMessageOrderlyService.this.tryLockLaterAndReconsume(this.messageQueue, this.processQueue, 10);
46: break;
47: }
48:
49: // 当前周期消费时间超过连续时长,默认:60s,提交延迟消费请求。默认情况下,每消费1分钟休息10ms。
50: long interval = System.currentTimeMillis() - beginTime;
51: if (interval > MAX_TIME_CONSUME_CONTINUOUSLY) {
52: ConsumeMessageOrderlyService.this.submitConsumeRequestLater(processQueue, messageQueue, 10);
53: break;
54: }
55:
56: // 获取消费消息。此处和并发消息请求不同,并发消息请求已经带了消费哪些消息。
57: final int consumeBatchSize = ConsumeMessageOrderlyService.this.defaultMQPushConsumer.getConsumeMessageBatchMaxSize();
58: List<MessageExt> msgs = this.processQueue.takeMessags(consumeBatchSize);
59: if (!msgs.isEmpty()) {
60: final ConsumeOrderlyContext context = new ConsumeOrderlyContext(this.messageQueue);
61:
62: ConsumeOrderlyStatus status = null;
63:
64: // ....省略代码:Hook:before
65:
66: // 执行消费
67: long beginTimestamp = System.currentTimeMillis();
68: ConsumeReturnType returnType = ConsumeReturnType.SUCCESS;
69: boolean hasException = false;
70: try {
71: this.processQueue.getLockConsume().lock(); // 锁定队列消费锁
72:
73: if (this.processQueue.isDropped()) {
74: log.warn("consumeMessage, the message queue not be able to consume, because it's dropped. {}",
75: this.messageQueue);
76: break;
77: }
78:
79: status = messageListener.consumeMessage(Collections.unmodifiableList(msgs), context);
80: } catch (Throwable e) {
81: log.warn("consumeMessage exception: {} Group: {} Msgs: {} MQ: {}", //
82: RemotingHelper.exceptionSimpleDesc(e), //
83: ConsumeMessageOrderlyService.this.consumerGroup, //
84: msgs, //
85: messageQueue);
86: hasException = true;
87: } finally {
88: this.processQueue.getLockConsume().unlock(); // 锁定队列消费锁
89: }
90:
91: // ....省略代码:解析消费结果状态
92:
93: // ....省略代码:Hook:after
94:
95: ConsumeMessageOrderlyService.this.getConsumerStatsManager()
96: .incConsumeRT(ConsumeMessageOrderlyService.this.consumerGroup, messageQueue.getTopic(), consumeRT);
97:
98: // 处理消费结果
99: continueConsume = ConsumeMessageOrderlyService.this.processConsumeResult(msgs, status, context, this);
100: } else {
101: continueConsume = false;
102: }
103: }
104: } else {
105: if (this.processQueue.isDropped()) {
106: log.warn("the message queue not be able to consume, because it's dropped. {}", this.messageQueue);
107: return;
108: }
109:
110: ConsumeMessageOrderlyService.this.tryLockLaterAndReconsume(this.messageQueue, this.processQueue, 100);
111: }
112: }
113: }
114:
115: }
处理消费结果
顺序消费消息结果有四种情况(ConsumerOrderlyStatus)
- SUCCESS:消费成功但不提交
- ROLLBACK:消费失败,并进行消费回滚
- COMMIT:消费成功并提交
- SUSPEND_CURRENT_QUEUE_A_MOMENT:消费失败并挂起消费
ROLLBACK、COMMIT只有在事务消息下(Binlog)使用,被官方标记为@Deprecated
在并发场景中,如果消费失败,Consuner会将消f失败消息发回到Broker重试队列中。然后跳过当前消息,等到下次拉取该消息再进行消费。
不过消费失败的消息一直失败,也不可能一直消费。当超过消费重试上限时,Consumer 会将消费失败超过上限的消息发回到 Broker 死信队列。默认16次,最后会提交消费进度
在完全严格顺序消费中,如果消费失败,就挂起队列一会儿,稍后继续消费
消息处理队列核心方法
1: // ⬇️⬇️⬇️【ProcessQueue.java】
2: /**
3: * 消息映射
4: * key:消息队列位置
5: */
6: private final TreeMap<Long, MessageExt> msgTreeMap = new TreeMap<>(); /**
7: * 消息映射临时存储(消费中的消息)
8: */
9: private final TreeMap<Long, MessageExt> msgTreeMapTemp = new TreeMap<>();
10:
11: /**
12: * 回滚消费中的消息
13: * 逻辑类似于{@link #makeMessageToCosumeAgain(List)}
14: */
15: public void rollback() {
16: try {
17: this.lockTreeMap.writeLock().lockInterruptibly();
18: try {
19: this.msgTreeMap.putAll(this.msgTreeMapTemp);
20: this.msgTreeMapTemp.clear();
21: } finally {
22: this.lockTreeMap.writeLock().unlock();
23: }
24: } catch (InterruptedException e) {
25: log.error("rollback exception", e);
26: }
27: }
28:
29: /**
30: * 提交消费中的消息已消费成功,返回消费进度
31: *
32: * @return 消费进度
33: */
34: public long commit() {
35: try {
36: this.lockTreeMap.writeLock().lockInterruptibly();
37: try {
38: // 消费进度
39: Long offset = this.msgTreeMapTemp.lastKey();
40:
41: //
42: msgCount.addAndGet(this.msgTreeMapTemp.size() * (-1));
43:
44: //
45: this.msgTreeMapTemp.clear();
46:
47: // 返回消费进度
48: if (offset != null) {
49: return offset + 1;
50: }
51: } finally {
52: this.lockTreeMap.writeLock().unlock();
53: }
54: } catch (InterruptedException e) {
55: log.error("commit exception", e);
56: }
57:
58: return -1;
59: }
60:
61: /**
62: * 指定消息重新消费
63: * 逻辑类似于{@link #rollback()}
64: *
65: * @param msgs 消息
66: */
67: public void makeMessageToCosumeAgain(List<MessageExt> msgs) {
68: try {
69: this.lockTreeMap.writeLock().lockInterruptibly();
70: try {
71: for (MessageExt msg : msgs) {
72: this.msgTreeMapTemp.remove(msg.getQueueOffset());
73: this.msgTreeMap.put(msg.getQueueOffset(), msg);
74: }
75: } finally {
76: this.lockTreeMap.writeLock().unlock();
77: }
78: } catch (InterruptedException e) {
79: log.error("makeMessageToCosumeAgain exception", e);
80: }
81: }
82:
83: /**
84: * 获得持有消息前N条
85: *
86: * @param batchSize 条数
87: * @return 消息
88: */
89: public List<MessageExt> takeMessags(final int batchSize) {
90: List<MessageExt> result = new ArrayList<>(batchSize);
91: final long now = System.currentTimeMillis();
92: try {
93: this.lockTreeMap.writeLock().lockInterruptibly();
94: this.lastConsumeTimestamp = now;
95: try {
96: if (!this.msgTreeMap.isEmpty()) {
97: for (int i = 0; i < batchSize; i++) {
98: Map.Entry<Long, MessageExt> entry = this.msgTreeMap.pollFirstEntry();
99: if (entry != null) {
100: result.add(entry.getValue());
101: msgTreeMapTemp.put(entry.getKey(), entry.getValue());
102: } else {
103: break;
104: }
105: }
106: }
107:
108: if (result.isEmpty()) {
109: consuming = false;
110: }
111: } finally {
112: this.lockTreeMap.writeLock().unlock();
113: }
114: } catch (InterruptedException e) {
115: log.error("take Messages exception", e);
116: }
117:
118: return result;
119: }