定时更新
用户反馈:
- “为什么你们的股票数据是 5 分钟前的?”
- “新闻更新太慢了”
技术分析:
- 被动缓存:用户访问时才更新
- 热点数据:应该主动更新
- 定时任务:需要任务调度系统
引入任务调度系统,主动更新缓存。
方案对比
| 方案 | 优势 | 劣势 |
|---|---|---|
| cron | 简单、系统自带 | 不支持分布式 |
| Celery | 功能强大、分布式 | 复杂、依赖多 |
| APScheduler | 轻量、易用 | 单机 |
| 云流程 | 免运维 | 成本高、有延迟 |
选择 APScheduler(轻量、满足需求)
选型边界
为什么先用 APScheduler
- 触发问题
- 热点数据需要主动刷新,单靠用户访问触发更新会让股票、新闻等 API 显得滞后。
- 候选方案
- 系统 cron、APScheduler、Celery Beat、云函数定时触发。
- 选择理由
- 当前任务数量少、依赖简单,APScheduler 可以直接嵌入服务,足够验证定时预热和定时清理的价值。
- 代价
- 它偏单机,服务重启、重复执行和任务漂移都需要额外处理;多实例部署时还要避免多个实例同时跑同一任务。
- 暂不解决
- 暂不引入 Celery 或云流程,因为现在还没有大量长任务、分布式 worker 和复杂编排需求。
安装
选 APScheduler 的前提是它能嵌进现有服务,不用额外运维一套进程。所以安装方式决定了后面所有行为:调度器跟着应用进程走,进程重启后任务就得能重新注册,否则预热会在重启后悄悄停掉。
安装:部署操作
- 步骤 1:把 APScheduler 装进项目依赖,确认与当前 Python 版本兼容
- 步骤 2:让调度器随应用进程一起启动,在启动钩子里集中注册任务
- 步骤 3:本地跑一个测试任务,确认触发时间与实际执行时刻一致
- 步骤 4:重启服务后核对任务是否被重新注册,既不丢失也不重复
关注点:调度器与服务进程的生命周期绑定方式,以及重启后的任务状态。
基础配置
基础配置要解决三件事:调度器实例怎么建、任务参数放在哪、进程退出时怎么收尾。这些一旦分散在各处,后面每加一个任务都会变成复制粘贴。
基础配置
- 步骤 1:创建 BackgroundScheduler 实例,显式设置时区与 misfire_grace_time
- 步骤 2:把任务函数、触发参数、并发数和合并策略集中写在同一张任务注册表里
- 步骤 3:在启动流程里调用 start(),同时保留一个手动 shutdown 的出口
关注点:调度器实例怎么建,以及任务默认参数放在哪里。
1. 固定频率任务
固定频率最适合行情刷新这种节奏明确的任务。但间隔一旦小于执行耗时就会立刻堆积,所以任务内部一定要能判断「上一轮还没跑完」,宁可跳过这一轮也不要并发。
1. 固定频率任务
- 步骤 1:用 IntervalTrigger 按固定秒数设定触发间隔,比如每 5 秒一次
- 步骤 2:在任务函数里先判断是否已有一次执行在跑,避免任务叠加
- 步骤 3:记录本轮的触发时间与执行耗时,超过间隔时打点告警
- 步骤 4:观察任务实际触发间隔,确认没有因为耗时而堆积
关注点:固定间隔触发的任务,如何应对执行耗时超过间隔的情况。
2. Cron 表达式任务
cron 适合「整点、每天几点」这类按日历走的任务,最容易踩的坑是时区——服务器在 UTC、日志按本地时间看,就会差八小时。所以时区必须显式配置。
2. Cron 表达式任务
- 步骤 1:用 CronTrigger 按 cron 表达式设定触发时刻,例如每小时的第 5 分钟
- 步骤 2:明确调度器时区,按业务所在时区计算,避免服务器时区偏差
- 步骤 3:预先校验表达式是否合法,并确认错过的任务按预期补跑
- 步骤 4:观察首次触发时间是否符合预期,确认后再放开到真实流量
关注点:cron 表达式的时区处理,以及错过触发时间后的补跑策略。
3. 一次性任务
一次性任务用完就该消失,但它偏偏最容易在服务重启后重复执行。所以除了设定触发时间,还必须给任务本身加一个「是否已执行」的判断。
3. 一次性任务
- 步骤 1:用 DateTrigger 指定一个明确的时间点,让它只触发一次
- 步骤 2:执行前检查任务是否已跑过,避免服务重启后重复触发
- 步骤 3:执行完成后标记为已结束并从调度器移除,失败任务保留记录支持人工重新排队
关注点:一次性任务的重启语义,以及它与普通调度的区别。
查看任务列表
排查任务问题时,第一件事是想知道「现在到底有哪些任务在排期」。任务列表接口要给出触发器和下次执行时间,光有任务名是不够的。
查看任务列表
- 步骤 1:遍历调度器里已注册的任务,输出任务 ID、触发器和下次执行时间
- 步骤 2:标出任务的启用状态与所属接口,方便按业务维度排查
- 步骤 3:对长时间没有触发的任务给出提示,排查调度是否已经停摆
- 步骤 4:把这份视图暴露成接口,供运维和控制台直接查看
关注点:任务元数据是否足以判断一次任务的真实状态。
手动触发任务
手动触发是出事时最快的补救手段,也是最容易引入不一致的地方。所以它必须复用同一份任务函数,而不是另写一套逻辑,否则线上和手动的行为会对不上。
手动触发任务
- 步骤 1:暴露手动触发接口,参数只接受任务标识,不允许传入任意可执行内容
- 步骤 2:复用调度器里的同一个任务函数,保证人工触发与自动触发行为一致
- 步骤 3:任务已在执行时直接返回「正在执行」,而不是并发再跑一遍
- 步骤 4:记录触发来源、操作人与触发原因,便于事后追溯
关注点:人工触发与自动触发是否共用同一条执行路径。
前面的任务都是固定形态的,这里开始处理更贴近业务的三类活:更新热点数据、预热缓存、清理过期数据。它们共用同一套调度机制,但各自要解决自己的失败处理。
热点数据更新
被动缓存要等用户先访问,热点数据更新就是反过来:不等用户访问,先把最常被访问的那些 key 刷一遍。关键在失败时保留旧值——宁可数据旧一点,也不要把空值写进缓存。
热点数据更新
- 步骤 1:从缓存访问统计里取出热点 key 名单,作为主动更新的输入
- 步骤 2:任务按名单逐个回源最新数据,并覆盖写入缓存
- 步骤 3:单个 key 回源失败时保留旧值并记录失败原因,不写入空结果
- 步骤 4:统计每轮更新的成功率与耗时,确认热点数据的新鲜度
关注点:热点名单从哪里来,以及更新失败时旧数据如何继续可用。
缓存预热
预热和热点更新做的事很像,区别在于预热是上线前的一次性动作,范围由「上线后会访问什么」决定。它最大的风险是把上游一次打满,所以批次和节奏都要控制。
缓存预热
- 步骤 1:确定预热的 key 范围,只覆盖上线后会立刻被访问的接口
- 步骤 2:在低峰期按小批次依次回源,成功后写入缓存并设置与平时一致的过期时间
- 步骤 3:预热结束后核对缓存填充率与上游的 QPS 曲线
关注点:预热范围的选择,以及预热对上游造成的压力。
数据清理
清理任务最危险的不是慢,而是删错了。判定口径要写成显式条件并先小批量试跑,同时保证归档还在,即使删错了也能从归档里恢复,不至于让业务数据真的消失。
数据清理
- 步骤 1:按保留策略筛出超过保留期的数据,作为本轮清理对象
- 步骤 2:先归档再删除,或按分区整块下线,避免大批量 DELETE
- 步骤 3:清理前先小批量试跑,确认筛选条件没有误伤有效数据
- 步骤 4:记录本轮清理的行数与耗时,出现异常时用归档数据回滚
关注点:清理任务的判定口径,以及误删之后的兜底手段。
定时任务跑起来之后就没人盯着了,一次静默失败可能几周都没人发现。这一节先给出可追溯的执行日志,再谈失败重试和告警。
任务执行日志
任务日志是定时更新这一章唯一的事后证据。要能回答「昨晚那次预热到底更新了多少、哪几个失败了」,日志里就得有任务标识、处理条目数和失败原因。
任务执行日志
- 步骤 1:在任务入口和出口各记一条日志,写明任务 ID、触发来源和起止时间
- 步骤 2:记录本轮处理的条目数量、成功数、失败数与失败原因
- 步骤 3:把重试次数和最终结论一并写进日志,便于判断是否需要人工介入
- 步骤 4:日志保留足够长的时间,至少覆盖下一个归档周期
关注点:一次任务执行留下了哪些可追溯的证据。
上游数据源不稳定是常态,重试是必须的,但必须有上限。没有上限的重试会把一次小抖动放大成一次雪崩;超过上限之后的兜底策略,其实比重试本身更重要。
失败重试
- 步骤 1:只对可重试的错误(超时、限流、临时 5xx)设置有限次重试
- 步骤 2:重试间隔按指数退避递增,避免故障时把上游压得更狠
- 步骤 3:超过重试上限后保留旧缓存数据并打告警,恢复后核对缓存内容是否完整
关注点:重试次数上限、退避间隔与最终兜底策略。
用户请求
GET https://dashboard.kuaiyizhi.cn/api/v4/weather?city=北京Nginx 负载均衡
Nginx
Public API Gateway
Admin API
Admin Dashboard
MySQL
Redis 集群 + 哨兵
MySQL 分层集群
消息队列
离线数仓
发现内容有问题?
告诉我们哪里需要改进。
