架构师必备:系统性解决幂等问题

要在应用中做到幂等,其实并不难,本文尝试做一个系统性的总结,欢迎一起探讨。

什么是幂等

某个操作执行一次,跟执行多次的效果一样。幂等一词来自于数学中的幂等,即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 {
        // 稍后重试
    }
    

你可能感兴趣的