下单接口先查 Redis 有没有幂等键,没有就写业务表,写完再 set 键。压测时进程在写表和 set 键之间被杀掉,重放同一请求发现键不存在,又写了一次业务表,出现两笔相同订单。滚动发布后线上也复现了两例。

真正麻烦的地方在于,几个原因的现象几乎一样。幂等标记与业务写入不在同一个事务里;先写业务后标记,中间崩溃就把标记丢了;Redis 与数据库之间没有共同的提交点;重放请求没有携带原始 request_id。

上来先把幂等键落成数据库唯一索引

mysql -e "ALTER TABLE orders ADD UNIQUE KEY uk_req (request_id)" app

接着把业务写入与幂等记录放进同一事务

python -c "print('BEGIN; insert order; insert idem; COMMIT;')"

捕获唯一键冲突后返回已有结果

python -c "print('catch IntegrityError then select existing')"

模拟中途崩溃后重放同一请求

python -c "print('kill -9 between write and set')"

别只看报错消失,验证要多做一步。同一 request_id 连续重放 10 次只产生一条订单,唯一键冲突时返回第一次的结果,不再出现重复扣款。

这里还有个坑。幂等最终要落在数据库唯一约束上,Redis 只能做前置拦截,不能当成唯一的幂等凭据。监控比日志更早给出信号,事后回看经常能提前半小时发现异常。

作者 9823189