前言
谈起这个问题,相信大家并不陌生,不管是在各种面试提问中,还是在真实业务场景中,都很常见了,保证消息消费的有序性还是很有意义的。
问题分析
首先要知道的是,MQ的每个Topic本身是有序的,它无法保证的是消费层面的有序性。而我们通常要解决的也是消费视角的复杂场景,其本质可以细化为:
- 单queue多consumer消费:不同consumer的执行时间是不固定的,无法保证先读到消息的consumer一定先完成操作,这样就可能出现消息并没有按照顺序执行,造成数据顺序错误。
- 单queue单consumer并发消费:单个一consumer在消费消息的时候采用了多线程消费,而各个线程的执行顺序没有做限制,这样也可能造成消息消费的顺序错误。
问题解决
通过分析问题背后的本质,我们就能直观感受到,要么就一个queue或一个topic绑定一个消费者,消费者一条一条消费,要么就让多个消费者共同消费的时候做并发控制,保证消费顺序。即:
- 单队列顺序消费
将所有需要保证顺序的消息发送到同一个队列中,消费者挨个消费,保证消费是顺序性。如:
// 生产者代码示例
DefaultMQProducer producer = new DefaultMQProducer("ProducerGroup");
producer.start();
Message msg = new Message("TopicTest", "OrderID001", "Hello world".getBytes());
producer.send(msg, new MessageQueueSelector() {
@Override
public MessageQueue select(List<MessageQueue> mqs, Message msg, Object arg) {
int queueIndex = arg.hashCode() % mqs.size(); // 根据订单ID的hash值选择队列
return mqs.get(queueIndex);
}
}, "OrderID001");
// 消费者代码示例
DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("ConsumerGroup");
consumer.subscribe("TopicTest", "*");
consumer.registerMessageListener(new MessageListenerOrderly() {
@Override
public ConsumeOrderlyStatus consumeMessage(List<MessageExt> msgs, ConsumeOrderlyContext context) {
for (MessageExt msg : msgs) {
// 处理消息
}
return ConsumeOrderlyStatus.SUCCESS;
}
});
consumer.start();
- 并发控制消费
对多个消费者或者多个线程进行手动并发控制,保证消费的顺序性。
这里我们可以手动原始建锁进行并发控制,也可以借助相关数据结构,例如BlockingQueue:
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.LinkedBlockingQueue;
public class BlockingQueueTest {
public static void main(String[] args) throws InterruptedException {
// 声明一个容量为10的缓存队列
BlockingQueue<String> queue = new LinkedBlockingQueue<String>(10);
//new了三个生产者和一个消费者
Producer producer1 = new Producer(queue);
Producer producer2 = new Producer(queue);
Producer producer3 = new Producer(queue);
Consumer consumer = new Consumer(queue);
// 借助Executors
ExecutorService service = Executors.newCachedThreadPool();
// 启动线程
service.execute(producer1);
service.execute(producer2);
service.execute(producer3);
service.execute(consumer);
// 执行10s
Thread.sleep(10 * 1000);
producer1.stop();
producer2.stop();
producer3.stop();
Thread.sleep(2000);
// 退出Executor
service.shutdown();
}
}
生产者:
import java.util.Random;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
/**
* 生产者线程
*
* @author jackyuj
*/
public class Producer implements Runnable {
private volatile boolean isRunning = true;//是否在运行标志
private BlockingQueue queue;//阻塞队列
private static AtomicInteger count = new AtomicInteger();//自动更新的值
private static final int DEFAULT_RANGE_FOR_SLEEP = 1000;
//构造函数
public Producer(BlockingQueue queue) {
this.queue = queue;
}
public void run() {
String data = null;
Random r = new Random();
System.out.println("启动生产者线程!");
try {
while (isRunning) {
System.out.println("正在生产数据...");
Thread.sleep(r.nextInt(DEFAULT_RANGE_FOR_SLEEP));//取0~DEFAULT_RANGE_FOR_SLEEP值的一个随机数
data = "data:" + count.incrementAndGet();//以原子方式将count当前值加1
System.out.println("将数据:" + data + "放入队列...");
if (!queue.offer(data, 2, TimeUnit.SECONDS)) {//设定的等待时间为2s,如果超过2s还没加进去返回false
System.out.println("放入数据失败:" + data);
}
}
} catch (InterruptedException e) {
e.printStackTrace();
Thread.currentThread().interrupt();
} finally {
System.out.println("退出生产者线程!");
}
}
public void stop() {
isRunning = false;
}
}
消费者:
import java.util.Random;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.TimeUnit;
/**
* 消费者线程
*
* @author jackyuj
*/
public class Consumer implements Runnable {
private BlockingQueue<String> queue;
private static final int DEFAULT_RANGE_FOR_SLEEP = 1000;
//构造函数
public Consumer(BlockingQueue<String> queue) {
this.queue = queue;
}
public void run() {
System.out.println("启动消费者线程!");
Random r = new Random();
boolean isRunning = true;
try {
while (isRunning) {
System.out.println("正从队列获取数据...");
String data = queue.poll(2, TimeUnit.SECONDS);//有数据时直接从队列的队首取走,无数据时阻塞,在2s内有数据,取走,超过2s还没数据,返回失败
if (null != data) {
System.out.println("拿到数据:" + data);
System.out.println("正在消费数据:" + data);
Thread.sleep(r.nextInt(DEFAULT_RANGE_FOR_SLEEP));
} else {
// 超过2s还没数据,认为所有生产线程都已经退出,自动退出消费线程。
isRunning = false;
}
}
} catch (InterruptedException e) {
e.printStackTrace();
Thread.currentThread().interrupt();
} finally {
System.out.println("退出消费者线程!");
}
}
}
当然,借助版本标识、时间戳啥的在消费前重排序,本质也是一样的,总之我们要知道解决问题的方向。