孤舟笔记 并发篇十三 阻塞队列被异步消费顺序乱了怎么办?这道题藏着并发编程的核心思维
文章目录先说结论顺序消费的核心要点乱序的根本原因出队有序 ≠ 处理有序方案一单消费者——最简单但最慢方案二按 key 路由——同一 key 串行不同 key 并行方案三序号窗口——并发处理按序输出方案四CompletionService 序号重排顺序消费方案全景回答技巧与点评标准回答加分回答面试官点评个人网站你用阻塞队列做生产者-消费者模型生产者按顺序放了 1、2、3、4结果消费者那边收到的是 3、1、4、2——顺序全乱了。明明队列是 FIFO 的怎么消费顺序不对了这个问题在面试中高频出现因为它看似简单实则涉及线程池、消费并发度、顺序保证等多个知识点。今天咱们就把阻塞队列 顺序消费这个难题彻底搞明白。先说结论顺序消费的核心要点维度说明队列本身阻塞队列是 FIFO入队顺序 出队顺序乱序原因多个消费者线程并发处理处理速度不同核心矛盾队列保证了出队有序但不保证处理完成有序方案一单消费者——最简单但吞吐量低方案二相同 key 路由到同一队列/同一线程方案三序号窗口——按序号排序后再输出方案四CompletionService 序号重排一句话记住队列出队是有序的但多线程处理完的顺序不可控——保证顺序要么串行要么分组要么重排。乱序的根本原因出队有序 ≠ 处理有序阻塞队列本身是严格 FIFO的。先入队的消息一定先出队这点没问题。问题出在多个消费者线程并发处理队列: [1] [2] [3] [4] 线程A 取走消息1 → 处理中...耗时 3 秒 线程B 取走消息2 → 处理中...耗时 1 秒→ 先完成 线程C 取走消息3 → 处理中...耗时 2 秒消息 2 先处理完消息 1 还在处理。如果后续流程看谁先处理完顺序就是 2、3、1——乱了。生活类比银行取号排队3 个窗口同时服务。1 号去了慢窗口办贷款2 号去了快窗口存个钱。2 号比 1 号先办完——队伍是排好了但出银行的顺序乱了。关键认知队列保证了取的顺序但没法保证处理完的顺序。这是两码事。方案一单消费者——最简单但最慢最直接的办法只用一个线程消费。ExecutorServiceexecutorExecutors.newSingleThreadExecutor();// 单线程 executor.submit(()-{while(true){Messagemsgqueue.take();// 单线程取单线程处理process(msg);// 严格按序 }});优点绝对有序简单可靠。缺点吞吐量上不去单线程处理能力有限。适合场景消息量小、顺序要求极高如交易指令。方案二按 key 路由——同一 key 串行不同 key 并行大部分业务场景不需要全局有序只需要同一 key 的消息有序。比如同一个订单的消息必须有序不同订单之间无所谓。// 按消息 key 的 hash 路由到固定的队列/线程 intindexMath.abs(msg.getKey().hashCode())%queues.length;queues[index].put(msg);// 每个队列一个消费者线程同一个 key 的消息一定进同一个队列生活类比银行按业务类型分窗口——存取款一队、贷款一队、理财一队。每队内部严格 FIFO但不同队之间互不影响。这就是 Kafka 的 partition 思路——同一 partition 内有序不同 partition 之间并行。方案三序号窗口——并发处理按序输出如果必须全局有序又想并发处理怎么办处理时并发输出时按序号重排。// 每条消息带序号classMessage{longsequence;// 全局递增序号 Objectdata;}// 消费者处理完后放入排序缓冲区ConcurrentHashMapLong,MessagebuffernewConcurrentHashMap();AtomicLongexpectednewAtomicLong(1);// 期望的下一个序号 voidonMessageProcessed(Messagemsg){buffer.put(msg.sequence,msg);// 尝试按序输出while(true){longexpexpected.get();Messagembuffer.remove(exp);if(mnull)break;// 还没到等 output(m);// 按序输出expected.compareAndSet(exp,exp1);}}生活类比快递柜取件——你的包裹可能后到但先到柜也可能先发但后到柜。你按取件码从小到大依次取保证顺序。这就是 TCP 的乱序重排机制——并发到达按序号组装。方案四CompletionService 序号重排用 JDK 自带的CompletionService配合序号重排ExecutorCompletionServiceResultcsnewExecutorCompletionService(executor);// 提交时记录序号MapFutureResult,LongfutureToSeqnewConcurrentHashMap();for(Messagemsg:messages){FutureResultfcs.submit(()-process(msg));futureToSeq.put(f,msg.sequence);// 记录序号 }// 按完成顺序拿到结果再按序号排序输出ListResultresultsnewArrayList();for(inti0;imessages.size();i){FutureResultfcs.take();// 谁先完成拿谁Resultrf.get();results.add(r);}results.sort(Comparator.comparingLong(r-r.sequence));// 按序号排序 顺序消费方案全景阻塞队列顺序消费 全景 根本原因 队列出队有序 → 多线程并发处理 → 处理完成无序 四种方案 ├── 单消费者 ── 严格有序吞吐低 │ └── 适合消息量小、顺序要求极高 ├── 按 key 路由 ── 同 key 串行不同 key 并行 │ └── 适合局部有序即可如同一订单 ├── 序号窗口 ── 并发处理按序号输出 │ └── 适合全局有序 高吞吐 └── CompletionService 排序 ── 并发处理完成后重排 └── 适合批量处理 有序输出 口诀单消费保顺序key 路由局部序 序号窗口并发排完成排序也解难。回答技巧与点评标准回答阻塞队列本身是 FIFO 的出队有序。乱序的原因是多个消费者线程并发处理处理速度不同导致完成顺序和出队顺序不一致。保证顺序有四种方案单消费者简单但吞吐低、按 key 路由到同一队列局部有序类似 Kafka partition、序号窗口并发处理按序号排序输出、CompletionService 序号重排。实际中最常用的是按 key 路由兼顾顺序性和吞吐量。加分回答设计原则顺序性和吞吐量是天然矛盾的——完全有序必须串行并行必然可能乱序。设计时先问需要全局有序还是局部有序大部分场景局部有序就够了边界情况序号窗口方案中如果某个消息处理很慢后续消息会堆积在缓冲区造成内存压力。需要设置超时和降级策略。Kafka 的方案也有类似问题——某个 partition 消费慢会拖慢整体进度实际应用Kafka 用 partition 路由保证同 key 有序MQTT 协议用 QoS 级别和消息 ID 保证顺序数据库的 binlog 消费Canal用序号窗口做并发消费 有序提交面试官点评这道题考的是你在并发场景下的顺序保证思维。只说用单线程太浅。能分析出乱序的根本原因出队有序 ≠ 处理有序给出多种方案并说清适用场景才能拿高分。如果你能联系到 Kafka 的 partition 机制或 TCP 的乱序重排说明你有架构视角。原文阅读内容有帮助点赞、收藏、关注三连评论区等你