
前两天有个做了 3 年 Java 的朋友找我复盘面试。二面问得不算偏甚至可以说很常见用户下单后 30 分钟没支付系统怎么自动取消订单他几乎没停顿直接回答写个定时任务每分钟扫一次订单表把超时未支付的订单改成已取消。如果放在小系统里这个答案其实不算离谱。但面试官接着追了三句他一下就没法往下接了如果未支付订单有上千万条你每分钟扫一次数据库每分钟扫一次订单可能 30 分 59 秒才取消这个误差业务能接受如果定时任务那台机器挂了漏掉的订单怎么办很多人挂在这道题上不是因为不会做而是因为只答了“能跑”没答到“怎么稳定地跑”。一、为什么“定时任务扫库”不是好答案这个方案的问题不在于不能用而在于规模一上来就很吃力。所以这类题真正要表达的是不要让系统不断去数据库里“找”哪些订单过期了而是要让“到期的订单”自己进入处理链路。这就是典型的延迟任务设计。二、很多人会想到 Redis 过期监听但它不适合当主方案有些候选人会说下单的时候往 Redis 写一个 KeyTTL 设 30 分钟Key 过期后监听事件再取消订单。这思路听起来挺顺但面试里最好别把它当核心方案。原因很简单所以 Redis 过期监听可以提一句说明你知道这条路但最好顺手点明这个方案简单但可靠性不够不适合作为订单超时取消的主链路。三、比较稳的做法Redis ZSet 做延迟队列这道题里更常规、也更容易展开的答案是Redis ZSet。思路不复杂score存订单超时时间member存订单号消费端按当前时间去取已经到期的订单生产数据时ZADD order_delay_queue 1737558000 order_10086这里的1737558000就是“当前时间 30 分钟”的时间戳。消费数据时ZRANGEBYSCORE order_delay_queue 0 当前时间戳 LIMIT 0 100取出的就是已经到期的订单。四、这套方案为什么比扫库强它的核心优势有两个第一不扫订单表。 数据库不用反复查“哪些数据超时了”查询压力从 MySQL 转移到了 Redis。第二只处理已到期任务。 不是把海量订单全拿出来筛而是直接处理应该被取消的那一小批。五、真正拉开差距的不是 ZSet而是你怎么处理“任务丢失”面试官通常不会停在“用 ZSet”这一步。更关键的问题是如果你把订单从 ZSet 里取出来了但业务还没执行完服务就挂了这个订单怎么办这时候如果你只会说“重试一下”基本还不够。更稳的设计是做成二段式处理。flowchart TD A[用户下单] -- B[写入 Redis ZSetbr/score超时时间] B -- C[消费者扫描到期任务] C -- D[Lua 脚本原子转移到 processing_queue] D -- E[执行取消订单逻辑] E -- F{取消成功?} F -- 是 -- G[从 processing_queue 删除] F -- 否 -- H[保留任务等待补偿重试]这里最重要的点有两个1. 不要直接“取出就删”更稳妥的是先从delay_queue原子转移到processing_queue业务执行成功后再从processing_queue删除如果服务中途挂了补偿线程还能从processing_queue找回任务这样就不会因为服务异常导致任务直接丢失。2. 业务接口必须幂等即使任务被重复执行也不能重复取消。比如取消订单时SQL 应该带状态条件UPDATEorders SETstatusCANCELED WHEREorder_id ? ANDstatusWAIT_PAY;只有“待支付”的订单才允许取消。如果订单已经支付了或者已经被取消了这条 SQL 就不会再生效。这就是面试里很加分的一句话延迟任务可以重复投递但业务结果必须幂等。六、如果订单量到了千万级怎么继续往下做如果数据规模继续变大一个 ZSet 也会慢慢变成大 Key。这时候就不能只说“我用 Redis”还要补一句分片。分片思路按订单号做 Hash把任务分散到多个队列里路由规则类似这样queueIndex hash(orderId) % 16这样做的好处很直接单个 ZSet 不会无限膨胀可以多线程、多实例并行消费某个分片积压不会把全部任务拖死当然多个节点并发消费时还要继续保证一件事抢任务必须原子化取消订单必须幂等化。这两件事少一个方案都不算完整。七、如果面试官继续追问还可以再往上答再大一点的场景可以把方案再往上抬一层这里不用讲太深但你可以顺手补一句如果公司已经有成熟 MQ 体系我会优先评估延时消息能力如果追求更高调度效率可以考虑“持久化队列 时间轮”的组合。这句话的作用不是显摆而是告诉面试官你知道这不是一道“背 Redis”题而是一道“延迟任务系统设计”题。八、面试里可以怎么答比较顺如果你想把这道题说得更完整可以按这个顺序回答订单超时取消本质上是延迟任务问题。 小系统可以用定时任务扫库但在高并发场景下这个方案会有数据库压力大、时效性不稳定、任务容错差的问题。我更倾向用 Redis ZSet 做延迟队列。下单时把订单号写入 ZSetscore 存 30 分钟后的过期时间。后台线程按当前时间拉取已到期订单再触发取消逻辑。为了避免并发重复消费我会用 Lua 脚本保证取任务的原子性。为了避免任务取出后服务宕机导致丢失我会引入 processing 队列处理成功后再 ACK 删除失败任务走补偿重试。另外取消订单接口本身必须做幂等控制保证订单只能从待支付改成已取消防止重复执行带来脏数据。如果数据量继续增大我会对延迟队列做分片必要时再评估 MQ 延时消息方案。最后还会保留一个低频兜底扫描任务确保极端情况下数据最终一致。这套回答基本就已经超过“知道怎么做”的层级了。写在最后这道题表面看是“30 分钟后取消订单”。但面试官真正想看的是另外几件事你有没有规模意识你有没有可靠性意识你知不知道任务会丢、会重、会积压你会不会给系统留兜底很多人不是不会写功能而是习惯只讲“正常流程”。可系统设计题恰恰最看重异常流程。所以下次再被问到“订单超时自动取消”别急着回答“定时任务”。先把延迟任务、原子性、幂等、补偿机制、最终一致性这几个关键词讲出来答案就立住了。