数据库唯一索引仅能防止重复插入,无法保障更新操作幂等性,因update不触发约束;并发select+update易导致状态多次变更;需结合insert...on duplicate key update或redis原子校验实现全链路幂等。

为什么直接用数据库唯一索引无法覆盖所有幂等场景
Go 应用里常见做法是给业务表加 UNIQUE KEY (user_id, order_id) 防止重复插入,但这只在「插入时检测」生效。一旦请求重试发生在更新阶段(比如支付回调多次触发 UpdateOrderStatus),唯一索引就完全失效——因为 UPDATE 不触发表约束,也不会报 Duplicate entry 错误。
更麻烦的是:如果业务逻辑本身需要先查再判再改(比如“未支付才允许支付”),并发请求可能同时通过 SELECT 检查,然后都执行 UPDATE,导致状态被多次修改。
所以纯靠 DDL 级唯一索引只能保插入,不能保状态变更的幂等性。
用 INSERT ... ON DUPLICATE KEY UPDATE 实现原子状态标记
核心思路是:把「是否已处理」这个判断,下沉到数据库写操作本身,靠 MySQL 的原子性保证。
- 建一张轻量幂等表:
idempotent_records,字段为idempotency_key VARCHAR(128) PRIMARY KEY+created_at TIMESTAMP - 每次请求进来,先用
INSERT INTO idempotent_records (idempotency_key) VALUES (?) ON DUPLICATE KEY UPDATE created_at = NOW() - 检查返回的
rowsAffected:若为 1,说明首次写入,可执行主业务;若为 0,说明键已存在,直接返回成功(或跳过)
注意:MySQL 5.7+ 才支持 ON DUPLICATE KEY UPDATE 对主键/唯一键生效;PostgreSQL 要用 INSERT ... ON CONFLICT DO NOTHING,返回值判断逻辑不同(需用 sql.Result.RowsAffected() 或捕获 unique_violation 错误)。
Go 代码中如何安全提取和验证幂等 key
幂等 key 不能只依赖客户端传的 X-Idempotency-Key header,必须绑定业务上下文,否则不同用户用同一个 key 就会互相干扰。
- 推荐组合方式:
sha256(fmt.Sprintf("%s:%s:%s", clientID, actionType, payloadHash)),其中payloadHash是对关键参数(如订单号、金额、币种)做结构化哈希,不包含时间戳、随机数等易变字段 - 在 Gin/echo 中,应在中间件里统一解析并校验:
c.Get("idempotency_key"),失败直接return c.AbortWithStatus(400) - 务必设置 TTL:用
created_at 定期清理,避免表无限增长;或者用 Redis 替代,但要注意 Redis 的 SETNX + EXPIRE 不是原子的,得用 <code>SET key value EX 86400 NX
高并发下唯一索引冲突的错误处理要区分类型
INSERT 失败不等于幂等失败。MySQL 可能返回多种错误:
-
ER_DUP_ENTRY(错误码 1062):正常幂等路径,可忽略并继续走「已存在」逻辑 -
ER_LOCK_WAIT_TIMEOUT(1205)或ER_LOCK_DEADLOCK(1213):是事务锁问题,需重试,不能当成幂等成功 -
ER_DATA_TOO_LONG(1406):key 超长被截断,会导致不同 key 哈希碰撞,必须提前校验长度并拒绝
所以不能简单 catch sql.ErrNoRows 或泛化 error string 匹配,要用 mysql.MySQLError 类型断言,精确识别错误码。这点在用 github.com/go-sql-driver/mysql 时特别容易漏掉。
实际部署时,idempotent_records 表的 idempotency_key 索引一定要是前缀索引还是完整索引,取决于你用的 MySQL 版本和 key 长度——5.7 默认最大前缀 767 字节,若用 utf8mb4,128 字符可能超限,得显式声明 VARCHAR(128) 并确认 innodb_large_prefix=ON。











