沉默是金,总会发光
大家好,我是沉默
昨天,一位 3 年经验的兄弟 找我,语音一接通就开始骂。
“我感觉字节二面挂得很冤,题不难,但我就是不知道哪儿错了。”
我让他复盘题目。
面试官问的是一个经典到不能再经典的业务题:
“淘宝 / 美团的订单,如果用户下单 30 分钟没支付,怎么自动取消?”
他说他几乎是秒答:
“这不简单吗?
写个定时任务(@Scheduled),每分钟扫一次数据库,
把超过 30 分钟没支付的订单状态改成【已取消】不就完了?”
他说完还挺自信。
但面试官的脸色,瞬间就变了。
接下来,对方连着甩了 3 个问题,一个比一个狠:
1. “如果现在数据库里有 1000 万条未支付订单,你每分钟全表扫一次,数据库顶得住吗?”
2. “你每分钟扫一次,那用户第 1 分钟下的单,可能要等到第 31 分 59 秒才被取消,这个延迟你觉得合理吗?”
3. “如果你的定时任务机器挂了,或者任务执行超过 1 分钟,这段时间的订单怎么办?”
他说到这的时候,直接沉默了。
“我当时脑子一片空白,完全不知道该怎么往下接。”
说实话,这不是他一个人的问题。
**-**01-
透过现象看本质
这题根本不是“取消订单”
很多人以为这道题考的是:
- 会不会写定时任务
- 会不会改数据库状态
完全错了。
这道题真正考的是:
海量数据下的「延迟任务(Delayed Task)」系统设计能力
说得再直白点:
-
你不是只会「扫表 + 改状态」
-
是需要有 分布式系统思维
这道题,在大厂面试里有个外号:
“延迟任务试金石”
答好了,是中高级
答错了,直接暴露天花板
- 02-
为什么「定时任务 + 扫数据库」是低级回答?
在高并发系统里,用 Cron 扫表 = 自杀式设计
在低并发、数据量小的场景(比如内部 OA、后台工具),
@Scheduled 没问题。
**
**
但在大厂业务里,它有 三个致命缺陷。
1. 时效性差:天生“不准时”
轮询一定有间隔:
- 每分钟扫一次
- 意味着 30 分钟超时 ≠ 30 分钟取消
最极端的情况:
用户 00:01 下单
系统 00:59 才扫到
延迟接近 1 分钟
在支付系统里,这是不可接受的。
** **
2. 数据库压力大:CPU 杀手
从架构视角看:
- 正确方式:事件驱动(Event-driven)
- 定时任务:反向拉数据(Polling)
全表扫描 + 高并发:
MySQL CPU 飙升
Buffer Pool 被打爆
慢查询雪崩
你不是在取消订单,你是在攻击数据库**。**
** **
3. 资源浪费:99% 的时间在“空跑”
现实情况是:
- 大部分时间 根本没有超时订单
- 但任务依然在不停跑
这在大厂眼里叫一句话:
“无效计算”
** **
高分答案的核心思想
不要去“找”超时订单,
要让“超时订单自己出现”。
- 03-
核心架构:3 种主流解法
下面这 3 种方案,是面试官默认期待你能逐层讲清楚的进阶路线**。**
** **
解法一:Redis 过期监听(面试官眼里的“陷阱”)
很多“半懂不懂”的候选人会抢答:
“Redis 不是有 Key 过期回调吗?
下单时把订单 ID 存 Redis,TTL 设 30 分钟,过期就取消订单!”
千万别这么答,这是送命题。
** **
为什么这是个坑?
1. 不可靠(致命)
Redis 的过期事件是:
“发了就算,没人接就丢”
- 服务重启
- 网络抖动
- 消费端异常
事件直接消失,订单永远不会被取消
** **
2. 不精确
Redis 的过期策略是:
- 惰性删除
- 定期扫描
并不保证:
Key 在 30 分钟那一刻准时失效
延迟几分钟是完全可能的。
面试结论:
Redis 过期监听 只能当辅助手段,不能当核心链路
** **
解法二:Redis ZSet 延迟队列(中高级标准解)
这是最通用、最稳妥、面试通过率最高的方案。
核心思想
用时间戳当排序规则,让 Redis 帮你“排队等时间”
** **
核心设计
- ZSet 的 Score:订单超时时间戳
- ZSet 的 Value:订单 ID
下单时(生产):
ZADD delay_queue <当前时间 + 30分钟> <orderId>
后台任务(消费):
ZRANGEBYSCORE delay_queue 0 <当前时间戳> LIMIT 0 10
含义:
“把所有已经过期的订单捞出来,一次最多处理 10 条”
** **
优点
- 内存操作,性能高
- 秒级精度
- 不扫数据库
这是 80% 大厂的默认实现方案。
** **
面试官一定会追问的「致命问题」
“如果你把订单从 ZSet 里删了,
但业务代码执行失败(比如服务宕机),
订单是不是就永远丢了?”
** **
满分回答:ACK + 二段式处理
“我们不会直接删除,而是 原子转移。”
流程是:
**1. Lua 脚本把订单
delay_queue → processing_queue
- 执行业务逻辑
- 成功后再删除
processing_queue - 后台守护线程扫描
processing_queue,
超时任务自动重试**
保证至少消费一次(At Least Once)
这一段说完,面试官基本就不再刁你了。
** **
解法三:MQ / 时间轮(架构师级)
当数据规模上到 亿级,ZSet 也会遇到瓶颈。
A. 消息队列延迟消息
- RocketMQ
-
- 4.x:固定延迟等级(面试一定要提局限)
- 5.0:支持任意时间(加分点)
- RabbitMQ
-
- TTL + 死信队列有 队头阻塞
- 必须提
delayed_message_exchange插件
B. 时间轮(Hashed Wheel Timer)
这是 Netty / Kafka 内部在用的算法。
- 本质:一个“时间刻度盘”
- 任务挂在未来的某个槽位
- 指针走到就执行
优缺点:
- 极快(纯内存)
- 不可靠(重启即丢)
大厂真实做法:
Redis 做持久化
内存时间轮做高频触发
**-****04-**总结
最后的“防杠三连问”(面试必考)
Q1:多个节点同时消费,怎么防重复?
Lua 保证原子性 + 业务幂等
Q2:ZSet 变成大 Key 怎么办?
分片(delay_queue_0 ~ delay_queue_9)
Q3:中间件全挂了怎么办?
T+1 数据库兜底扫描(跑从库)
面试标准答案模板(可直接背)
“订单超时本质是高并发延迟任务,
数据库轮询性能不可接受。我的方案是:
Redis ZSet 实现延迟队列 + Lua 保证原子性 + ACK 防丢失。下单时写入 ZSet,Score 为过期时间;
后台线程秒级扫描过期数据;
引入处理中队列防止宕机丢单;
接口严格幂等。大流量场景可升级 MQ 延迟消息,
最后保留离线兜底扫描保证最终一致性。”
技术面试,本质不是:
“你会不会用某个技术”
而是:
你是否尊重资源、
是否预判极端情况、
是否对系统失效有敬畏心。
这套 ZSet + 幂等 + MQ + 兜底 的组合拳:
- 订单超时
- 优惠券过期
- 红包回收
- 定时提醒
全部通用。
如果你觉得有用,点个赞、收藏一下。
下次面试,很可能就靠它救命。
**-****05-**粉丝福利
我这里创建一个程序员成长&副业交流群,
和一群志同道合的小伙伴,一起聚焦自身发展,
可以聊:
技术成长与职业规划,分享路线图、面试经验和效率工具,
探讨多种副业变现路径,从写作课程到私活接单,
主题活动、打卡挑战和项目组队,让志同道合的伙伴互帮互助、共同进步。
如果你对这个特别的群,感兴趣的,
可以加一下, 微信通过后会拉你入群,
但是任何人在群里打任何广告,都会被我T掉。