Redis Streams 是 Go 中最实用的轻量级分布式队列方案:支持消费者组、ACK、水平扩展;需注意 XADD 限长、XGROUP 预创建、XCLAIM 处理 pending 消息。

用 Redis 实现 Go 分布式队列最轻量可行
直接上结论:Go 本身没有内置分布式队列,但用 redis + github.com/go-redis/redis/v9 是最简单、低侵入、能跨进程/机器协同的方案。别碰自建 TCP 服务或 ZooKeeper 集成——除非你明确需要事务性或严格顺序保证。
核心思路是用 Redis 的 LPUSH/BRPOP(或 RPOPLPUSH)实现生产者-消费者模型,配合原子操作避免重复消费。
-
LPUSH queue_name job_json:生产者推任务(JSON 字符串) -
BRPOP queue_name 0:消费者阻塞取任务(0 表示永不超时) - 取到后立即
RPOPLPUSH queue_name processing_queue进入处理中状态,防止崩溃丢失 - 处理成功再
LREM processing_queue 1 job_json或用独立 key 记录完成状态
为什么不用 channel 或 sync.Mutex 做“分布式”
Go 的 chan 和 sync.Mutex 只在单进程内有效。两个 Go 进程(哪怕在同一台机器)之间无法共享内存,更别说跨机器。试图用它们模拟分布式队列,结果只会是:本地测试全过,一上多实例就丢任务、重复执行、死锁。
常见错误现象包括:
- 多个 worker 同时从同一个
chan读到同一份任务 - 用文件或本地 SQLite 当“队列”,并发写入导致数据损坏
- 用全局变量 +
sync.Map,但只在单个进程里生效,集群中各节点互不知情
BRPOP 阻塞消费要注意超时和连接复用
BRPOP 看似简单,但实际部署常因网络抖动或 Redis 重连失败导致消费者卡住或 panic。必须显式控制上下文和连接生命周期。
- 永远用带 timeout 的
context.WithTimeout(ctx, 30*time.Second)包裹client.BRPop调用 - 不要每次消费都新建
redis.Client,复用全局单例(client := redis.NewClient(...)),并调用client.Ping()做启动时连通性检查 - 遇到
redis.Nil错误(超时无数据)要主动 continue,而不是 panic 或退出 goroutine - 如果 Redis 断连,
client.BRPop会返回redis: connection closed,需捕获并重试(建议指数退避)
示例片段:
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
val, err := client.BRPop(ctx, 0, "my_queue").Result()
if err == redis.Nil {
// 无任务,继续轮询
return
}
if err != nil {
log.Printf("BRPOP failed: %v", err)
return
}
jobData := val[1] // BRPOP 返回 [key, value]
消息幂等性和失败重试不能靠队列本身解决
Redis 队列不提供“至少一次”或“恰好一次”语义。它只负责传递,不保证处理结果。业务逻辑出错(如 HTTP 超时、DB 写失败)后,必须由应用层决定是否重推、进死信、还是丢弃。
关键实操点:
- 每个任务 JSON 中必须含唯一
id字段(如 UUID),用于下游幂等判断(例如插入前查 DB 是否已存在该id) - 不要依赖 Redis 的
TTL自动清理失败任务——TTL 是针对 key 的,不是单条 list 元素 - 建议额外维护一个
failed_jobs:job_id的 set,记录失败超过 3 次的任务,供人工干预 - 若需重试,用
LPUSH推回原队列,但加延迟:先SETEX retry_job_id 60 "job_json",另起 goroutine 60 秒后LPUSH
真正难的从来不是“怎么把消息塞进 Redis”,而是“怎么确认它被正确且仅执行了一次”。这部分逻辑必须写在你的 handler 里,而不是指望中间件。


















