• 【幂等幂等幂等,重要的知识说三遍!】常见的九种解决方案汇总


    幂等是什么?

    最核心的概念
    执行一次或多次操作,其产生的影响都是一样的。也就是说,针对同一次操作,请求参数相同,操作最后产生的结果相同。

    简单举例

    • 提交订单操作,用户短时间内点击多次提交,最后产生的订单只有1个,不会重复
    • 用户支付,在支付过程中,无论系统发生了什么异常情况,用户最后只支付一次

    幂等问题是如何产生的?

    1. RPC框架超时重试,比如Dubbo框架的超时重试。为了避免超时而导致无法请求,可以在超时的时候引用重试机制,此时用户的一个操作超时有可能会引发多次重复请求,就会导致幂等问题
    2. 表单重复提交,这是最常见。用户在点击提交的时候,手快点多了几次,如果前端没做好处理,就会发送多次请求
    3. MQ的消费者接受到了重复消费
    4. 业务需求性重试,就比如xxl-job执行任务失败,重新执行节点任务
    5. update累加操作,是非幂等的
    6. insert新增数据,也是非幂等的

    保证幂等的方案有哪些?

    1、前端防抖

    前端防抖是保证幂等最简单的方式,但是其无法完全解决幂等问题,如果有大牛的话还是有办法直接跳过前端防抖进行重复请求的。

    2、Token去重

    Token去重的主要思路其实和分布式锁的思路有点相似,token值一般会被缓存起来,比如缓存到redis中。不过其需要前、后端配合进行操作,大致流程如下:

    1. 前端首先请求后台拿到Token(Token需要保证在分布式的场景下是唯一的),这一步操作需要在进行业务操作前,就比如需要进行一次发帖操作,这个Token获取的操作可以在打开编辑帖子页面时,不要等到用户发送帖子点击提交时才获取Token,否则的话是无法保证幂等的(试想用户连续点击10次提交,每次提交都重新获取这个Token,到后面相当于重复提交了10次请求,这是无法保证幂等的,所以为什么这个token要提前拿)
    2. 拿到Token后,前端可以在提交帖子操作时将token在header中传递给后台。后台校验Token在redis中是否存在,这里要采用delete if exist的方式。存在的话需要delete掉当前的token,然后执行后续的操作,此时已经完成请求去重操作。如果重复请求的话,token当前无法被delete,因为已经被delete过了,因此无法进行重复操作(注意:delete token的时候,一定不能先select再delete,否则会有并发问题。假设并发进行先select再delete,那select了两次,但是delete只发生了一次,那么另外一个没有被delete掉的select token依旧是有效数据,固然会导致并发问题)
    3. delete if exist操作成功,则后台可以执行后续操作;delete if exist失败说明当前这次操作是重复操作,直接返回结果

    为保证原子性,这里删除redis - key可以采用lua脚本来进行操作

    3、悲观锁

    悲观锁的核心思想其实就是在更新数据的时候,保证当前的更新操作是全局唯一的。一般会在MySQL层面采用 for update 的操作,但是这种操作如果不指定主键或唯一id的话,是会锁全表,出大事的!

    “ 由于 InnoDB 预设是 Row-Level Lock,所以只有「明确」的指定主键,MySQL 才会执行 Row lock (行锁) ,否则 MySQL 将会执行 Table Locck(锁表)Lock”

    悲观锁的核心SQL思想是锁行:

    select * from table_1 where id = 10 for update

    在不指定主键的情况下进行查询,锁表极有可能会导致死锁的发生:

    用户A查询表1,然后想访问表2,此时表1是被锁死的;突然,有用户B查询表2,想访问表1,此时表1是锁死的。因为用户B访问的表1已经被锁表了,用户B在等待表1释放表锁;用户A访问的表2也已经被锁表了,用户A在等待表2释放表锁;其互相等待,最终产生死锁

    4、MVCC(版本号机制 & 乐观锁思想、状态机制)

    这里将MVCC、版本号机制、状态机制、乐观锁思想放在一起,其实是因为他们的核心思想太相似了!

    • 版本号机制 & 乐观锁:其实乐观锁会产生ABA问题,为了解决ABA问题,乐观锁可以引用版本号机制来进行处理,来看下面三条SQL来加强一下这方面的思想:

      • 正常的版本号机制解决乐观锁问题(新增一个version字段,更新时需要带上version的条件):update table1 set count = count + 1 where account_id = #{account_id} and version = #{version}
      • 防止超卖的操作,也是基于上面的SQL衍生过来的:update table1 set count = count - #{num} where market_id = #{market_id} and count - #{num} > 1
    • 状态机制在我们的业务开发过程中,再常见不过了:

      • 举例订单状态区分为以下4种(0未下单 1已下单 2已付款 3申请退款 4已退款),其状态是递增的,假设我们需要进行付款操作,我们可以指定订单号在已下单状态下才可以进行更新付款,那么我们的SQL就是:update table1 set order_status = #{has_pay} where order_no = #{order_no} and order_status = #{not_pay}
    5、分布式锁

    分布式锁在业务中经常使用,其相比于token机制来说,纯后台来实现即可,减少了很多没有必要的交互操作。但是分布式锁需要后台维护好唯一的分布式锁的key,比如采用 order_no + user_id 作为key,保证分布式锁的key是唯一的。但是这个分布式锁的粒度不能太大,比如有的通过user_id来加锁(尼玛我真的见过,排查起来真心难,粒度过大摆明了坑人

    具体步骤

    1. 后台获取到能够保证唯一的条件,作为redis的key进行缓存,采用 SETNX 的方式设置redis-key,并且设置过期时间
    2. SETNX 成功,说明是首次操作,后续的逻辑继续进行,记得最后finally处要release掉这个分布式锁
    3. SETNX 失败,说明是重复操作,直接return

    SETNX 其实使用范围还挺广的,譬如发生MQ重复消费的话,我们可以采用 SETNX 来保证消费唯一:
    if(SETNX成功){ 则进行MQ消费 } else { 消费失败,直接return,避免重复消费 }

    6、唯一索引

    这里其实主要是针对MySQL新增唯一索引

    alter table t_table_1 add UNIQUE KEY t_table_1 (order_no);

    当有重复异常时会抛出: Duplicate entry '002' for key 't_table_1.order_no,其实这种方式从最底层的MySQL解决了幂等问题,在业务中也很常用,操作简单且有效。

    7、定义防重表

    定义唯一索引来达到表数据唯一的目的,其实还有一种比较花哨的玩法,那就是定义一张防重表。这种场景大致是出现在,目前我们有业务表A,我们的业务表A无法接受定义唯一索引的操作来完成去重操作。定义防重表的方式其实也很容易。

    基本流程

    1. 假设需要保证唯一的目标表为 表A,其无法、定义一个唯一索引;那么我们定义 表B,表B根据业务需求指定唯一索引
    2. 插入数据的时候,数据都先经过 表B 过滤,判断 表B 插入数据的时候是否会产生 Duplicate 相关的未唯一异常
    3. 倘若 表B 发生了异常,说明当前数据已经是重复数据,直接return
    4. 表B 插入数据时暂未有异常,则可以将当前数据插入到目标表 表A
    8、分区覆盖

    这种场景常见在数仓ETL开发中,大致的思想就是,我们的表数据按天进行分区,并且编写hive-sql的时候需要指定覆盖插入:

    insert into override table xxx partition(date)

    此hive-sql就是定义了一个覆盖写的分区表,插入数据的时候就按照天维度进行插入,即可完成分区覆盖写了(数仓开发保证幂等常用手段)

    9、设计业务唯一数据

    这个思想是在近期在业务开发中在同事中学到的,大致场景就是有一种周报的数据场景,每周会定时推送周报数据给用户,那么定义一张表:用户 id+ 当前周作为唯一标识,周报数据对应一个唯一的周报id,三个字段聚合成一个表(暂称表A);周表详情表(暂称表B)通过周报id关联表A。

    倘若上周周报数据异常,我们可以重新执行周报生成任务,重新给表A中的数据生成周报数据(根据用户id + 周)逐一生成新周报,替换表A中旧的周报id。表B数据继续生成,新的周报增量加到表B中去,旧的周报数据不受任何影响。

    总结

    截至目前为止,常见的九种幂等解决方案列举了出来,其实幂等的解决方案真的可以有很多,针对不同的业务场景和需求使用不同的方式来进行应用,没有绝对之说

  • 相关阅读:
    【沐风老师】3DMAX一键云生成器插件使用教程
    【推荐系统】wss课程-重排序
    游戏开发中,常见的贴图压缩方式
    探索Java设计模式:中介者模式
    【数据结构与算法】顺序表
    jvm中对象内存空间的分配与回收
    realsenseD435i ros auto_exposure设置
    VBA -[知识点]: 字典
    python psutil模块获取系统磁盘|CPU|内存Memory|时区TimeZone等信息
    pytest--fixture的使用(前置、后置)
  • 原文地址:https://blog.csdn.net/qq_43097201/article/details/126818450