从数据库到缓存:一个任务调度系统的分层设计
调度系统的分层,要让职责清楚。
如果只是从功能角度看,定时任务系统好像没有那么多层。
无非就是任务创建、存储、扫描、触发、执行。但我后面做 xTimer 的时候越来越觉得,这类系统真正的关键,不在于模块列得全不全,而在于不同存储层到底负责什么。
因为只要这个边界模糊,后面很多问题都会跟着变乱。你会分不清谁是最终状态,谁只是加速层,谁又只是中间搬运层。
我为什么后来特别在意“分层”
一开始做项目的时候,我也有过一种冲动:哪个组件顺手就多让它干一点。
比如 Redis 很快,那是不是可以既当调度结构又当状态存储;数据库很稳,那是不是所有任务都直接靠数据库扫出来;消息队列已经接了,那是不是干脆把很多控制逻辑也放到消息流里。
但这些想法一旦混起来,系统虽然还能跑,后面就会越来越难讲清楚。
所以我后来给自己定了一个简单原则:每一层都要有清楚职责,不要让“快”和“准”混在一个地方。
如果让我拆 xTimer 的存储分层
我会把它大概拆成三层。
1. 数据库:最终真相层
数据库在我这里最重要的职责,不是快,而是稳。
它负责保存:
- 任务定义。
- 任务实例。
- 执行状态。
- 重试信息。
- 恢复和排障需要的历史痕迹。
换句话说,如果系统出了问题,最后要回去对账、补偿、复盘,能信的还是数据库这层。
2. Redis:调度加速层
Redis 在这个系统里,我更愿意把它看成一个高频调度结构,而不是唯一存储。
它适合承担的事情包括:
- 缓存临近执行窗口的任务。
- 做时间有序推进。
- 承接频繁扫描带来的压力。
这样设计的好处是,数据库不用被高频轮询打爆,Redis 也不用承担太重的业务一致性责任。
3. 消息或执行通道:任务推进层
任务被识别为该执行之后,系统还需要把它往真正的执行节点推。
这一层的核心不是存,而是流转。它要解决的是:
- 怎么把可执行任务高效交给执行器。
- 如果执行失败,怎么重试或者回写状态。
- 如果下游慢了,怎么做缓冲和节奏控制。
这一步做不好,前面扫得再准也没意义。
为什么我不喜欢让单个组件承担全部语义
因为那样看起来省事,实际上会把复杂度藏起来。
比如如果你让 Redis 同时承担调度、状态、恢复和一致性语义,那它一开始会显得很高效。但只要一进入故障排查或者数据修复阶段,就会发现系统没有一个真正稳定的落点。
反过来,如果所有东西都压数据库,虽然语义最完整,但性能和调度时效又会吃不消。
所以分层设计真正解决的,不是“多用几个组件”,而是“让不同组件承担它们最适合承担的复杂度”。
这个思路对面试表达也很有用
很多时候面试官问你系统设计,不一定是想听你背一个标准架构图,他更想知道你有没有边界感。
比如你能不能说清楚:
- 为什么数据库是最终状态来源。
- 为什么 Redis 只是调度热层。
- 为什么执行通道不应该反向承担业务真相。
这些问题答清楚之后,你的项目就不只是“用了数据库和缓存”,而更像是你真的思考过系统该怎么站稳。
我现在回头看这个项目
我觉得收获最大的,不是我多会用了一个组件,而是我开始理解:系统设计很多时候不是找一个最强的技术把事情全做了,而是主动承认不同技术各有边界,然后让它们在合理位置上协作。
至少对 xTimer 这种项目来说,这比单纯堆技术名词有用得多。
调度系统、缓存、幂等、服务端设计。