从数据库到缓存:一个任务调度系统的分层设计

调度系统的分层,要让职责清楚。

如果只是从功能角度看,定时任务系统好像没有那么多层。

无非就是任务创建、存储、扫描、触发、执行。但我后面做 xTimer 的时候越来越觉得,这类系统真正的关键,不在于模块列得全不全,而在于不同存储层到底负责什么。

因为只要这个边界模糊,后面很多问题都会跟着变乱。你会分不清谁是最终状态,谁只是加速层,谁又只是中间搬运层。

我为什么后来特别在意“分层”

一开始做项目的时候,我也有过一种冲动:哪个组件顺手就多让它干一点。

比如 Redis 很快,那是不是可以既当调度结构又当状态存储;数据库很稳,那是不是所有任务都直接靠数据库扫出来;消息队列已经接了,那是不是干脆把很多控制逻辑也放到消息流里。

但这些想法一旦混起来,系统虽然还能跑,后面就会越来越难讲清楚。

所以我后来给自己定了一个简单原则:每一层都要有清楚职责,不要让“快”和“准”混在一个地方。

如果让我拆 xTimer 的存储分层

我会把它大概拆成三层。

1. 数据库:最终真相层

数据库在我这里最重要的职责,不是快,而是稳。

它负责保存:

  • 任务定义。
  • 任务实例。
  • 执行状态。
  • 重试信息。
  • 恢复和排障需要的历史痕迹。

换句话说,如果系统出了问题,最后要回去对账、补偿、复盘,能信的还是数据库这层。

2. Redis:调度加速层

Redis 在这个系统里,我更愿意把它看成一个高频调度结构,而不是唯一存储。

它适合承担的事情包括:

  • 缓存临近执行窗口的任务。
  • 做时间有序推进。
  • 承接频繁扫描带来的压力。

这样设计的好处是,数据库不用被高频轮询打爆,Redis 也不用承担太重的业务一致性责任。

3. 消息或执行通道:任务推进层

任务被识别为该执行之后,系统还需要把它往真正的执行节点推。

这一层的核心不是存,而是流转。它要解决的是:

  • 怎么把可执行任务高效交给执行器。
  • 如果执行失败,怎么重试或者回写状态。
  • 如果下游慢了,怎么做缓冲和节奏控制。

这一步做不好,前面扫得再准也没意义。

为什么我不喜欢让单个组件承担全部语义

因为那样看起来省事,实际上会把复杂度藏起来。

比如如果你让 Redis 同时承担调度、状态、恢复和一致性语义,那它一开始会显得很高效。但只要一进入故障排查或者数据修复阶段,就会发现系统没有一个真正稳定的落点。

反过来,如果所有东西都压数据库,虽然语义最完整,但性能和调度时效又会吃不消。

所以分层设计真正解决的,不是“多用几个组件”,而是“让不同组件承担它们最适合承担的复杂度”。

这个思路对面试表达也很有用

很多时候面试官问你系统设计,不一定是想听你背一个标准架构图,他更想知道你有没有边界感。

比如你能不能说清楚:

  • 为什么数据库是最终状态来源。
  • 为什么 Redis 只是调度热层。
  • 为什么执行通道不应该反向承担业务真相。

这些问题答清楚之后,你的项目就不只是“用了数据库和缓存”,而更像是你真的思考过系统该怎么站稳。

我现在回头看这个项目

我觉得收获最大的,不是我多会用了一个组件,而是我开始理解:系统设计很多时候不是找一个最强的技术把事情全做了,而是主动承认不同技术各有边界,然后让它们在合理位置上协作。

至少对 xTimer 这种项目来说,这比单纯堆技术名词有用得多。

主分类 后端项目复盘

调度系统、缓存、幂等、服务端设计。

标签

阅读文章

相关记录