天天看点

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

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左右。