go用redis实现消息幂等性必须结合状态机、唯一业务键和db兜底,仅靠setnx会导致状态不一致;需用lua脚本维护pending/success/failed三态,客户端生成带语义的biz_id,db加联合唯一索引作为最终防线。

Go 用 Redis 实现消息幂等性,核心不是“存个 ID 就完事”,而是必须配合状态机 + 唯一业务键 + DB 兜底,否则在节点重启、缓存击穿或网络分区时必然漏判重复。
redis.SetNX() 为什么不能直接用于幂等判定
很多人写 redis.SetNX(ctx, key, "1", expiration) 然后判断返回值就放行,这在单次请求下看似有效,但实际会出问题:
- 它只表示“这个 key 第一次被设成功”,不等于“这个操作第一次被执行完成”——中间若 panic、超时、DB 写失败,key 已存但业务未成功,后续请求会被错误拒绝
- 没有状态区分:无法知道上次是
success、failed还是processing,导致失败重试被拦住,或超时后重复执行 - Redis 单命令原子性 ≠ 业务流程原子性;
SetNX成功后若服务宕机,状态就卡在processing,既不回滚也不释放
必须用 Lua 脚本维护三态状态机
推荐在 Redis 中用一个 key 存 JSON 字符串或哈希字段,包含 status("pending"/"success"/"failed")、result(可选)、updated_at。所有读写都走 Lua 脚本保证原子性:
local key = KEYS[1]
local status = ARGV[1]
local result = ARGV[2]
local ttl = tonumber(ARGV[3])
if redis.call("EXISTS", key) == 0 then
redis.call("HSET", key, "status", status, "result", result, "updated_at", ARGV[4])
redis.call("EXPIRE", key, ttl)
return "first"
else
local cur_status = redis.call("HGET", key, "status")
if cur_status == "success" then
return {cur_status, redis.call("HGET", key, "result")}
elseif cur_status == "failed" and status == "pending" then
redis.call("HSET", key, "status", status, "updated_at", ARGV[4])
return "retry"
else
return cur_status
end
end
Go 调用时传入 status 和当前时间戳,脚本统一处理状态跃迁逻辑,避免应用层“查-判-写”竞态。
Redis 缓存和数据结构管理技能。通过自然语言操作 Redis,支持 String、Hash、List、Set、ZSet、Stream 等数据结构操作。当用户提到 Redis、缓存、消息队列、会话存储时使用此技能。
业务 ID 必须由客户端生成且带语义
服务端生成 uuid.New() 或 time.Now().UnixNano() 无法支撑重试场景——上游超时重发时拿不到原 ID,等于放弃幂等。正确做法是:
- HTTP 场景:强制客户端在 header 中传
X-Request-ID,或 body 里带biz_id字段(如pay_20260714_8892345) - gRPC 场景:用
metadata.MD{"biz_id": ["xxx"]}透传,拦截器统一注入到context.Context - ID 需含业务类型前缀和可追溯字段(用户ID/订单号),例如
order_create:u12345:20260714001,防止不同业务共用同一 ID 空间造成冲突
数据库唯一索引是最后一道不可绕过的防线
Redis 故障、过期、集群分片迁移都可能导致 key 丢失。关键表必须加联合唯一索引,且字段要覆盖业务语义:
- 错例:
UNIQUE KEY `uk_ref_id` (`external_ref_id`)—— 同一用户反复提交不同订单仍能通过 - 正例:
UNIQUE KEY `uk_user_order_type_ref` (`user_id`, `order_type`, `external_ref_id`) - Go 代码中捕获
mysql.ErrDuplicateEntry或 PostgreSQL 的unique_violation错误,转为http.StatusConflict或幂等成功响应,而不是打日志后吞掉 - 绝不写
SELECT ... FOR UPDATE再INSERT—— 分布式下查与插之间存在窗口,必然脏写
真正难的不是写对那一行 redis.SetNX,而是把客户端 ID 透传链路、Redis 状态机、DB 唯一约束、超时后主动查状态这四件事全串通且不出时序漏洞。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!










