
写在开头昨天一位 3 年经验的兄弟找我哭诉说字节二面挂得莫名其妙。 面试官问了一个很经典的业务题“淘宝/美团的订单如果用户下单 30 分钟没支付怎么自动取消订单”他想都没想直接回答“简单啊写个定时任务Schedule每分钟去数据库捞一次把超过 30 分钟的订单查出来状态改成取消不就行了”面试官听完连问了三个问题“如果数据库里有 1000 万条未支付订单你每一分钟全表扫一次数据库不崩吗”“你每分钟扫一次那用户第 1 分钟下单岂不是可能第 31 分 59 秒才被取消延迟这么大能接受吗”“如果你的定时任务机器挂了或者任务执行时间超过了 1 分钟这期间的订单怎么办”他瞬间哑火。其实这道题是分布式系统设计的“试金石”。面试官考的不是“取消”这个动作而是“海量数据的延迟任务Delayed Task”怎么设计。今天我们拆解这道题的3 种进阶打法从“小作坊”到“大厂架构”最后给你一套无懈可击的面试模板。一、 为什么 “定时任务 (Cron)” 是低级回答在低并发、数据量小的系统比如内部 OA用 SpringScheduled跑定时任务没问题。但在大厂高并发场景下它有三个致命死穴时效性差轮询有间隔无法精确到秒级取消。数据库压力大由“推”变“拉”频繁的全表扫描Scan是数据库 CPU 飙升的元凶。资源浪费大部分时候可能根本没有超时订单但任务还在空跑。所以高分答案的核心思路必须是不要去轮询数据库而是让超时订单“自己找上门”。二、 核心架构3 种主流解法从青铜到王者解法 1Redis 过期监听面试官眼里的“大坑”很多自作聪明的候选人会说“Redis 有 Key 过期回调功能把订单号存 Redis过期时间设 30 分钟过期了触发事件不就行了”千万别这么答这是个陷阱不可靠Redis 的过期监听Expired Event是“发后即忘”的。如果你的服务当时重启了或者网络抖动没收到通知这个事件就丢了订单永远不会被取消。延迟大Redis 的过期删除策略是“惰性定期”并不保证 Key 刚好在 30 分钟那一刻立刻删除延迟几分钟是常事。解法 2Redis ZSet 轮询中高级标准解法这是最推荐的通用方案利用 Redis 的有序集合ZSet。原理利用 ZSet 的Score属性存储“订单超时的具体时间戳”Value存订单号。生产阶段下单ZADD delay_queue 30分钟后的时间戳 OrderId消费阶段轮询 启动一个后台线程每秒从 ZSet 里“捞”数据。我们要找的是Score 当前时间的元素即已经超时的。ZRANGEBYSCORE delay_queue 0 当前时间戳 LIMIT 0 10优点性能高内存读写精准秒级误差。⚠️ 高阶防坑点关键面试官可能会问“如果 Lua 脚本把 Redis 数据删了但业务逻辑执行失败比如服务挂了这笔订单岂不是永久丢失了”满分补丁 “为了防止这种情况我们采用Ack 机制或二段式处理 Lua 脚本不是直接删除而是把订单号从delay_queue原子移动到processing_queue处理中队列。 业务处理完毕后再删除processing_queue里的数据。 如果服务宕机后台有守护线程扫描processing_queue中停留过久的任务进行重试。这样就保证了至少消费一次At Least Once。”解法 3消息队列 / 时间轮架构师级解法如果数据量达到亿级ZSet 的大 Key 也会有性能瓶颈。这时候要搬出“大杀器”。A. 消息队列RocketMQ / RabbitMQ利用 MQ 的“延时消息”功能。RocketMQ注意RocketMQ 4.x 只支持固定的延时等级1s, 5s...30m不够灵活。如果面试官问“非固定时间的任意延迟怎么办”你要提RocketMQ 5.0支持任意时间或者用Redis ZSet兜底。RabbitMQ原生 TTL 死信队列有一个坑叫**“队头阻塞”**如果第一个消息没过期后面的过期了也取不出来。必须使用rabbitmq_delayed_message_exchange插件才能解决。B. 时间轮算法 (Hashed Wheel Timer)这是 Netty、Kafka 内部都在用的底层算法。逻辑想象一个钟表有 60 个格子指针每秒走一格。订单 30 分钟后过期就把它挂在“当前格子 1800”的那个槽位上。优势纯内存操作极其高效。短板内存不可靠重启即丢。大厂实践通常是用Redis ZSet 做持久化存储 内存时间轮做高频触发。Redis 负责存 1 小时后的任务应用启动时把近期任务加载到内存时间轮里执行。三、 最后的“防杠”指南扫清死角设计完架构面试官一定会追问死角这三个回答能帮你拿 offerQ1多个节点同时轮询 ZSet怎么防止重复取消订单答 “这是一个经典的并发问题。 第一利用Lua 脚本保证ZRANGE和ZREM的原子性谁抢到谁删防止多线程读到同一条。 第二业务幂等。取消订单的 Service 接口必须实现幂等不管调几次状态只能从‘未支付’变‘已取消’更新成功才返回 true否则返回 false。”Q2Redis ZSet 变成大 Key 怎么办千万级订单答 “分片Sharding。 不要把所有订单放一个 Key。按订单 ID 哈希取模分散到delay_queue_0到delay_queue_9这 10 个 ZSet 里。启动 10 个线程分别去轮询吞吐量直接翻 10 倍。”Q3万一中间件全崩了怎么办答 “虽然概率极低但必须有兜底Fallback。 我会保留一个T1 的离线扫描任务跑在从库上每天凌晨把昨天遗漏的未支付订单扫一遍进行取消。架构设计要有‘中间件解耦’的自信也要有‘最终一致性’的敬畏。”四、 面试标准答案模板直接背诵下次被问到“订单超时取消”或“延迟任务”直接按这个套路输出“对于订单超时这种高并发延迟任务简单的数据库轮询是绝对不行的性能太差。我的设计思路是‘存储与计算分离利用中间件解耦’架构选型首选Redis ZSet实现轻量级延迟队列。Score 存过期时间戳Value 存订单号。核心流程后台调度线程每秒利用ZRANGEBYSCORE查询超时的订单拿到后利用Lua 脚本原子性地移除并执行取消逻辑。可靠性保障为了防止‘取出后宕机’导致数据丢失我会引入‘处理中队列’做 ACK 机制同时取消接口严格实现幂等防止重复消费。进阶优化如果业务量极大我会考虑RocketMQ 5.0 的任意延迟消息彻底解放业务服务。兜底保障最后保留一个低频的数据库兜底扫描任务确保数据在极端情况下也能最终一致。”资料展示下面是我整理的AI大模型学习资料和工具包预览适合收藏后按主题逐步学习。如果你想看完整资料目录可以在评论区留言「资料」也欢迎告诉我你更关注AI大模型里的哪类内容。