Redis 可选架构与多协议通信

完结篇:分析 go-ai-scheduler 的 Redis 可选缓存设计和 HTTP/gRPC 双协议透明切换的实现思路。

问题背景

在设计分布式系统时,一个常见的争论是:要不要强制依赖 Redis?

支持强制依赖的论点:

  • Redis 可以大幅降低数据库负载
  • 缓存预热、分布式锁等功能开箱即用

反对强制依赖的论点:

  • 增加了运维复杂度(部署、监控、故障恢复)
  • 小公司/初创团队可能只有 MySQL
  • Redis 故障时系统不应该完全不可用

go-ai-scheduler 选择了一条中间道路:Redis 是可选的。当 Redis 可用时,系统享受缓存加速;当 Redis 不可用时,系统自动降级到直接查询 MySQL。

Redis 的使用场景

在 go-ai-scheduler 中,Redis 被用于三个场景:

1. 到期任务缓存

调度器需要频繁查询"未来一段时间内将要触发的任务"。Redis 使用 Sorted Set 存储任务 ID 和触发时间戳:

// internal/scheduler/cache/manager.go
func (m *Manager) warmDueTasks(ctx context.Context) {
    tasks, err := m.taskRepo.ListDueTasks(ctx, 500)
    ids := make([]int64, 0, len(tasks))
    scores := make(map[int64]float64, len(tasks))
    for _, t := range tasks {
        ids = append(ids, t.ID)
        scores[t.ID] = float64(t.NextTriggerTime.Unix())
    }
    m.redis.WarmDueTasks(ctx, ids, scores)
}

Trigger Loop 扫描时优先从缓存读取:

if l.cache != nil && l.cache.Enabled() {
    cachedIDs, cacheErr := l.cache.GetCachedDueTaskIDs(ctx, time.Now())
    if cacheErr == nil && len(cachedIDs) > 0 {
        // 使用缓存数据
    }
}
if len(tasks) == 0 {
    tasks, _ = l.taskRepo.ListDueTasks(ctx, 100) // 降级到数据库
}

2. Worker 状态缓存

Worker 的心跳数据也可以缓存在 Redis 中,减少数据库写入压力:

func (m *Manager) warmWorkerCache(ctx context.Context) {
    workers, _ := m.workerRepo.ListAvailableWorkers(ctx)
    for _, w := range workers {
        data[w.ID] = map[string]any{
            "hostname": w.Hostname,
            "status":   w.Status,
            "current_load": w.CurrentLoad,
            // ...
        }
    }
    m.redis.WarmWorkerCache(ctx, ids, data)
}

3. AI 查询结果缓存

AI 服务对相同问题的回答可以缓存一段时间,降低 LLM API 调用成本。

优雅降级

Cache Manager 的核心设计是**"无 Redis 也能工作"**:

// internal/scheduler/cache/manager.go
func (m *Manager) Enabled() bool {
    return m.redis != nil
}

func (m *Manager) StartWarmLoop(ctx context.Context, interval time.Duration) {
    if !m.Enabled() {
        return // 无 Redis 时直接退出
    }
    // ...
}

当 Redis 不可用时:

  • Trigger Loop 直接查 MySQL
  • Router 直接从数据库读取 Worker 状态
  • 系统性能可能下降,但功能不受影响

<Callout type="info" title="降级后的性能影响"

在无 Redis 模式下,Trigger Loop 每 5 秒查一次 MySQL。如果任务量很大(数万级别),这种查询会成为瓶颈。建议生产环境部署 Redis,开发测试环境可以只用 MySQL。

HTTP/gRPC 双协议透明切换

go-ai-scheduler 在多个通信场景支持双协议:

通信方向协议选择依据实现位置
Worker → Scheduler(注册/心跳)INTERNAL_PROTOCOL 环境变量HeartbeatClient
Scheduler → Worker(任务分发)Worker 注册的 Protocol 字段Dispatch Client
AI Service → LLM API固定 HTTP/SSELLMAdapter

Scheduler → Worker 的协议切换

Dispatch Client 是双协议设计的典型代表:

// internal/scheduler/dispatch/client.go
func (c *Client) Dispatch(ctx context.Context, worker *model.WorkerNode, req model.ExecuteTaskRequest) error {
    if c.rateLimiter != nil {
        if !c.rateLimiter.Allow() {
            return fmt.Errorf("dispatch rate limit exceeded")
        }
    }
    if strings.EqualFold(worker.Protocol, "grpc") {
        return c.dispatchGRPC(ctx, worker.GRPCAddr, req)
    }
    return c.dispatchHTTP(ctx, worker.CallbackURL, req)
}

Worker 在注册时声明自己支持的协议:

registerReq := apiservice.WorkerRegistrationRequest{
    Protocol:    cfg.InternalProtocol, // "http" or "grpc"
    CallbackURL: "http://127.0.0.1" + cfg.HTTPAddr,
    GRPCAddr:    "127.0.0.1" + cfg.GRPCAddr,
}

Worker → Scheduler 的协议切换

Heartbeat Client 同样支持双协议:

// internal/worker/heartbeat_client.go
func (c *HeartbeatClient) Heartbeat(ctx context.Context, req service.WorkerHeartbeatRequest) error {
    if c.protocol == "grpc" {
        return c.heartbeatGRPC(ctx, req)
    }
    return c.post(ctx, "/api/v1/workers/heartbeat", req)
}

协议选择的权衡

维度HTTPgRPC
连接管理短连接,每次新建长连接,复用
序列化JSONProtobuf
带宽占用高(文本格式)低(二进制)
调试难度低(curl 即可)高(需要 grpcurl)
流式支持SSE / WebSocket原生双向流
跨语言极好好(需要 protobuf 定义)

<Callout type="tip" title="为什么默认是 HTTP?"

HTTP 的调试成本更低。当 Worker 分发失败时,可以直接用 curl 模拟请求排查问题。gRPC 虽然性能更好,但在问题排查时需要额外的工具支持。系统默认 HTTP,gRPC 作为可选优化。

统一请求模型

无论使用哪种协议,任务执行请求使用统一的 Go 结构体:

// internal/model/task.go
type ExecuteTaskRequest struct {
    ScheduleInstanceID string
    TaskID             int64
    TaskType           string
    Payload            string
    Image              string
    TimeoutSeconds     int
    RetryCount         int
    ShardNo            int
    ShardTotal         int
    IdempotencyKey     string
    SchedulerURL       string
}

gRPC 的 proto 定义与这个结构体一一对应,通过生成的代码自动转换:

message ExecuteTaskRequest {
    string schedule_instance_id = 1;
    int64 task_id = 2;
    string task_type = 3;
    string payload = 4;
    // ...
}

这种统一模型让协议切换对业务逻辑完全透明。

系列总结

十篇文章拆解了 go-ai-scheduler 的完整设计:

篇目主题核心设计
1架构概览四服务架构、控制平面强一致、执行平面高自治
2混合调度引擎时间轮 + 最小堆、预热机制
3可靠性设计触发/重试链路、去重幂等、本地缓冲
4Leader 选举etcd/MySQL/Local 三级降级
5路由与背压乐观预留、三态背压、令牌桶
6Worker 高可用心跳、去重、沙箱、断网续传
7LLM Agent10 轮推理、12 工具、SSE 流式
8AI 服务架构建议不决策、自然语言转任务
9可观测性Prometheus + JSON 日志 + Grafana
10Redis 与多协议Optional 缓存、HTTP/gRPC 透明切换

这个项目的核心设计哲学是**"够用就好、逐步演进"**:

  • 不追求最复杂的方案,而是选择工程上可维护的实现
  • 每个依赖都是可选的,系统始终能降级运行
  • AI 是增强而不是替代,核心链路保持确定性

希望这个系列对你的分布式系统设计有所帮助。