【设计】幂等性[2] 借鉴设计

108 阅读6分钟

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

他山之石

那么,我们不妨来看看为了保证线程安全,常见的解法有哪些:

  • 悲观锁 - 这个没什么好说的,上锁呗,拿不到锁?要么等着,要么走

  • 乐观锁 - 这里就有2个解决方法(其实MVCC我觉得更类似于CAS的一种实现):

    • CAS:通过比较当前值和我预期的更新前的值,来确定我是否能进行变更,常见的就是JDK的JUC中的AQS以及依赖AQS实现的容器

      • 版本号:CAS比较麻烦的一个地方,就是ABA问题;其实现在数据存储便宜了,我们就可以加个类似于更改次数的东西,修改之前先查一下改了多少次,最后准备改的时候再一比,如果这俩不一样那就说明不能改了,否则修改,并且让版本号+1。其实也是CAS的一种实现,只是通过版本号的控制,规避了ABA的问题。

可以攻玉

既然这俩面对的困境类似,那么看看如何照抄了。

悲观锁

我们知道这老哥最可靠了:就一个可以拿锁。

这样至少可以保证,多请求条件下,我们同一时间只有一个请求可以被执行

但这样也并不能完全解决问题:如果执行完了,我们却再来执行一次,这时候加锁就没用了,难道上一辈子的锁吗?这里我们就得加上使用版本号类似的机制了。

乐观锁

这里我们要设置个状态链路,总之是一路往前不能后退的,例如上面提到的版本号,也可以是不能回退的状态链路。

方法

那么我们就可以有个思路了:

  • 带个状态申请悲观锁,状态在后面了?那就回去吧,没你啥事了

  • 悲观锁被锁了?那也不执行你了,你回去或者等着都行

  • 拿到锁了:再检查一下状态,如果状态还是被改了:这代表前面有人给你搞定了,那就释放锁回去了

    • 这里就是线程安全里DCL的做法
  • 状态和我带的一样:那我就干活了呗

  • 活干完了:改状态,改完就释放锁了

落地

经过上面一同分析,思路有了,那么我们怎么做呢?

悲观锁

首先是上锁:

  • 这个大家轻车熟路了,无非是:

    • 土一点,我就一台机器,那就多线程那套,上锁!

    • 我多机,县官还能现管?那就公共数据源来查:

      • 能用同一个库,我也没redis啥的,那就数据库加个记录上锁呗,记得加个过期时间咯
      • 不能用一个库/我有redis:那好说,上分布式锁呗

这里就得保证锁的是一个玩意了,我们可以用唯一键(可以是某个字段,也可以是组合的一些字段,也可以是token之类的东西)来保证。

乐观锁

根据上面的,直接照搬最简单的就是给具体的记录加个不可回退的标记了。

加了标记之后我们就可以通过版本控制和DCL,来帮助上锁过程了。

但这里我们就会发现额外的问题:

  • 有记录的话,当然可以控制整个流程了
  • 但是,如果没有记录,这问题就大了,第二个请求进来了并不会认为自己在这个scope里,我给你再来条记录,这咋整?

因此,我们的请求就得带点其他的东西来保证没有数据的时候(也就是说插入的时候),不会发生这种问题,这里就有几种办法:

分布式id
  • 我们对表做一下改造:新增一列叫分布式的id,这个就得是全局唯一的了

  • 请求方:

    • 在请求之前,去获取一下这个分布式id
    • 请求方请求的时候,带上这个id,以及我预期此时的状态(其实这里一般在接收方接口中就直接处理掉了,因为如果要满足幂等性,外部的状态其实不能算准,我们要按照预期在这里是什么状态,来反推接收方请求的时候,预期是什么状态)
  • 处理方:

    • 接收到请求了,先数据库捞一把:这个分布式id有没有,状态对不对的上
    • 分布式id,或者状态也对不上,那就回去吧
    • 都对上了,那就请进吧,数据库里的记录把这个分布式id再插上去,后面的拿着这个分布式id就进不去了

这里就得保证这个分布式id是唯一的了,一般来说会有一个第三者(当然也可以是处理方给这个id)来担当这个造id的角色:根据一些唯一键,来生成对应的id。

其实这里就可能会涉及到额外问题了,我们假设以下场景:

  • 用户下了订单A,此刻订单还未创建,我们来走流程

如果这部分我们用了消息队列,那么就会有这么一个问题:

订单要唯一吧?那你怎么生成唯一id?

  • 范围大一点,用户信息加商品信息生成一个:那用户一个商品不得一辈子只能下一个单啊,做不做生意了?
  • 上面的加时间生成一个:那不得单机啊,多机同时去申请分布式id,时间难免不一样,不控制的话,那不得多个订单?买个东西多个订单,该滚蛋了吧

一种比较合适的方法是:

  • 上分布式锁,让同一个请求,只能获取一个分布式id,后面就采用第二个方案,方法就如下:

    • 下单如果是分布式处理的情况,那么大概率是从MQ里拿请求了,MQ生产者此时就得去生成一个唯一id,这个就不能重复了吧?
    • 然后再第二个方案。

    不难发现:还是唯一id,这里也能看出来:

    • 哦,幂等性原来也可以分层的啊(如果我们采取了上面的方案,那么就有2个地方用到唯一id了,那么其实是不是只需要有一个地方申请就可以了?),最后总归是要到单机上的,毕竟一个发往确定服务器的确定的请求,终归会在某个步骤只有一台机器在执行嘛。
token机制

其实这里的token和上面的分布式id有异曲同工之妙,只是token作用的地方就属于悲观锁那部分的了。