幂等性问题概述

166 阅读4分钟

这是我参与11月更文挑战的第9天,活动详情查看:2021最后一次更文挑战

  • 问题场景

    • 重试多次执行幂等

        1. 前端页面重复提交表单。
        1. 服务间调用有超时重试机制。请求已经执行成功了,但是没有收到返回结果,然后调用再重试了一下。(dubbo就会默认重试2次)
        1. mq消费消息的时候,可能读到重复消息。
    • 高并发下幂等

      • 并发调用一个接口N次,比如超买超卖。这个其实更多属于高并发问题,而不是幂等问题。
  • 影响结果(幂等最终的表象)

    • insert操作,结果存在多条。

    • update操作,单纯的set一般没有问题,但是如果涉及计算set=x+y就会有问题。比如一个按钮是给人工作+500,错误的按多次就会加超过500。

  • 解决方法(和网上很多的思路可能分析的不一样)

    • 前端保证幂等

      • 按钮点击一次后,变为不可点击。

      • Post-Redirect-Get:客户提交表单之后,重定向到另一个页面。方式客户F5刷新再次提交。

    • 依据上面的场景,其实可以分为2种情况,不同的情况可以采取对应的方式来解决

      • insert场景下,防止避免重复插入(以上这些能保证的是重复insert,以及部分高并发下的update!对于那种重试的请求并不能解决!)

        • 添加唯一索引

          • 缺点:表中数据并非要唯一,只是特殊场景下需要这个操作是唯一的就不能满足了。
        • insert前面先select(不推荐)

          • 主要针对insert的场景,并且并发低的场景。高并发下,insert和select时间差内可能依然插入多条。
        • 悲观锁(不推荐)

          • 上面高并发下无法使用,可以通过select * from user id=123 for update,查询要更新的数据前,先锁住数据,然后更新。

          • 缺点:select和insert必须在一个事务中提交,相当于把请求串行化。如果事务执行时间长,会造成大量的请求等待,影响接口性能。

        • 对于这种insert的,一般做法:先select,如果有就直接返回。如果没有,加上分布式锁,再查询下,然后执行insert。(像单例模式一样)

      • update场景下,避免错误的重复更新

        • 乐观锁

          • 表中加version字段,更新前先查出来,然后更新的时候,带着version去更新。查上次version的并update新的version+=1。
        • 状态机

          • 订单的状态,按照支付中,待支付这些状态来判断是否需要执行。
      • 两者皆通用的方法:

        • 加入防重表(相当于Mysql分布式锁)--- 最常用,可以做到对业务代码的零侵入。

          • 将请求的参数拼接为一个value值,然后防重表该字段唯一索引。

          • 每次执行操作的时候,先尝试插入表(可以是redis)。如果成功了才能继续往下执行。

          • 注意点:防重表的插入逻辑和后面的更新逻辑,必须要在一个事务内。

          • 具体实战做法:AOP+注解的方式,对需要幂等的接口,拦截并处理幂等问题。

        • 全局唯⼀号实现幂

          • 每个请求都有一个唯一的id号,每次请求都带上唯一号。可以客户端自己生成(缺点就是有些情况下,比如用户刷页面,无法每次都生成一样的id)。
        • 基于token机制(和防重表以及全局唯一唯一号实现方式性质都是一样的)

          • 调用方先向服务端获取一个token,服务方将该token放到redis里面,当接口调用完毕,从redis删除token。如果第二次调用发现redis没有token,那么就直接返回重复调用。(缺点:需要多一次请求token)

          • 问题

            • 先删除token:业务未能成功处理,应该执行重复调用的,但是因为没有token了,导致后续调用无法执行。(推荐 - 业务出现执行错误本身就应该抛出来,然后让调用方再次获取token后执行)

            • 后删除token:业务还在处理中,接口就会重复调用。(这个没办法防止重复,所以不能后删除,并且后删除还有其他的问题)