• 并发扣款一致性,幂等性问题,这个话题还没聊完!!! --- 架构师之路


    《并发扣款,如何保证数据的一致性?》,分享了同一个用户并发扣款时,有一定概率出现数据不一致,可以使用CAS乐观锁的方式,在不降低吞吐量,并且只有少量修改的情况下,保证数据的一致性。

    文章发布不到24小时,就有近200的评论。

    其中,问的比较多的是ABA问题,这个问题已经在《并发扣款一致性优化,CAS下ABA问题,这个话题还没聊完!!!》中扩展。

    其次,问的比较多的是作业题,为什么一定要用select&set的方式进行余额写回:

    UPDATE t_yue SET money=$new_money WHERE uid=$uid AND money=$old_money;
    

    为什么不能采用直接扣减的方法:

    UPDATE t_yue SET money=money-$diff WHERE uid=$uid;
    

    很人说,在并发情况下,会将money扣成负数。

    为了保证余额不被扣成负数,再加一个where条件:

    UPDATE t_yue SET money=money-$diff WHERE uid=$uid AND money-$diff>0;
    

    这样是否可行?
    画外音:额,撇开业务不谈,这个SQL用列做运算,其实是不好的,建议使用:

    UPDATE t_yue SET money=money-$diff WHERE uid=$uid AND money>$diff;
    

    很遗憾,仍然不行。原因在《并发扣款,如何保证数据的一致性?》一文里点赞最多的评论,不幂等。
    画外音:说明绝大部分同学,能够回答正确作业。

    聊幂等性之前,先看另一个测试用例的case。

    假设有一个服务接口,注册新用户:

    bool RegisterUser($uid, $name){
    
             //查看uid是否已经存在
    
             select uid from t_user where uid=$uid;
    
             //不是新用户,返回失败
    
             if(rows>0)return false;
    
             else{
    
                       //把新用户插入用户表
                       insert into t_user values($uid, $name);
                       //返回成功
                       return true;
             }
    }
     
    有一个测试工程师,对该接口写了一个测试用例:
    bool TestCase_RegisterUser(){
    
             //造一些假数据
    
             long uid=123;
    
             String name='shenjian';
    
             //调用被测试的接口
    
             bool result= RegisterUser(uid,name);
    
             //预期注册成功,对结果进行断言判断
    
             Assert(result,true);
    
             //返回测试结果
    
             return result;
    
    }
    

    这是不是一个好的测试用例?
    这个用例存在什么问题?
    你会发现,相同条件下,这个测试用例执行两次,得到的结果不一样:
    (1)第一次执行,第一次造数据,调用接口,注册成功;
    (2)第二次执行,又造了一次相同的数据,调用接口,注册会失败;
    这不是一个好的测试用例,多次执行结果不同。

    什么是幂等性?
    相同条件下,执行同一请求,得到的结果相同,才符合幂等性。
    画外音:Google一下,比我解释得更好,但意思应该说清楚了。

    如何将上面的测试用例改为符合“幂等性”的测试用例呢?

    只需要加一行代码:

    bool TestCase_RegisterUser(){
    
             //造一些假数据
    
             long uid=123;
    
             String name=’shenjian’;
    
             //先删除这个伪造的用户
    
             DeleteUser(uid);
    
             //调用被测试的接口
    
             bool result= RegisterUser(uid,name);
    
             //预期注册成功,对结果进行断言判断
    
             Assert(result,true);
    
             //返回测试结果
    
             return result;
    
    }
    

    这样,在相同条件下,不管这个用例执行多少次,得到的测试结果都是相同的。

    是不是对幂等性有点感觉了。

    读请求,一般是幂等的。

    写请求,视情况而定:

    insert x,一般来说不是幂等的,重复插入得到的结果不一定一样
    
    delete x,一般来说是幂等的,删除多次得到的结果仍相同
    
    set a=x是幂等的
    
    set a=a-x不是幂等的
    

    因此,这么扣减余额:

    UPDATE t_yue SET money=$new_money WHERE uid=$uid AND money=$old_money;
    

    是幂等操作。

    要是这么扣减余额:

    UPDATE t_yue SET money=money-$diff WHERE uid=$uid AND money-$diff>0;
    

    不是幂等操作。

    聊到这里,或许有朋友要抬杠了,测试用例会重复执行,扣款怎么会重复执行呢?
    重试。

    重试,是异常处理里很常见的手段。

    你在写业务的时候有没有写过这样的代码:

    result = DoSomething();
    
    if(false==result || TIMEOUT){
    
             //错误,或者超时,重试一次
    
             result= DoSomething();
    
    }
    
    return result;
    

    当然,又会有朋友抬杠了,我从来不重试!!!
    画外音:额,这是合格,还是不合格呢?

    你可以决定业务代码怎么写,你不能决定底层框架代码怎么写:
    (1)站点框架有没有自动重试?
    (2)服务框架有没有自动重试?
    (3)服务连接池,数据库连接池有没有自动重试?
    画外音:
    (1)服务化分层的架构中,建议只入口层重试,服务层不要重试,防止雪崩;
    (2)dubbo底层,调用超时是默认重试的,这个设计不好;

    因此,在有重试的架构体系里,幂等性是需要考虑的一个问题。

    现在该懂了,为啥扣款和充值业务,一般使用:
    select&set,配合CAS方案

    而不使用:
    set money-=X方案

    画外音:充了100电话费,怎么多了200块?

    知其然,知其所以然,希望大家有收获。

  • 相关阅读:
    BZOJ 2154 Crash的数字表格 【莫比乌斯反演】
    BZOJ 3529 [Sdoi2014]数表 【莫比乌斯反演】
    BZOJ 2820 YY的GCD 【莫比乌斯反演】
    BZOJ 2440 [中山市选2011]完全平方数 【莫比乌斯反演】
    [BalticOI 2004] Sequence
    AtCoder [ARC070E] NarrowRectangles
    AtCoder [AGC022E] Median Replace
    AtCoder [ARC101E] Ribbons on Tree
    CF107D Crime Management
    Loj 6497「雅礼集训 2018 Day1」图
  • 原文地址:https://www.cnblogs.com/ianlab/p/14656538.html
Copyright © 2020-2023  润新知