美文网首页Redis专题程序员
k码特权中关于 Redis & MySQL DB 乐观锁

k码特权中关于 Redis & MySQL DB 乐观锁

作者: markfork | 来源:发表于2017-07-13 15:14 被阅读152次

    1.【背景】


    斐讯路由App 需要新增k码特权模块。

    2.【需求】

    1.已通过k码激活状态验证的用户可免费领取k码特权商品
    2.每个用户每天只能领取一张k码特权奖品

    3.【应用场景及难点分析】

    1.接口数据安全性要求:

    1.1 当某k码特权商品数据量为1,且高并发情况下,
    1.2 如何防止超卖(即多个用户都抢到了剩余的一个商品)
    

    2.接口性能要求:

    斐讯路由App 现用户量为300w+,日活4w+,2/8原则分析(**指80%的业务量在20%的时间里完成**)。
    经验可知用户使用斐讯路由App 的持续时间为12小时,
    所以2/8分析后,80%的日活在20%的时间内完成。
    即32000人免费领取k码特权商品要在2.4小时内完成,
    换算成每秒 完成请求数即 QPS = 3.7/s 。
    即每个接口响应请求时间至少要在 270ms 以内。才算是高性能。
    

    4.问题分析:

    1.读多写少

    每个用户每日只能领取一个k码特权商品。即1个用户加入请求免费领取k码特权接口多次,在k码商品库存量充足的情况下,只能领取到1个商品,其余请求都应该返回“对不起,您今日已领取k码特权商品”。从这个方面来定义,其属于读多写少的问题。
    

    2.并发量低

    以斐讯路由现在日活情况为4w+的数据量来估算、接口并发能力 QPS = 3.7/s ,
    属于低并发,但在k码特权模块优化程度达到一定量时,并发量是否会上升有待考察。
    但总体来说属于并发量不高的场景。
    

    也就是说k码特权问题经过模型抽象,已经变成了读多写少、并发量不大,但要保证性能,和数据安全性一致性的问题。

    对于这类问题,乐观锁思想可以作为解决这类问题的指导思想。

    5.乐观锁思想

    网上文章对乐观锁理解的误区:


    1.乐观锁是一种思想,并不是一种具体的技术实现。
    2.乐观锁类似于CAS无锁编程技术(其实也加锁,只不过在cpu层面)

    即当多个线程同时并发更新统一个变量,
    采用先select再update的方式,select出当前变量a的副本值b,然后用新值c去更新,
    更新时需要拿select 出来的变量值a的副本值b与当前非副本变量a的值做对比,
    若暂存副本值b与当前变量a非副本值相同,则正常更新,
    如果不同,则认为在当前线程更新之前已经有一个值将a变量更新,
    则更新失败,在并发情况不大的情况下,
    采用循环的方式去更新,总能更新成功,且循环更新次数不会太多。因此CAS也叫自旋锁。
    

    6.k码特权-免费领取解决方案


    1.用户每日成功领取k码特权商品次数的限制

    采用redis 数据结构 String,记录用户每日免费领取成功次数。并且可以轻松使用redis 缓存的过期 (expire) 机制做每日领次数的控制),用户每日成功领取k码特权的次数次日凌晨清空。
    为什么不使用数据库来进行用户成功领取k码特权商品次数的控制。当然建立好索引此问题也可以完美解决。
    使用redis进行用户成功领取k码特权商品次数的控制原因有两个:

    1.因为redis 纯粹的查询快,减轻数据库压力!不用每次都通过数据库二次索引,从磁盘找到目标记录并读入到内存。**
    2.线上配置的redis使用量10%都不到,为了更好的利用硬件资源。**
    

    2.用户每日成功领取k码特权次数的并发更改

    从接口安全性考虑,若有用户恶意领取、那么有可能产生一个用户在一天之内领取了多个k码特权奖品,这个是业务需求所不允许的。
    这里我们使用到了redis 提供的 事务(multi)与watch(乐观锁实现) 机制来控制 用户每日成功领取k码特权次数的并发更改。

    watch机制:对键值进行监控,当被其他客户端改变时,
    当前的客户端的所有操作将会失败,抛出错误信息。
    

    3.用户并发更新同一k码特权商品库存、同一商品的具体某个item

    上述问题,属于对竟态资源的并发修改,在接口请求并发量不大、且读多写少的情况下,采用数据库乐观锁来解决问题。

    数据库乐观锁实现方式:
    在竞态资源(商品)记录上添加一列,update_version,表示更新次数。
    数据库乐观锁实现方式伪代码:
    for(;;){
        //获取某k码商品库存,更新版本号 sql
        $getRewardStcokSql = 'select reward_stock,reward_update_version from fx_platform_reward_amount where reward_type_id = {$reward_type_id}';
        $getReardStockResult = $model->query($getRewardStcokSql);
        if(!$getReardStockResult ){
            die;
        } 
        $reward_stock = getReardStockResult['reward_stock'];
        $reward_update_version = getReardStockResult['reward_update_version'];
        //如果库存量>0
        if($reward_stock>0){
          //更新k码商品库存,版本号需要进行对比,其实本质上是不再使用数据库提供的排它锁,而将排他控制的职责交给选择某条需要更新记录的过滤条件。
            $updateRewardStockSql = 'update fx_platform_reward_amount set reward_stock = reward_stock-1 and reward_update_version = reward_update_version + 1 where reward_type_id = {$reward_type_id} reward_update_version = {$reward_update_version} ';
            $updateRewardStockResult = $model->excute($updateRewardStockSql);
        }
        //并发更新失败,表示在此用户更新商品库存之前已经有用户更新成功,需要重新尝试更新。
        if(!$updateRewardStockResult){
            continue;
        }
    }
    

    7.测试结果


    7.1 并发测试,数据能保持一致性
    7.2 用户免费领取k码特权商品响应时间均值为 110ms 左右,
          用户当日已领取过k码特权奖品的接口响应时间40-55ms左右。
    

    相关文章

      网友评论

      • ef61651dbece:博主,有一点问题希望您能解答

        注释中提到 "其实本质上是不再使用数据库提供的排它锁,而将排他控制的职责交给选择某条需要更新记录的过滤条件"

        update本身应该是一个隐式的事务。而对于乐观锁,版本号的更新必须是串行执行的(必须是原子的,同一时刻只能有一个线程更新版本成功),您的例子中利用的就是update本身提供的锁机制(悲观锁)来串行化版本号的更新。那这种方式能带来什么增益呢?直接用update来更新不就可以了么?
        markfork:@生气的六爷 多谢六爷打赏~
        ef61651dbece:@fxliutao 谢谢博主的回答!

        我对数据库底层的实现并不是特别了解,看了您关于update的解释,我是否可以这样理解,update这个隐式的事务,它首先会判断条件(Where自居)是否为真,这一步是一个读操作。若条件为真,才会执行更新操作(写操作),而独占锁是在写操作的时刻才加上的。而version的目的在于过滤掉一些update的执行操作。

        您的博客写的非常好,看了您的博客受益匪浅!
        markfork:@生气的六爷
        六爷,多谢你提出的问题,对于你提的问题我的回答如下:
        乐观锁应用:假设的是当并发写操作对同一竟态资源更新时,假设程序有思想, 每个写操作(线程)乐观的认为自己肯定再短时间内执行到,那么就产生一个问题,如果我单纯的update table_name set column_name = $val where primary_key = $primary_key ,那么这个是真正意义上的排队,同步,在任何隔离级别下正在执行的update 都会阻塞这个操作,直到前一个操作执行成功后,才顺延至刚才等待的线程去执行。使用版本号,为什么性能就提升了呢?
        当多线程并发操作同一条记录,(乐观锁针对的是并发不是很高的更改竟态资源的场景),多个线程先select old_version,在现在添加了新的过滤条件,where current_version = old_version ,在更新一条记录前必须先根据过滤条件找到这条记录-这个是重点,加上这个条件之后,其他线程发现先前读取的oldversion已经不等于最新的version了,说明在其操作之前有个操作已经提前执行了,所以就直接放弃,重试,比起利用行锁产生的硬性阻塞是不是性能提高了很多呢,我觉得有点像识“时务者为俊杰"的意思在里面,即是勇于放弃,勇于尝试。所以乐观锁又叫自旋锁啦。

      本文标题:k码特权中关于 Redis & MySQL DB 乐观锁

      本文链接:https://www.haomeiwen.com/subject/jmuwhxtx.html