重复消费问题一直是一个热点问题,不管是面试还是实际工作过程中都会遇到,今天我就盘一下这个问题。
1. 重复消费是怎么出现的
重复消费的问题出现的情况有很多,我列举一下常见的吧:
-
用户重复提交表单。
-
用户使用软件恶意刷单。
-
消息由于失败,但是并没有失败,存在重试机制导致重复消费。
-
MQ 中的经典重复消费问题。
2. 那怎么解决呢?
解决重复消费问题,就需要保证一个特性,那就是幂等性,即调用一次请求的结果和调用多次相同请求的结果是一样的。
一般情况下,像查询业务,删除业务这些都是幂等的,而用户下单和退款业务这些不是幂等的。
保证幂等性有以下解决方式:
-
用户提交表单后,让提交按钮变为 loading 标志确保不能重复点击,同时跳转到一个提交成功的页面。
-
去重,每条消息都有一个唯一标识,通过判断标识是否存在从而去重,这样就能避免处理已经处理过的消息。
-
保证接口的幂等性,通过先查再更新的方式修改数据库记录。比如消息有一个唯一标识,那么先去数据库中查找是否有这个标识,如果没有再添加;也可以根据业务来,比如支付业务,先去查对应的支付订单是否为未付款,如果是则进行扣款并更新订单状态。
3. 详细说一下去重
上面提到的解决方式中1,3点都是老生常谈了(个人拙见),我重点说一下去重有哪些方案吧。
1.Redisson 客户端的布隆过滤器
它的底层数据结构是一个 bitmap(位图),里面存放的是二进制0或者1。刚开始全是0,当一个key来了之后先经过3次hash运算,模于数组长度找到对应的数组下标,把对应的0改为1。这样的话,三个数组的位置就能标明一个key的存在。查找的过程也是一样的。
但是它存在一定程度的误判,比如一个 key 过来布隆过滤器判断它存在于 bitmap 中,但是实际上不存在。我们可以通过增加数组的长度或者增加 hash 计算的次数从而减少误判率,但是这也会带来一定的内存和性能开销,实际应用中需要结合业务预估误判率,并且我们需要对误判的情况进行兜底操作,比如查询数据库中是否存在相应的记录。
-
业务代码
2. 创建自定义注解实现去重
可以自定义一个幂等注解,然后配合 AOP 进行方法拦截,对拦截的请求信息(包括ip+方法名+参数名+参数值)根据固定的规则去生成一个 key,然后调用 redis 的 setnx 方法,如果返回 ok,则正常调用方法,否则就是重复调用了。这样可以保证重复请求接口在一定时间内只会被成功处理一次。至于锁的有效时长要根据业务情况而定的。
-
创建幂等性注解
-
获取 IP 的工具类
-
创建幂等性切面
上述解决调用的重复问题,核心是找到一个唯一的标识,从而判断是否为重复操作。