我在构建一个不信任 Redis 的作业调度器时的收获

发布: (2026年2月9日 GMT+8 20:01)
6 分钟阅读
原文: Dev.to

Source: Dev.to

《我在构建一个不信任 Redis 的作业调度器时学到的东西》封面图

介绍

它是什么?

Tickr 是一个使用 Go、Redis 和 MySQL 构建的后台任务调度器。
它由一组等待分配任务的工作者组成。任务通过使用 Redis 实现的等待队列和就绪队列流转,并由调度器分配给工作者。所有操作并发执行,且基于事件驱动。

我为什么要构建它?

我已经厌倦了在 Node.js 和 Go 中构建 CRUD 后端 API。我想做点不同的、陌生的东西,这会迫使我思考并发、故障处理和系统行为,而不是仅仅关注端点和模式。

什么在 v1 中坏了?

轮询

在 v1 中,调度器每秒轮询一次以检查是否有作业准备执行。它能工作,但效率低下。我用无缓冲的 Go channel 和阻塞操作取代了轮询——channel 成为项目中最重要的概念之一。

假设

我假设很多东西会自行工作。当这些假设失效时,调试和恢复变得困难。

示例: 如果服务器在作业仍在等待队列中时终止,重启后恢复这些作业会很混乱且不可靠。

Redis 作为唯一真相来源

最初我只依赖 Redis 作为唯一的真相来源。边缘案例测试提出了令人不安的问题:

  • 如果 Redis 在作业仍在排队时崩溃会怎样?
  • 如果 Redis 在崩溃后丢失了状态会怎样?
  • 如果作业中途失败会怎样?
  • 如果我需要已完成作业的日志或历史记录怎么办?

对这些问题的回答需要重新设计,导致了 Tickr v2 的诞生。

Source:

v2 架构

MySQL 作为唯一可信来源

在 v2 中,MySQL 存储完整的作业数据。只有 JobID 和调度信息会被推送到 Redis,Redis 仅用于调度和协作,而不承担持久化职责。

Architecture diagram

这种做法:

  • 防止在崩溃时丢失作业
  • 避免不必要的 MySQL 轮询
  • 允许作业使用轻量级 ID 在队列之间移动
  • 让工作节点仅在需要执行时从 MySQL 获取完整作业数据

调度器与工作节点

调度器运行一个单独的 goroutine(PopReadyQueue),在 Redis 上阻塞等待作业可用。

如果 Redis 断开连接:

  • 调度器会等待 Redis 再次可达
  • 检查 Redis 是否丢失了状态
  • 若状态丢失,则从 MySQL 重建队列

从就绪队列弹出的作业会被发送到一个无缓冲的作业通道,所有工作节点都在监听该通道。当通道中出现作业时,恰好有一个工作节点会接收并执行它。

如果作业失败:

  • 会在延迟后重试
  • 重试次数受限(最多 3 次)
  • 每次尝试的延迟线性递增

处理的边缘情况

Redis 崩溃

当 Redis 挂掉时,调度器会暂停并等待 Redis 恢复可用。Redis 恢复后,调度器会检查状态是否丢失,并在需要时从 MySQL 触发恢复;否则继续正常运行。

逾期任务

如果在任务已排期时 Redis 挂掉,任务的执行时间可能会过去却未被触发。之前这些任务会一直等到有新任务进入队列才会运行。我通过在等待队列重新填充时向调度器发送信号(使用 channel),强制立即重新评估逾期任务,从而解决了该问题。

执行期间关闭

如果服务器在任务执行过程中关闭:

  • 工作线程停止接受新任务
  • 正在执行的任务允许完成
  • 程序在退出前等待工作线程结束

曾出现已完成任务无法在 MySQL 中更新状态的情况,因为全局 context 已被取消。我通过为最终状态更新专门使用一个后台 context 来解决此问题,确保即使在关闭过程中也能正确更新。

我学到的

这个项目与我以前构建的任何东西都非常不同,我非常享受其中的过程。我学到了:

  • 为什么状态的所有权很重要
  • 假设有多么脆弱
  • 如何对失败进行推理,而不是回避它
  • 如何设计能够恢复的系统,而不仅仅是重新启动
  • 如何编写在边缘情况下面不易出错的后端代码

Tickr v2 教会我的后端系统知识,比任何教程都要多。

GitHub 仓库可在此处获取:Tickr

0 浏览
Back to Blog

相关文章

阅读更多 »

Go 模板

什么是 Go 模板?Go 模板是一种在 Go 中通过将数据与纯文本或 HTML 文件混合来创建动态内容的方式。它们允许您替换占位符……