这个问题,决定你能不能进大厂!

0 阅读7分钟

沉默是金,总会发光

大家好,我是沉默

昨天,一位 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

  1. 执行业务逻辑
  2. 成功后再删除 processing_queue
  3. 后台守护线程扫描 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掉。