美文网首页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左右。

相关文章

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

    1.【背景】 斐讯路由App 需要新增k码特权模块。 2.【需求】 1.已通过k码激活状态验证的用户可免费领取k码...

  • 秒杀随笔

    方法: mysql悲观锁 mysql乐观锁 PHP+redis分布式锁 PHP+redis乐观锁(redis wa...

  • mysql中的乐观锁和悲观锁

    关于mysql中的乐观锁和悲观锁面试的时候被问到的概率还是比较大的。 mysql的悲观锁: 其实理解起来...

  • 表锁和行锁

    MySQL中的锁总体可以分为悲观锁和乐观锁。悲观锁MySQL中有自带的锁。乐观锁需要自己写程序控制来实现乐观锁的功...

  • rails中乐观锁和悲观锁的使用

    MySQL乐观锁和悲观锁的介绍可以参考之前的一篇文章MySQL中的锁(行锁,表锁,乐观锁,悲观锁,共享锁,排他锁)...

  • MS汇总

    数据库相关[MS-关于锁(乐观锁,悲观锁,行锁、表锁,共享锁,排他锁)Mysql索引优化Mysql查询优化Mysq...

  • DB2隔离级别和锁相关

    DB2 中四种隔离级别的叫法 DB2默认的隔离级别是CS。 关于DB2中的锁, 参考 学习DB2中的锁---锁基础

  • (4)头条mysql

    1、MySQL有哪些锁,乐观锁和悲观锁实现 如果避免、减少锁等待、团队中如何监控MySQL的锁等待的情况 锁监控:...

  • Redis事务锁CAS实现以及深入误区

    Redis中的事务 Redis的事务没有mysql那么的丰富,但在JAVA web的开发过程中利用mysql事务锁...

  • Redis实现CAS的乐观锁

    Redis实现CAS的乐观锁

网友评论

  • 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