字节二面挂了!被问“订单超时自动取消”

前两天,有个做了 3 年 Java 的朋友找我复盘面试。

二面问得不算偏,甚至可以说很常见:

用户下单后 30 分钟没支付,系统怎么自动取消订单?

他几乎没停顿,直接回答:

写个定时任务,每分钟扫一次订单表,把超时未支付的订单改成已取消。

如果放在小系统里,这个答案其实不算离谱。

但面试官接着追了三句,他一下就没法往下接了:

  1. 如果未支付订单有上千万条,你每分钟扫一次数据库?
  2. 每分钟扫一次,订单可能 30 分 59 秒才取消,这个误差业务能接受?
  3. 如果定时任务那台机器挂了,漏掉的订单怎么办?

很多人挂在这道题上,不是因为不会做,而是因为只答了"能跑",没答到"怎么稳定地跑"。


一、为什么"定时任务扫库"不是好答案?

这个方案的问题,不在于不能用,而在于规模一上来就很吃力

所以这类题真正要表达的是:

不要让系统不断去数据库里"找"哪些订单过期了,而是要让"到期的订单"自己进入处理链路。

这就是典型的**延迟任务设计**。


二、很多人会想到 Redis 过期监听,但它不适合当主方案

有些候选人会说:

下单的时候往 Redis 写一个 Key,TTL 设 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 里取出来了,但业务还没执行完,服务就挂了,这个订单怎么办?

这时候,如果你只会说"重试一下",基本还不够。

更稳的设计是做成二段式处理

css 复制代码
flowchart TD
  A[用户下单] --> B[写入 Redis ZSet<br/>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 应该带状态条件:

ini 复制代码
UPDATEorders
SETstatus='CANCELED'
WHEREorder_id = ?
ANDstatus='WAIT_PAY';

只有"待支付"的订单才允许取消。

如果订单已经支付了,或者已经被取消了,这条 SQL 就不会再生效。

这就是面试里很加分的一句话:

延迟任务可以重复投递,但业务结果必须幂等。


六、如果订单量到了千万级,怎么继续往下做?

如果数据规模继续变大,一个 ZSet 也会慢慢变成大 Key。

这时候就不能只说"我用 Redis",还要补一句分片

分片思路

按订单号做 Hash,把任务分散到多个队列里:

路由规则类似这样:

ini 复制代码
queueIndex = hash(orderId) % 16

这样做的好处很直接:

  • 单个 ZSet 不会无限膨胀
  • 可以多线程、多实例并行消费
  • 某个分片积压,不会把全部任务拖死

当然,多个节点并发消费时,还要继续保证一件事:

抢任务必须原子化,取消订单必须幂等化。

这两件事少一个,方案都不算完整。


七、如果面试官继续追问,还可以再往上答

再大一点的场景,可以把方案再往上抬一层:

这里不用讲太深,但你可以顺手补一句:

如果公司已经有成熟 MQ 体系,我会优先评估延时消息能力;如果追求更高调度效率,可以考虑"持久化队列 + 时间轮"的组合。

这句话的作用,不是显摆,而是告诉面试官:

你知道这不是一道"背 Redis"题,而是一道"延迟任务系统设计"题。


八、面试里可以怎么答,比较顺?

如果你想把这道题说得更完整,可以按这个顺序回答:

订单超时取消,本质上是延迟任务问题。 小系统可以用定时任务扫库,但在高并发场景下,这个方案会有数据库压力大、时效性不稳定、任务容错差的问题。

我更倾向用 Redis ZSet 做延迟队列。下单时把订单号写入 ZSet,score 存 30 分钟后的过期时间。后台线程按当前时间拉取已到期订单,再触发取消逻辑。

为了避免并发重复消费,我会用 Lua 脚本保证取任务的原子性。为了避免任务取出后服务宕机导致丢失,我会引入 processing 队列,处理成功后再 ACK 删除,失败任务走补偿重试。

另外,取消订单接口本身必须做幂等控制,保证订单只能从待支付改成已取消,防止重复执行带来脏数据。
如果数据量继续增大,我会对延迟队列做分片,必要时再评估 MQ 延时消息方案。最后还会保留一个低频兜底扫描任务,确保极端情况下数据最终一致。

这套回答,基本就已经超过"知道怎么做"的层级了。


写在最后

这道题表面看,是"30 分钟后取消订单"。

但面试官真正想看的是另外几件事:

  • 你有没有规模意识
  • 你有没有可靠性意识
  • 你知不知道任务会丢、会重、会积压
  • 你会不会给系统留兜底

很多人不是不会写功能,而是习惯只讲"正常流程"。

可系统设计题,恰恰最看重异常流程。

所以,下次再被问到"订单超时自动取消",别急着回答"定时任务"。

先把延迟任务、原子性、幂等、补偿机制、最终一致性这几个关键词讲出来,答案就立住了。

相关推荐
羑悻1 小时前
我受够了 Coding Agent 的“假聪明,真串行”,于是做了 Meta-Orchestrator!
后端
饼干哥哥1 小时前
每晚300+个爆款疯狂跑,0人值守TikTok工厂开张!!
后端·操作系统·产品
65岁退休Coder1 小时前
LangChain v1.3.4 笔记 - 02 工具、结构化输出
后端
颜进强1 小时前
Claude Code - 18 从 Figma 到 React:一套让 AI 生成代码"不瞎编"的实战工作流
前端·后端·ai编程
geovindu2 小时前
CSharp: Bridge Pattern
开发语言·后端·桥接模式·结构型模式
霸道流氓气质2 小时前
SpringBoot中设计模式组合使用示例-策略、模板、观察者、门面、工厂、单例。
spring boot·后端·设计模式
货拉拉技术2 小时前
复杂业务规则太多?我们搭了一个规则可视化配置平台
后端
爱勇宝3 小时前
AI 都能写代码了,还要学计算机吗?
前端·后端·程序员
cfm_29143 小时前
Spring监听器
java·spring boot·后端