【架构实战】RPC超时重试引发重复扣款?基于防重表与分布式锁的幂等性解法
在云原生分布式系统中,网络抖动是常态。当消费者发起一次 RPC 调用后,如果遇到网络超时,底层框架通常会进行静默重试。但如果服务提供方其实已经处理成功,只是响应包在网络中丢失,这层“贴心”的重试就会瞬间变成灾难——导致用户被扣了两次钱,或者生成了两个相同的订单。
标准的云原生工程解法是:放弃“不重试”的幻想,在服务提供端全面实施接口幂等性(Idempotency)改造,依靠“短效拦截 + 长效兜底”双管齐下。
1. 颁发全局唯一业务流水号(BizID)
绝对不要依赖数据库的自增 ID 来做幂等。客户端(或 API 网关)在发起写请求前,必须生成并携带一个全局唯一的流水号(如 req_id: 10086_pay_20261027),整个重试链路必须透传该 ID。
2. 短效拦截:Redis 分布式锁防并发激增
当极其短暂的网络抖动引发瞬间并发重试时,直接让所有请求打到数据库是非常危险的。必须在应用入口使用 Redis 的 SETNX 指令(或 Redisson)构建第一道防线:
// 使用 Redisson 构建防并发重试拦截
String lockKey = "idem:pay:" + request.getBizId();
RLock lock = redissonClient.getLock(lockKey);
// 尝试获取锁,最多等待 0 秒,锁持有 5 秒
if (!lock.tryLock(0, 5, TimeUnit.SECONDS)) {
// 💡 核心:获取锁失败,说明 5 秒内有相同的请求正在处理,直接返回“处理中”
return Result.success("请求正在处理中,请勿重复提交");
}
try {
// 执行核心业务逻辑...
} finally {
lock.unlock();
}
3. 长效兜底:数据库唯一索引(防重表)
Redis 锁只能防并发,如果隔了 10 分钟用户又点了一次提交怎么办?必须依赖关系型数据库的 ACID 特性进行绝对的物理兜底。新建一张 idem_record 防重表,将 biz_id 设置为 UNIQUE INDEX:
-- 在业务 DB 的同一个本地事务中,优先插入防重记录
BEGIN;
INSERT INTO idem_record (biz_id, status) VALUES ('10086_pay_20261027', 'DONE');
-- 如果上一条抛出 DuplicateKeyException,说明已处理过,直接 Rollback 并返回成功
UPDATE user_account SET balance = balance - 100 WHERE uid = 10086;
COMMIT;
总结: 在分布式世界里,重试是保障可用性的利器,而幂等性是驾驭这把利器的剑鞘。通过“Redis 前置挡高并发 + 数据库唯一键兜底强一致性”的组合拳,我们在不大幅修改原有业务逻辑的前提下,为高危写入接口穿上了一层坚不可摧的防弹衣。
- 点赞
- 收藏
- 关注作者
评论(0)