必须封装成 redisqueue 结构体,因裸调 lpush+brpop 缺失超时控制、错误归一、payload 校验、key 动态注入四大关键点,否则必然丢消息、重复消费或 goroutine 卡死。

直接用 LPush + BRPop 能跑通,但上线后大概率丢消息、重复消费、goroutine 卡死——这不是 Redis 的问题,是你没把“取-处理-确认”这个闭环收进可控结构里。
BRPop 返回值为什么必须取 result[1]?
BRPop 的返回格式固定是 []string{"queue_name", "message_body"}。第一个元素永远是队列名,不是消息内容。写成 result[0] 会静默拿到 key 字符串,后续 JSON 解析直接 panic 或静默失败。
- 必须先检查
len(result) >= 2,空队列超时返回时result可能为nil或空切片 - 建议统一包装为
io.EOF错误:调用方可用errors.Is(err, io.EOF)判断空队列,比字符串比对更健壮 - 别依赖日志里打印的
nameAndData就以为自己取对了——生产环境里错在索引上,查三天都找不到原因
timeout 设为 0 是最危险的默认值
BRPop(ctx, 0, key) 表示无限阻塞。一旦网络抖动、Redis 临时不可达或客户端 goroutine 卡住,consumer 就永远 hang 住,不响应 cancel、不释放资源、不参与 graceful shutdown。
- 必须显式设为正数,比如
30 * time.Second,让ctx超时机制真正生效 - 超时后不要无脑重试:先检查
ctx.Err(),如果是context.DeadlineExceeded,该丢弃或告警;如果是context.Canceled,说明服务正在退出,应立即返回 -
go-redis/v8默认重试 3 次,但BRPop是阻塞命令,重试逻辑不适用——超时必须由上层兜底
为什么必须封装成 RedisQueue 结构体?
裸调命令看着简单,实际缺失四个关键控制点:超时、错误归一、payload 提取校验、key 运行时注入。不包一层,消息丢失和重复消费就是必然。
-
BRPop一返回,消息就从 List 里删了——消费者 panic、kill -9、节点宕机,这条消息就彻底消失 - 结构体字段
key应支持带环境前缀(如"prod:queue:notify"),别硬编码 - 哪怕只加三件事:
timeout显式设、错误统一转io.EOF、payload 提取前做len(result) >= 2校验,就能避开 80% 的线上事故
最常被忽略的一点:List 本身没有 pending list 和 ACK 机制。你得自己在业务层补上“处理成功才算真正消费”,否则崩溃即丢失——这不是优化项,是上线前必须填平的坑。











