198. 打家劫舍要点prepre2动态规划class Solution { //只用维护prepre2 public int rob(int[] nums) { if(nums.length 1){ return nums[0]; } if(nums.length 2){ return Math.max(nums[0], nums[1]); } int pre2 nums[0]; //int pre nums[1]; int pre Math.max(nums[0], nums[1]); for(int i 2; i nums.length; i){ int curr Math.max(pre, pre2nums[i] ); pre2 pre; pre curr; } return pre; } }560. 和为 K 的子数组要点前缀和hashmapmap.put(0,1)初始化class Solution { public int subarraySum(int[] nums, int k) { //前缀和 MapInteger, Integer map new HashMap(); //要记得加这个 map.put(0,1); int ans 0; int sum 0; for(int i 0; i nums.length; i){ sum nums[i]; if(map.containsKey(sum - k)){ ans map.get(sum - k); } map.put(sum, map.getOrDefault(sum, 0) 1); } return ans; } }543. 二叉树的直径要点dfs/** * Definition for a binary tree node. * public class TreeNode { * int val; * TreeNode left; * TreeNode right; * TreeNode() {} * TreeNode(int val) { this.val val; } * TreeNode(int val, TreeNode left, TreeNode right) { * this.val val; * this.left left; * this.right right; * } * } */ class Solution { //dfs int ans 1; public int diameterOfBinaryTree(TreeNode root) { //int ans 1; depth(root); return ans - 1; } public int depth(TreeNode root){ if(root null){ return 0; } int left depth(root.left); int right depth(root.right); ans Math.max(ans, leftright1); return Math.max(left, right) 1; } }14. 最长公共前缀要点双重for对比class Solution { public String longestCommonPrefix(String[] strs) { String first strs[0]; StringBuilder ans new StringBuilder(); if(strs.length 0 || strs null){ return ; } for(int i 0; i first.length(); i){ char curr first.charAt(i); for(int j 1; j strs.length; j){ //注意小于等于 if(strs[j].length() i || strs[j].charAt(i) ! curr ){ return ans.toString(); } } ans.append(curr); } return ans.toString(); } }152. 乘积最大子数组要点动态规划dpmaxdpminclass Solution { public int maxProduct(int[] nums) { //记录最大最小max int[] dp new int[nums.length]; int[] dpmax new int[nums.length]; int[] dpmin new int[nums.length]; dpmax[0] nums[0]; dpmin[0] nums[0]; int max nums[0]; for(int i 1; i nums.length; i){ int num nums[i]; int tempmin Math.min(num, Math.min(dpmax[i-1]*num, dpmin[i-1]*num)); int tempmax Math.max(num, Math.max(dpmax[i-1]*num, dpmin[i-1]*num)); dpmax[i] tempmax; dpmin[i] tempmin; max Math.max(max, dpmax[i]); } return max; } }随机知识一、消息队列的作用必问核心题你项目里为什么要用消息队列解决了什么问题面试官为什么这么问这题考的是你对 MQ 存在价值的理解。我要看你是否遇到过同步处理太慢、系统耦合太紧的问题并知道用 MQ 的削峰、异步、解耦三大作用来解决。希望听到怎样的回答三大核心作用每个都配上例子异步处理比如用户注册后要发短信、发欢迎邮件同步调用会拖慢注册接口。扔给 MQ注册立刻返回短信/邮件后台慢慢发。应用解耦订单系统生成订单后需要通知库存系统和积分系统。如果直接调接口库存挂了订单会失败。改成发消息订单系统不需要知道谁在消费上下游独立。流量削峰秒杀活动时大量请求一瞬间涌入直接打 DB 会挂。请求先写 MQ后端服务按能力拉取处理平滑高峰。候选人消息队列在项目里主要解决三类问题同步耗时、系统耦合和突发流量冲击。这对应了消息队列最核心的三个作用异步处理、应用解耦、流量削峰。我结合实际业务分别说一下。第一异步处理提升接口响应速度。很多业务操作其实不需要立刻做完。比如用户注册后我们需要送积分、发欢迎邮件、发短信通知。如果这些操作全挤在注册接口里同步完成接口响应速度会变得特别慢而且积分系统或邮件服务如果稍微慢一点整个注册就失败了。引入消息队列后注册接口只需把用户信息写入数据库然后向 MQ 发一条“用户注册成功”的消息就直接返回给前端。积分系统、邮件服务这些下游慢任务各自订阅这条消息后台异步处理。注册接口耗时从按秒算直接降到几十毫秒用户体验大幅提升系统可靠性也更高。第二应用解耦降低系统间依赖和故障风险。复杂系统里服务之间往往存在上下游依赖。比如订单生成后需要通知积分系统加积分、通知物流系统发货、通知数据部门做统计。如果不引入中间件订单系统就得直接调用这三个下游系统的接口。一旦某下游系统挂了或响应超时订单就可能创建失败形成故障雪崩。用消息队列就完全不同。订单系统只负责把“订单已生成”这条消息投递出去至于谁读这条消息、怎么处理它完全不用关心。积分、物流、统计这些下游系统各自订阅同一个 Topic互不影响。以后若要新增一个消费方例如上线新的推荐系统也只需新增订阅消费者无需修改订单系统的任何代码。这就是真正的低耦合、高扩展。第三流量削峰在高并发下保护核心服务。大促或秒杀场景下瞬时流量可能是平时的几十倍。比如秒杀活动开始时几万个请求在一秒内同时冲过来扣减库存、写入订单。如果直接打到数据库连接数瞬间被打满轻则响应超时重则系统宕机。引入消息队列作为缓冲池后端用有限的消费者按固定速度去拉取消息处理。那些超出处理能力的请求并不是被丢弃或直接返回失败而是在队列中排队等待平滑了高峰流量冲击。我们还可以配合令牌桶或限流保证数据库的负载始终在安全水位而不是随瞬时流量一起抖动。通过异步、解耦、削峰三大手段消息队列让系统的响应更快、更稳、更容易扩展。这也是为什么在中大型系统里消息队列几乎是基础设施级别的组件。二、RabbitMQ 核心概念必问基础到不能再基础核心题RabbitMQ 里的 Exchange、Queue、Binding、Routing Key 分别是什么整个消息流转过程是怎样的面试官为什么这么问这是 RabbitMQ 独有的模型和 Kafka、RocketMQ 不一样。我问这个是要看你是否真正理解 AMQP 协议的设计知道一条消息从 Producer 到 Consumer 经历了什么。希望听到怎样的回答四个核心组件Producer发送消息的应用。Exchange交换机接收消息根据规则路由到队列。Queue队列存储消息消费者从这里取。Binding绑定把 Exchange 和 Queue 关联起来并指定 routing key。流转过程Producer 发消息到 Exchange带上 Routing key。Exchange 根据类型和 Binding 决定消息投递给哪些 Queue。Queue 存储消息等待 Consumer 拉取或自动推送。Consumer 处理消息返回 ACK。常见交换机类型directRouting key 完全匹配。topicRouting key 支持通配符*匹配一个单词和#匹配零或多个单词。fanout广播忽略 routing key所有绑定的队列都收到。headers用消息头匹配极少用。候选人这其实对应的是 AMQP 协议的核心模型。RabbitMQ 和 Kafka、RocketMQ 最大的不同就是它多了Exchange 和 Binding这两个角色把消息的路由规则和存储队列完全分开了。我先分别说清这四个角色再把整个流转过程串起来。一、四个核心组件Producer生产者发送消息的应用。它不直接发消息到队列而是发给 Exchange。Exchange交换机负责接收生产者发来的消息然后根据路由规则把消息分发到一个或多个队列。注意Exchange 本身不存储消息它只是一个“路由器”。如果它找不到匹配的队列消息默认会被丢弃。Queue队列真正存放消息的地方消息在这里等待被消费。队列是消费者直接对接的对象一个队列可以被多个消费者监听但同一条消息只会被一个消费者消费。Binding绑定它是一根“线”把 Exchange 和 Queue 连接起来。在绑定的同时会指定一个Binding Key。这个 Binding Key 是路由匹配的依据决定了 Exchange 收到的消息该投递给哪个队列。Routing Key是生产者在发送消息时指定的一个字符串。Exchange 就拿这个消息的 Routing Key和队列的 Binding Key 去匹配匹配上了就把消息投递过去。可以这样理解整个路由系统是一个快递分拨中心。Exchange 是分拨中心Queue 是具体楼宇的快递柜Binding 是把快递柜挂到分拨中心的线缆。Routing Key 是快递单上的地址标签。生产者把快递扔到分拨中心分拨中心根据标签和线缆的匹配关系把快递精准投入对应的某个或某些快递柜消费者从指定的快递柜取快递。二、完整流转过程Producer 发送消息它连接 Broker将带有 Routing Key 的消息投递给指定名称的 Exchange。Exchange 路由判断Exchange 收到消息后根据类型和 Binding Key 列表进行匹配。如果有队列的 Binding Key 和这条消息的 Routing Key 匹配Exchange 就把消息的副本投递到该队列。如果不匹配消息直接被丢弃或者根据设置的 mandatory/alternate-exchange 来兜底。Queue 存储消息消息进入队列后等待消费者处理。多个消费者共享一个队列时同一条消息只会投给其中一个消费者不会重复消费。Consumer 拉取消息消费者通常采用 Push 模式开启 basic.consume 自动接收也可以 Pull 模式主动拉。收到消息后处理业务逻辑。消息确认 ACK消费者处理成功后向 Broker 返回 ACK。Broker 将这条消息从队列中彻底删除。如果消费者宕机或处理失败返回 NACKRabbitMQ 可以将消息重新入队或进入死信队列。三、常见交换机类型RabbitMQ 有四种 Exchange 类型核心区别在于Routing Key 和 Binding Key 的匹配规则不同。类型路由规则典型场景DirectRouting Key 完全等于 Binding Key单播消息比如订单通知Topic支持通配符*一个词和#零或多个词按.分隔的单词模式匹配多路分发比如日志收集error.order、info.paymentFanout忽略 Routing Key消息直接广播到所有绑定队列广播通知比如配置刷新、缓存失效Headers根据消息头自定义匹配规则极少使用极特殊的头匹配场景举例我们面试系统中如果用户回答完题目需要触发评估生产者发送消息到assessment_exchangeRouting Key 是assessment.user.xxx用 Topic 交换机匹配assessment.user.#的队列对应的评估服务就能收到消息异步处理。总结Exchange 负责路由Queue 负责存储Binding 和 Routing Key 共同决定消息投递到哪里。理解 AMQP 的这一层模型才能用好 RabbitMQ 的灵活路由能力。三、消息可靠性高频线上踩坑重点核心题你觉得怎么保证消息不丢RabbitMQ 为此提供了哪些机制面试官为什么这么问消息丢失是生产事故。我问这个是要你从生产端、Broker 端、消费端三个环节完整阐述每个环节都说到关键机制。能说全说明你至少有排查过问题的意识。希望听到怎样的回答生产端Producer → Broker开启Publisher Confirm机制。生产者发消息后等待 Broker 返回确认没收到就重发。Broker 端队列持久化durable true。消息持久化deliveryMode 2。注意持久化也不是绝对不丢毕竟刚写完缓存还没刷盘可能宕机。更可靠可用镜像队列Mirror Queue。消费端Broker → Consumer关闭自动 ACK采用手动确认basicAck。处理成功才确认失败则basicNack或basicReject消息重回队列或进入死信。总结至少要提到Confirm 持久化 手动 ACK三者组合才能做到消息可靠传递。候选人好的这是一个生产环境中非常关键的问题。消息丢失可能发生在三个环节生产端到 Broker、Broker 自身、Broker 到消费端。要保证消息不丢就必须在每个环节都做好防护形成闭环。第一生产端防止发送时消息丢失。Producer 把消息发送给 Broker 的过程中可能因为网络抖动或 Broker 宕机导致消息没有送达。RabbitMQ 提供了Publisher Confirm发布确认机制来解决这个问题。开启 Confirm 模式后生产者每发一条消息Broker 收到并持久化后会返回一个 ACK 告诉生产者“我收到了”。如果消息投递失败比如找不到指定的交换机Broker 会返回 NACK或者在一定时间内没有任何回应。生产端代码需要处理这两种情况收到 ACK 就继续发下一条收到 NACK 或超时就重发这条消息。Confirm 是异步的可以批量确认性能比事务机制好得多是生产环境的标配。第二Broker 端防止消息在存储时丢失。消息到了 Broker 之后如果 Broker 突然宕机重启内存中的消息会全部丢失。所以 RabbitMQ 提供了持久化能力但持久化不是单一开关需要同时满足两个条件队列持久化声明队列时设置durable true。这样即使 Broker 重启队列本身不会被删除积压的消息还在。消息持久化发送消息时设置deliveryMode 2消息属性标记为持久化。这样消息会被写入磁盘而不是只存在内存里。但注意持久化也不是绝对安全。RabbitMQ 并不是每收到一条消息就立刻刷盘而是间隔一段时间批量写入如果在刷盘间隔内 Broker 宕机最近几条消息仍然可能丢失。所以有了更进一步的方案——镜像队列。镜像队列把队列数据同步到集群中的多个节点上。消息写入主节点后同步复制到从节点。主节点宕机从节点自动接管消息不会丢。代价是性能下降和网络开销增加。现在更推荐的是Quorum Queue仲裁队列基于 Raft 协议保证多数节点一致比老式镜像队列更可靠。第三消费端防止处理时消息丢失。Broker 把消息投递给消费者如果消费者刚收到消息还没处理就挂了消息也会丢。所以 RabbitMQ 在消费端提供了手动 ACK 机制。默认是自动 ACK消息投递给消费者Broker 立刻删掉消息。消费者宕机导致消息没处理完这条消息就永丢了。生产上必须关掉自动 ACK开启手动确认消费者处理完业务逻辑后调用basicAck告诉 Broker 可以安全删除消息。如果业务处理失败调basicNack或basicReject让消息重新入队或进入死信队列排队后续处理。手动 ACK 是整个可靠性链的最后一道防线处理成功才确认失败就重试或补偿。第四用具体场景串联。用户注册为例注册成功后触发发短信。注册成功要保证短信至少投递一次。用的是Confirm 持久化 手动 ACK的组合。生产者先发消息到 Broker用 Publisher Confirm 确认消息已送达消息和队列都持久化防止重启丢失。消费端关掉自动 ACK手动确认。如果消费端拿到消息后调短信服务失败可以有两种处理方式一是返回 NACK 把消息重新入队等待重试需要做好幂等处理二是超过重试上限后将消息转存到死信队列后续人工或定时任务补偿处理避免无限制重试对系统的冲击。同时为了防止消息积压可以配合死信队列加上监控告警当积压超过阈值时能立刻发现并人工介入。总结一句话保证消息不丢需要在生产端用 Publisher Confirm、Broker 端用持久化和镜像/仲裁队列、消费端用手动 ACK三者组合形成可靠传输闭环。另外还有两个重要补充一是声明 Exchange 和 Queue 时开启持久化并通过管理界面确认 D 标识已经生效因为切换配置后若不重建队列旧的队列属性不会自动变化需要物理删除再重新声明二是即使消息不会丢失也可能会重复服务链路必须实现幂等结合业务唯一键或消费记录去重表来保证数据的最终一致性。四、死信队列与延迟队列中高频实际项目最爱用核心题什么是死信队列怎么利用它实现延迟队列面试官为什么这么问死信队列是 RabbitMQ 实现可靠服务的兜底方案延迟队列则是定时任务的异步实现。我问这个是想看你会不会用 RabbitMQ 的特性实现业务需求比如下单未支付自动取消。希望听到怎样的回答死信Dead Letter消息在以下情况会变成死信被消费者 basicReject 或 basicNack 且 requeuefalse。消息 TTL 过期。队列已满无法入队。死信交换机DLX在声明队列时可以指定x-dead-letter-exchange消息成为死信后会被重新路由到指定的死信交换机再进入绑定队列供专门消费者处理记录日志、重试、人工介入。延迟队列实现RabbitMQ 本身没有延迟队列但可以通过TTL DLX组合实现声明一个普通队列设置消息 TTL或者设置队列 TTL。该队列不设消费者消息过期后自动成为死信。死信被路由到真正处理的队列消费者从那里消费就实现了“延迟”消费。结合项目“我们面经 Agent 中用户请求生成一份模拟面试如果大模型服务暂时不可用我们会把请求放入一个带 TTL 的消息如果 30 秒内没被成功消费就自动转入死信通知用户稍后重试。”候选人好的。这个问题分为两部分死信队列的概念和生成条件,以及如何巧妙地用 TTL 死信队列实现延迟队列。我从死信的基本概念讲起,然后说明延迟队列的实现原理,最后结合项目场景举例。第一,消息怎么变成死信。死信其实就是正常队列里待不下去的消息。RabbitMQ 定义了三种情况,消息会被转成死信一是消费者拒绝且不让消息重回队列。消费者收到消息后发现处理不了、格式错误或者重试次数已经耗尽,调basicNack或basicReject返回,同时把requeue设为false。消息被标记为不回去了,变成死信。二是消息 TTL 过期。发送消息时设了存活时间,或者队列设了 TTL 属性,消息在队列里等待超时还没被消费,RabbitMQ 自动判定它过期,成为死信。三是队列满了。队列设置了最大长度,新消息过来时队列已经满了,最老的消息会被挤出去,也变成死信。这三种情况下的死信会被重新交给一个特殊的交换机处理,就是死信交换机(DLX)。声明队列时加参数x-dead-letter-exchange,指定死信的回收站,消息成了死信后,原来的队列把消息原封不动(还会带上一些死信相关的 header)重新路由到死信交换机,再进入绑定的死信队列,等待专门消费者做善后处理——记录异常、发送告警、人工补偿。第二,怎么用死信队列实现延迟队列。RabbitMQ 自身没有直接支持等 30 秒再投递这种延迟队列,但可以通过TTL DLX这个组合巧妙实现。做法是创建两个队列一个是延时队列。这个队列本身不配消费者,只设两个关键属性x-message-ttl指定消息存活时间(或发消息时设expiration),以及x-dead-letter-exchange指向真正的业务交换机。消息进入这个队列后,就静静等待 TTL 到期。一个是业务处理队列。这个队列正常绑定消费者。当延时队列里的消息 TTL 过期、变成死信后,RabbitMQ 自动把这条死信转发到业务交换机,再路由到业务处理队列。消费者从这个业务处理队列拿到消息时,TTL 时间已经过去,达到了延迟消费的效果。举个例子订单 30 分钟未支付自动取消。下单后发消息到延时队列,设 TTL 30 分钟。这 30 分钟内消息搁在延时队列里等过期,时间一到自动转死信,路由到业务处理队列,消费者收到消息后检查订单状态,如果还未支付就执行取消。需要特别注意的是,RabbitMQ 对消息 TTL 的处理是在队列头部检查而非主动轮询每条消息。如果队列是多条不同过期时间的消息混合排列,第一条消息 TTL 未到,后面即使有消息已经过期也不会被处理,因为检查只发生在队列头部。所以建议每个延迟时间独立一个队列,保证同一队列中的所有消息具有统一的 TTL,避免过期消息被头部阻塞。另外,RabbitMQ 3.8 之后也提供了官方的延迟交换机插件rabbitmq-delayed-message-exchange,本质上类似逻辑,只是内部把 TTLDLX 的链路封装好了,使用更简洁。但无论哪种实现,底层都是基于存活时间到期后重新路由的机制。第三,项目中的实际应用。在面试系统里,我们有一个场景用户请求生成模拟面试,此时大模型服务可能正在高负载中,直接返回失败用户体验很差。用延迟队列做如下处理接到生成请求后,先尝试调大模型服务。如果成功,直接返回结果。如果大模型返回繁忙,把请求消息发到延时队列,设 TTL 30 秒。消费者监听的是延时队列对应的业务处理队列。30 秒后,消费者收到消息,再次尝试调用大模型——如果这次成功,正常生成面试结果如果仍然失败,记录异常信息到专门的死信队列,人工介入或通知用户稍后重试。这样的好处是用户请求不会直接丢失,系统会自动在合理时间窗口内重试,同时对大模型形成一定程度的压力缓冲。总结一句话死信队列是消息被拒绝、过期或队列满时的兜底路由机制延迟队列通过 TTL DLX 组合把消息在延时队列里搁一段时间,到期后自动转为死信并路由到真正的处理队列,适用于订单超时、失败重试等需要异步延时的场景。五、常见问题的解决方案高频核心题消息重复消费怎么办怎么保证消息的顺序面试官为什么这么问这是两个最经典的 MQ 工程难题难在回答出来要体现幂等性设计和对顺序性的合理取舍。面试官想看你是否陷入了“必须绝对顺序”的思维误区。希望听到怎样的回答消息重复消费MQ 保证的是“至少一次”投递所以消费者必须设计成幂等的。方案每条消息带全局唯一 ID业务 ID消费前先查数据库或 Redis 判断是否已处理过比如订单状态已支付就不再处理。或利用数据库唯一约束如插入订单号唯一索引让重复写入失败。消息顺序RabbitMQ 中一个 Queue 对应一个 Consumer是顺序的消息先进先出。但如果多个 Consumer 监听同一 Queue顺序就不保证了。实际业务中只要保证局部有序即可比如同一个订单的消息发到同一个队列按订单 ID 取模消费者统一拉取即可保证该订单内部处理有序。不迷信全局顺序成本太高。候选人好的这是消息队列里最经典的两个工程难题。我先说消息重复消费的根源和解决方案再讲顺序性的保证手段。第一消息重复消费怎么办首先要明白一个前提MQ 保证的是“至少一次”投递而不是“恰好一次”。生产者可能因为网络超时没收到 ACK 而重发消费者可能因为处理超时导致消息重新入队。不管哪种情况同一条消息可能被消费多次这是 MQ 的兜底机制决定的所以消费者必须自己实现幂等。什么是幂等就是同一个操作执行一次和执行多次最终结果是一样的。在下单、支付、扣库存这些关键业务里重复处理会出大问题。解决幂等我主要用以下几种方案方案一业务唯一键 数据库唯一索引。消息体里带上业务的唯一标识比如订单号、用户 ID 操作类型等。消费端处理前先把这个唯一键写入数据库的幂等表这张表有一个以消息唯一键为字段的唯一索引。插入成功说明是第一次处理继续执行业务插入失败说明这条消息已经处理过了直接跳过并返回 ACK。因为唯一键的插入和业务操作在同一个事务里要么一起成功要么一起失败不会出现重复。方案二Redis 记录已处理消息 ID。每个消息有一个全局唯一的消息 ID可以用业务 ID 拼接也可以生产者用雪花算法生成。消费前用SET NX EX把消息 ID 写入 Redis设置一个合理的过期时间比如业务处理周期的 2~3 倍。如果设置成功说明第一次处理如果 key 已存在直接跳过。这个方案的性能比数据库方案更高但要注意 Redis 和业务操作不在同一个事务里需要额外考虑补偿机制。一般用在允许极小概率重复、但对性能要求很高的场景。方案三数据库状态机。利用业务数据的当前状态做天然幂等。比如订单状态是“待支付”才允许改成“已支付”。如果消息重复消费订单已经是“已支付”了UPDATE 语句不会生效。UPDATE 返回受影响的行数来判断是否真正执行了更新。这三种方案并不是孤立的可以组合使用。支付、退款这类强资金操作用唯一索引加状态机双重保护聊天消息、日志同步这类允许极小概率重复的用 Redis 过滤加队列 TTL 兜底。核心原则是根据业务的风险容忍度选择合适的幂等方案不是所有数据都需要支付级别的保障。第二消息顺序怎么保证这里有一个思维误区要先澄清大多数业务不需要全局顺序只需要局部有序。要求全系统所有消息严格顺序意味着所有消息必须进同一个队列、由一个消费者单线程处理这等于把分布式系统退化成单机串行吞吐量极低完全不可接受。RabbitMQ 保证顺序的基本原理同一个队列里的消息是先进先出的单消费者模式下消息按入队顺序依次消费顺序得到保证。但如果多个消费者监听同一个队列分发是不保证顺序的——因为谁能拿到哪条消息几乎随机。要保证顺序关键是让需要有序的消息都进入同一个队列并由单个消费者处理。具体怎么实现用一致性哈希或取模的策略在生产者发送消息时用业务 ID比如订单 ID做哈希取模把同一个订单的所有消息都路由到同一个队列。举个具体例子一个订单从创建到支付到发货会产生多条消息。如果这组消息凭借相同的 Routing Key按订单 ID 算出的队列编号被投递到了同一个队列这个队列只被一个消费者监听那订单 A 的所有消息就能按入队顺序被处理。那如果消费者挂了怎么办这需要 RabbitMQ 配合高可用队列Quorum Queue以及消费者单线程处理。消费者重启后继续从上次确认的位置开始消费顺序不受影响。如果业务真的需要严格的全局排序比如银行流水那 RabbitMQ 本身不适合可能要考虑 Kafka。Kafka 在分区层面天然保证写入和消费顺序多分区并发时可以指定同一个 key 进同一分区。第三项目中的实际实践。项目的面试评估流中用户提交答案和生成评估消息之间有明显的先后依赖——先确认答案已完整提交并持久化再触发评估。所有同一用户的操作只投递到同一个队列消费端单线程处理。同时所有消费端都做了幂等——评估服务记录的消息 ID 保存到数据库幂等表的唯一字段中重复触发的评估直接过滤并返回 ACK。这样既保证了同一用户的操作顺序合理又防住了重试和重复投递带来的副作用。要记住的关键权衡顺序性和吞吐量天然矛盾严格全局顺序不切实际局部有序加上幂等兜底才真正能在生产环境落地。碎碎念后续会更新每天学习的八股和算法 题暑假实习找不到了开始准备秋招的第8天。努力连续更新100天今天确实vibecoding了一天感觉做了很多但是又没进脑子还是得自己手敲代码看了看ai的八股。重新整理一下ai项目手敲一遍吧还是。