redis实现高并发下的抢购/秒杀功能

1, http://www.cnblogs.com/phpper/p/6716248.htmlphp

https://www.cnblogs.com/phpper/p/7085663.htmlhtml

https://www.cnblogs.com/TankXiao/p/4045439.htmlmysql

悲观锁(Pessimistic Lock)

悲观锁的特色是先获取锁,再进行业务操做,即“悲观”的认为获取锁是很是有可能失败的,所以要先确保获取锁成功再进行业务操做。一般所说的“一锁二查三更新”即指的是使用悲观锁。一般来说在数据库上的悲观锁须要数据库自己提供支持,即经过经常使用的select … for update操做来实现悲观锁。当数据库执行select for update时会获取被select中的数据行的行锁,所以其余并发执行的select for update若是试图选中同一行则会发生排斥(须要等待行锁被释放),所以达到锁的效果。select for update获取的行锁会在当前事务结束时自动释放,所以必须在事务中使用。laravel

这里须要注意的一点是不一样的数据库对select for update的实现和支持都是有所区别的,例如oracle支持select for update no wait,表示若是拿不到锁马上报错,而不是等待,mysql就没有no wait这个选项。另外mysql还有个问题是select for update语句执行中全部扫描过的行都会被锁上,这一点很容易形成问题。所以若是在mysql中用悲观锁务必要肯定走了索引,而不是全表扫描。redis

 

 

乐观锁(Optimistic Lock)

乐观锁是否在事务中其实都是无所谓的,其底层机制是这样:在数据库内部update同一行的时候是不容许并发的,即数据库每次执行一条update语句时会获取被update行的写锁,直到这一行被成功更新后才释放。所以在业务操做进行前获取须要锁的数据的当前版本号,而后实际更新数据时再次对比版本号确认与以前获取的相同,并更新版本号,便可确认这之间没有发生并发的修改。若是更新失败便可认为老版本的数据已经被并发修改掉而不存在了,此时认为获取锁失败,须要回滚整个业务操做并可根据须要重试整个过程。好吧,在此唠叨总结下这两个锁:sql

总结

  • 乐观锁在不发生取锁失败的状况下开销比悲观锁小,可是一旦发生失败回滚开销则比较大,所以适合用在取锁失败几率比较小的场景,能够提高系统并发性能数据库

  • 乐观锁还适用于一些比较特殊的场景,例如在业务操做过程当中没法和数据库保持链接等悲观锁没法适用的地方多线程

 

以前写过一篇文章,高并发的解决思路(点此进入查看),今天再次抽空整理下实际场景中的具体代码逻辑实现吧:
抢购/秒杀是现在很常见的一个应用场景,那么高并发竞争下如何解决超抢(或超卖库存不足为负数的问题)呢?并发

常规写法:oracle

查询出对应商品的库存,看是否大于0,而后执行生成订单等操做,可是在判断库存是否大于0处,若是在高并发下就会有问题,致使库存量出现负数

这里我就只谈redis的解决方案吧...
咱们先来看如下代码(这里我以laravel为例吧)是否能正确解决超抢/卖的问题:

<?php

  $num = 10; //系统库存量
  $user_id = \Session::get('user_id');//当前抢购用户id
  $len = \Redis::llen('order:1'); //检查库存,order:1 定义为健名
  if($len >= $num)
    return '已经抢光了哦';

  $result = \Redis::lpush('order:1',$user_id); //把抢到的用户存入到列表中
  if($result)
    return '恭喜您!抢到了哦';

?>

若是代码正常运行,按照预期理解的是列表order:1中最多只能存储10个用户的id,由于库存只有10个。
然而,可是,在使用jmeter工具模拟多用户并发请求时,最后发现order:1中老是超过5个用户,也就是出现了“超抢/超卖”。
分析问题就出在这一段代码:

 $len = \Redis::llen('order:1'); //检查库存,order:1 定义为健名 if($len >= $num)   return '已经抢光了哦';



在抢购进行到必定程度,假如如今已经有9我的抢购成功,又来了3个用户同时抢购,这时if条件将会被绕过(条件同时被知足了),
这三个用户都能抢购成功。而实际上只剩下一件库存能够抢了。
在高并发下,不少看似不大多是问题的,都成了实际产生的问题了。要解决“超抢/超卖”的问题,
核心在于保证检查库存时的操做是依次执行的,再形象的说就是把“多线程”转成“单线程”。
即便有不少用户同时到达,也是一个个检查并给与抢购资格,一旦库存抢尽,后面的用户就没法继续了。

  咱们须要使用redis的原子操做来实现这个“单线程”。首先咱们把库存存在goods_store:1这个列表中,假设有10件库存,就往列表中push10个数,
这个数没有实际意义,仅仅只是表明一件库存。抢购开始后,每到来一个用户,就从goods_store:1中pop一个数,
表示用户抢购成功。当列表为空时,表示已经被抢光了。由于列表的pop操做是原子的,即便有不少用户同时到达,也是依次执行的。抢购的示例代码以下:

好比这里我先把库存放入redis队列:
$num=10; //库存
 $len=\Redis::llen('goods_store:1'); //检查库存,goods_store:1 定义为健名
 $count = $num-$len; //实际库存-被抢购的库存 = 剩余可用库存
 for($i=0;$i<$count;$i++)
   \Redis::lpush('goods_store:1',1);//往goods_store列表中,未抢购以前这里应该是默认滴push10个库存数了

 //echo \Redis::llen('goods_store:1');//未抢购以前这里就是10了
好吧,抢购时间到了:
/* 模拟抢购操做,抢购前判断redis队列库存量 */
 $count=\Redis::lpop('goods_store:1');//lpop是移除并返回列表的第一个元素。
 if(!$count)
    return '已经抢光了哦';
 
 
/* 下面处理抢购成功流程 */
\DB::table('goods')->decrement('num', 1);//减小num库存字段
相关文章
相关标签/搜索