最可靠、最低开销的创建类接口幂等手段是唯一索引+INSERT IGNORE,由数据库原子性拦截重复插入;GORM直接db.Create()并捕获1062/23505错误,返回200/201而非500,严禁先查后插引发竞态。

用唯一索引 + INSERT IGNORE 实现创建类幂等
创建类接口(如下单、发券、注册)最可靠、最低开销的幂等手段,就是靠数据库唯一约束兜底。GORM 不需要先查再插,直接 db.Create(),让 MySQL 或 PostgreSQL 自己拦住重复插入。
常见错误是手写 SELECT ... WHERE order_id = ? 判断是否存在,这在并发下必然产生竞态:两个请求同时查不到,又同时插入成功。唯一索引能原子性地解决这个问题。
- 建表时必须对业务唯一字段加
UNIQUE索引,例如order_id或组合字段(user_id, biz_type, biz_ref) - GORM 中捕获冲突错误:MySQL 用
mysql.MySQLError断言错误码1062;PostgreSQL 用pgconn.PgError.Code == "23505" - 不要把冲突当异常抛出 500,而应返回 200 或 201,并查出已存在记录返回给前端
事务里用 Shared Lock 做状态校验防并发修改
修改类操作(如支付回调、审核通过)不能只靠唯一索引,因为同一笔订单可能被多次回调触发。这时需要在事务中加锁读取当前状态,确保“只允许从 pending → paid”这类单向流转。
GORM 必须显式使用 Clauses(clause.Locking{Strength: clause.LockingStrengthShared}),否则默认快照读(MVCC)不加锁,起不到校验作用。
- Shared Lock 只在事务内生效,
db.Transaction()是必需容器 -
WHERE条件必须走索引,否则 MySQL 可能升级为表锁;可用EXPLAIN确认执行计划 - 避免在锁住的行上做耗时操作(比如调第三方 API),否则会延长锁持有时间,增加死锁风险
Redis Token + GORM 事务外层校验的落地要点
Token 方案适合需要明确拒绝重试、且业务链路耗时可控的场景(如 H5 表单提交)。关键不是“用了 Redis”,而是怎么和 GORM 事务协同不漏判、不误判。
Token 校验必须放在事务最外层,而不是插入 DB 成功之后。否则 DB 写入成功但 Redis set 失败,下次请求因 token 已存在被拒,导致“实际已成功却提示失败”。
- 用
SET key value EX 300 NX,NX保证原子性,EX 300(5 分钟)覆盖最长业务耗时 - Token 建议用
uuid.NewString(),别拼时间戳+随机数——可预测性高、碰撞风险大 - 如果 Redis 不可用,要降级策略:日志告警 + 兜底走 DB 唯一索引,不能直接放行(否则退化成无幂等)
死锁重试必须和幂等逻辑耦合
GORM 事务遇到数据库死锁(MySQL 错误码 1213,PostgreSQL 40P01)时,不能简单重试,否则可能重复扣款、发券。
重试的前提是整个事务逻辑本身幂等:比如用唯一索引插入、用 version 字段乐观更新、或用 Shared Lock 校验后只改状态。否则重试一次就多一笔数据。
- 重试次数建议 ≤ 3 次,间隔用指数退避(如
time.Sleep(time.Millisecond * time.Duration(math.Pow(10, float64(i))))) - 不要在 HTTP handler 层写重试循环,应封装进 service 方法,保持 handler 职责单一
- GORM
Transaction()函数本身不重试,必须手动包裹 for 循环 + 错误类型判断
真正难的不是选哪种方案,而是识别哪条 SQL 实际上在锁什么行、哪个字段缺失索引会导致锁升级、Redis 不可用时业务是否还能自洽——这些细节一旦漏掉,幂等就只是幻觉。











