系统性解决幂等问题

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

 

 

什么是幂等

 

 

某个操作执行一次,跟执行多次的效果一样。幂等一词来自于数学中的幂等,即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 {             // 稍后重试    }
posted @ 2022-04-28 13:48  piaobodeyun0000  阅读(45)  评论(0编辑  收藏  举报