幂等性的相关知识

什么是幂等性

幂等(Idempotent)是一个数学与计算机学的概念,常见于抽象代数中。

f(n) = 1^n // 无论n等于多少,f(n)永远值等于1

在编程中一个幂等操作的特点是其任意多次执行所产生的影响均与一次执行的影响相同。幂等函数,或幂等方法,是指可以使用相同参数重复执行,并能获得相同结果的函数。这些函数不会影响系统状态,也不用担心重复执行会对系统造成改变。例如,“setTrue()”函数就是一个幂等函数,无论多次执行,其结果都是一样的,更复杂的操作幂等保证是利用唯一交易号(流水号)实现.

为什么需要幂等性

举个最简单的例子,那就是支付,用户购买商品后支付,支付扣款成功,但是返回结果的时候网络异常,此时钱已经扣了,用户再次点击按钮,此时会进行第二次扣款,返回结果成功,用户查询余额发现多扣钱了,流水记录也变成了两条。在以前的单应用系统中,我们只需要把数据操作放入事务中即可,发生错误立即回滚,但是再响应客户端的时候也有可能出现网络中断或者异常等等。

那些场景需要幂等性

1.前端重复提交选中的数据,后台也只会产生对应这个数据的一个反应结果。

2.用户发起一笔付款请求,就应该只扣用户一次钱,即使遇到网络重发或系统bug重发请求,也应该之扣一次钱。

3.发送验证短息也应该只发一次,同样的验证短信不应该发送多次。

4.创建业务订单,一个业务请求只能创建一个业务订单,创建多个就会出大问题。

HTTP的幂等性

  1. GET:只是获取资源,对资源本身没有任何副作用,天然的幂等性。
  2. HEAD:本质上和GET一样,获取头信息,主要是探活的作用,具有幂等性。
  3. OPTIONS:获取当前URL所支持的方法,因此也是具有幂等性的。
  4. DELETE:用于删除资源,有副作用,但是它应该满足幂等性,比如根据id删除某一个资源,调用方可以调用N次而不用担心引起的错误(根据业务需求而变)。
  5. PUT:用于更新资源,有副作用,但是它应该满足幂等性,比如根据id更新数据,调用多次和N次的作用是相同的(根据业务需求而变)。
  6. POST:用于添加资源,多次提交很可能产生副作用,比如订单提交,多次提交很可能产生多笔订单。

幂等性有什么缺点

1.增加了代码复杂度

2.并行改成了串行

3.降低了代码的执行效率

如何保证幂等性

1. 唯一约束方式

对于任何一个数据表,就是根据业务的操作和内容生成一个全局ID,在执行操作前先根据这个全局唯一ID是否存在,来判断这个操作是否已经执行。如果不存在则把全局ID,存储到存储系统中,比如数据库、redis等。如果存在则表示该方法已经执行。也利用数据库唯一索引特性,保证数据唯一。

唯一性token:

function getToken() {
    $charId = strtoupper(md5(uniqid(mt_rand(), true)));
    return substr($charId, 0, 8) . substr($charId, 8, 4) . substr($charId, 12, 4) . 
        substr($charId, 16, 4) . substr($charId, 20, 12);
}

唯一性id:

/**
     * 生成一个唯一分布式UUID,根据机器不同生成. 长度为18位。
     * 机器码(2位) + 时间(12位,精确到微秒)
     * @return mixed
     * @throws HeroException
     */
    public static function genGlobalUid() {

        $lock = SynLockFactory::getFileSynLock(self::UUID_LOCK_KEY);
        $lock->tryLock();
        usleep(5);
        //获取服务器时间,精确到毫秒
        $tArr = explode(' ', microtime());
        $tsec = $tArr[1];
        $msec = $tArr[0];
        if ( ($sIdx = strpos($msec, '.')) !== false ) {
            $msec = substr($msec, $sIdx + 1);
        }

        //获取服务器节点信息
        if ( !defined('SERVER_NODE') ) {
            $node = 0x01;
        } else {
            $node = SERVER_NODE;
        }
        $lock->unlock();

        return sprintf(
            "%02x%08x%08x",
            $node,
            $tsec,
            $msec
        );
    }

数据库索引:例如,唯一索引(UNIQUE KEY)

2.状态机

很多业务中多有多个状态,比如订单的状态有提交、待支付、已支付、取消、退款等等状态。后端可以根据不同的状态去保证幂等性,比如在退款的时候,一定要保证这笔订单是已支付的状态。例如symfony工作流,状态机是工作流的子集(subset),状态在不同的情况下会发生变更,一般情况下存在有限状态机,这时候,如果状态机已经处于下一个状态,这时候来了一个上一个状态的变更,理论上是不能够变更的,这样的话,保证了有限状态机的幂等。

'transitions' => [
            //从申请订单流转到分配跟单
            '1_2_success' => [
                'from' => OrderFlowConfig::PURCHASE_APPLY_ORDER,
                'to' => OrderFlowConfig::PURCHASE_DIRECTOR_VERIFY
            ],
            //从分配跟单流转到中心审批
            '2_3_success' => [
                'from' => OrderFlowConfig::PURCHASE_DIRECTOR_VERIFY,
                'to' => OrderFlowConfig::PURCHASE_CENTER_VERIFY
            ],
            //从中心审批流转到下载盖章
            '3_4_success' => [
                'from' => OrderFlowConfig::PURCHASE_CENTER_VERIFY,
                'to' => OrderFlowConfig::PURCHASE_DOWNLOAD_SEAL
            ],
            //从下载盖章流转到申请付款
            '4_5_success' => [
                'from' => OrderFlowConfig::PURCHASE_DOWNLOAD_SEAL,
                'to' => OrderFlowConfig::PURCHASE_APPLY_PAY
            ],
  ...

3. token机制

针对客户端连续点击或者调用方的超时重试等情况,例如提交订单,此种操作就可以用Token的机制实现防止重复提交。

TOKEN机制如何实现?简单的说就是调用方在调用接口的时候先向后端请求一个全局ID(TOKEN),请求的时候携带这个全局ID一起请求,后端需要对这个全局ID校验来保证幂等操作,流程如下图:
![在这里插入图片描述](https://img-blog.csdnimg.cn/20210127144601862.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3lpeGluX3N1b3poYW5n,size_16,color_FFFFFF,t_70#pic_center
主要的流程步骤如下:
在这里插入图片描述

  1. 客户端先发送获取token的请求,服务端会生成一个全局唯一的ID保存在redis中,同时把这个ID返回给客户端。
  2. 客户端调用业务请求的时候必须携带这个token,一般放在请求头上。
  3. 服务端会校验这个Token,如果校验成功,则执行业务。
  4. 如果校验失败,则表示重复操作,直接返回指定的结果给客户端。

通过以上的流程分析,唯一的重点就是这个全局唯一ID如何生成,在分布式服务中往往都会有一个生成全局ID的服务来保证ID的唯一性。

4. 悲观锁和乐观锁

悲观锁

悲观锁主要分为共享锁和排他锁:

  1. 共享锁:【shared locks】又称为读锁,简称S锁。顾名思义,共享锁就是多个事务对于同一数据可以共享一把锁,都能访问到数据,但是只能读不能修改。
  2. 排他锁:【exclusive locks】称为写锁,简称X锁。顾名思义,排他锁就是不能与其他锁并存,如果一个事务获取了一个数据行的排他锁,其他事务就不能再获取该行的其他锁,包括共享锁和排他锁,但是获取排他锁的事务是可以对数据行读取和修改。
  3. 说明: 悲观并发控制实际上是“先取锁再访问”的保守策略,为数据处理的安全提供了保证。但是在效率方面,处理加锁的机制会让数据库产生额外的开销,还有增加产生死锁的机会。另外还会降低并行性,一个事务如果锁定了某行数据,其他事务就必须等待该事务处理完才可以处理那行数据。
乐观锁

乐观锁是相对悲观锁而言的,乐观锁假设数据一般情况下不会造成冲突,所以在数据进行提交更新的时候,才会正式对数据的冲突与否进行检测,如果发现冲突了,则返回给用户错误的信息,让用户决定如何去做。乐观锁适用于读操作多的场景,这样可以提高程序的吞吐量。

乐观锁机制采取了更加宽松的加锁机制。乐观锁是相对悲观锁而言,也是为了避免数据库幻读、业务处理时间过长等原因引起数据处理错误的一种机制,但乐观锁不会刻意使用数据库本身的锁机制,而是依据数据本身来保证数据的正确性。乐观锁的实现:

  1. CAS实现:Java 中java.util.concurrent.atomic包下面的原子变量使用了乐观锁的一种 CAS 实现方式。
  2. 版本号控制:一般是在数据表中加上一个数据版本号 version 字段,表示数据被修改的次数。当数据被修改时,version 值会+1。当线程A要更新数据值时,在读取数据的同时也会读取 version 值,在提交更新时,若刚才读取到的 version 值与当前数据库中的 version 值相等时才更新,否则重试更新操作,直到更新成功。
  3. 说明 乐观并发控制相信事务之间的数据竞争(data race)的概率是比较小的,因此尽可能直接做下去,直到提交的时候才去锁定,所以不会产生任何锁和死锁。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值