架构师必备:系统性解决幂等问题
要在应用中做到幂等,其实并不难,本文尝试做一个系统性的总结,欢迎一起探讨。
什么是幂等
某个操作执行一次,跟执行多次的效果一样。幂等一词来自于数学中的幂等,即f(f(x)) = f(x)。
需要保证幂等的场景
查询类的读操作,天然是幂等的,多次调用不会有副作用。需考虑以下几种写操作的情况:
- 调用下游写接口
- 写数据库、写Redis等
- 消息订阅和处理
例子:不能给用户重复发放优惠券、现金奖励、通知等,商家更新商品时不能重复增加或减少库存。
下面分别讨论这几种情况。
1、调用下游写接口
主要依靠下游服务保证幂等。
本服务能做的是,在调下游写接口时不做重试,需设置重试次数为0。
2、自己服务保证
2.1 基于状态的幂等
这种情况比较简单,只有当满足前置条件时才允许操作,否则不允许更新(例如已经是终态),直接返回。
例子:订单支付成功后,不允许重复支付。
2.2 基于唯一键的幂等
幂等key的选取
与业务强相关,可以是商品id、订单id、用户id,或者日期等,或者是几个业务字段的组合。
几个例子:
- 一个用户每天只能领一张优惠券,通过 用户id+优惠券类型+日期字符串 即可唯一标识
- B端更新库存,商品id+该商品的版本号
- C端扣库存,订单id
值得注意的是,需要区分新增和修改:修改时的幂等key往往需要带上版本号,才能区分是否同一次修改,每次修改对应一个唯一的版本号。
实现方式
MySQL表中为幂等key建立唯一索引:强幂等,例如资金、订单,绝对不允许重复处理,当插入重复数据时报错。
不推荐用Redis实现幂等,一旦Redis出问题,比如节点宕机,可能出现2个client同时获取到锁的情况。
MySQL幂等伪代码:
插入重复记录,捕获异常,提示幂等拦截。
try {
// 插入记录
someDao.create(someRecord);
} catch (DataIntegrityViolationException e) {
// 如果是重复记录,返回异常
return failResponse("幂等拦截");
} catch (Throwable t) {
// 异常处理
return failResponse("其他异常");
}
3、消息订阅和处理
MQ通常会保证消息至少发送一次(可能多次),并且在机器实例重启或发版时,consumer group会做rebalance,进而收到重复的消息。因此,消息的幂等处理必不可少。
实现方式:
在处理消息前加上Redis锁:如果上锁成功,则继续处理,否则稍后重试。
- setnxex,不存在时才设置,时效即为锁的租期,否则忽略
- 接下来的业务处理,如果是自身逻辑需要强幂等则使用上述数据库幂等方式,如果全部依赖下游则依赖下游实现幂等
Redis幂等伪代码:
// 生成幂等key
String redisKey = buildRedisKey();
// 上Redis锁,租期为leaseTime
if (redisLock.tryLock(redisKey, leaseTime)) {
// 业务逻辑处理
} else {
// 稍后重试
}