php使用redis做缓存秒杀超卖刷单处理[高并发]
上篇文章 php+mysql处理产品秒杀减库存[高并发]
秒杀场景
单个产品库存有限
很多用户同时在同一时间抢购
抢购的用户分为正常用户和黄牛,黄牛会刷单
秒杀产品如果并发不是太高的情况下可以直接使用mysql的事务来处理就可以实现目的,一般情况下实现10到20个人同时在线是没有问题的(主要还得看带宽和服务器的配置),如果并发数超过100以上就得考虑加缓存啦,mysql是处理不了的,性能是个大问题,
使用redis实现简单的处理场景,抢购单个产品的时候判断缓存中有没有产品的信息,没有的话从数据库取并且保存到缓存中,下次直接从缓存中取,只要库存充足就让用户正常下单并且提示下单成功
$redis = new redis(); $res = $redis->connect('127.0.0.1', 6379); if (!$res) { die('connect error!'); } $redis->select(6); //第一次取库存,先用保存到缓存中 $goods_id = 1; $stock_key = 'goods_id_stock_' . $goods_id; $stock = $redis->get($stock_key); if ($stock === false) { echo 'set goodslist'; $info = $db->table('Goods')->get(1); $stock = $info['stock']; // 这个地方一定要用setnx,防止一开始并发的时候重复设置 $redis->setnx($stock_key, $info['stock']); } if ($stock > 0) { $redis->decr($stock_key); //把这个产品信息添加到这个用户的订单中 //....一些逻辑代码 echo 'success'; } else { echo 'fail'; }
上次的代码还可以优化最后一步,下单成功后的用户信息可以先保存到redis的集合里面,然后判断下是否所有产品都秒杀完毕然后统一保存到mysql数据库
高并发超卖问题
之前的文章中说过mysql中超卖的情况啦,在redis中也会存在这种情况,如下在减库存时加一个延时因为我这里没有任何的逻辑处理,而实际项目中是有一些逻辑处理的,所以我直接加延时1秒然后再减库存如下
$redis = new redis(); $res = $redis->connect('127.0.0.1', 6379); if (!$res) { die('connect error!'); } $redis->select(6); //第一次取库存,先用保存到缓存中 $goods_id = 1; $stock_key = 'goods_id_stock_' . $goods_id; $stock = $redis->get($stock_key); if ($stock === false) { echo 'set goodslist'; $info = $db->table('Goods')->get(1); $stock = $info['stock']; // 这个地方一定要用setnx,防止一开始并发的时候重复设置 $redis->setnx($stock_key, $info['stock']); } if ($stock > 0) { //加一些延时,模拟耗时的逻辑处理////////////////////// sleep(1); $redis->decr($stock_key); //把这个产品信息添加到这个用户的订单中 //....一些逻辑代码 echo 'success'; } else { echo 'fail'; }
并发压力测试,如下图所示100个用户同时访问,时间间隔很近
查看redis中的库存数据,已经成-8啦
出现这种情况的原因是因为redis的命令操作是原子操作,但是请注意上面代码中redis命令和php代码混合的执行redis命令后又执行啦php代码然后才执行redis减操作的,没有执行减操作的这个时间段是可能出现问题的
redis事务和乐观锁
redis事务的作用就是为啦保持操作上的原子性,也就是说保证redis的一批命令执行过程中不会被其它情况打扰,但是它并不像关系数据库那样有回滚操作,如果其中的一些命令执行失败后还是会照样执行下面其它的命令,处理方法为加一个乐观锁,(使用watch)监控你要处理的key值,如果这个值在提交事务前被其它线程或用户修改过的话就放弃这个事务操作,也就是说不执行事务里的命令,如果这个事务之前有其它的数据库修改操作的话就回滚(其实本来就是为啦提高性能,这里就不应该出现数据库的操作)
$redis = new redis(); $res = $redis->connect('127.0.0.1', 6379); if (!$res) { die('connect error!'); } $redis->select(6); //第一次取库存,先用保存到缓存中 $goods_id = 1; $stock_key = 'goods_id_stock_' . $goods_id; //先把库存取出来 $stock = $redis->get($stock_key); //监控key $redis->watch($stock_key); //开启事务 $redis->multi(); if ($stock === false) { echo 'set goodslist'; $info = $db->table('Goods')->get(1); $stock = $info['stock']; // 这个地方一定要用setnx,防止一开始并发的时候重复设置 $redis->setnx($stock_key, $info['stock']); } if ($stock > 0) { //加一些延时 sleep(1); $redis->decr($stock_key); //把这个产品信息添加到这个用户的订单中 //....一些逻辑代码 //提交事务 $res = $redis->exec(); if ($res === false) { //实际业务中上面如果有其它对数据库进行的操作这里要使用事务回滚 echo 'unknow'; } else { echo 'success'; } } else { echo 'fail'; }
经过这样处理后效果如下
黄牛刷单处理
刷单的情况如下,写一个脚本不间断的发送请求,抢购页面的每次请求用这个用户的标识为一个redis缓存标记加一个过期时间,比如10秒内重复的请求不做处理,起到限流的作用
黄牛多买的情况,引导用户到付款成功时才根据活动规则减库存,限制购买个数
如果遇到全国大量黄牛集体刷单,比如淘宝双11,小米抢购,在优化购买流程已经到极限的情况下,可以加硬件配置,使用redis分布式架构配合nginx反向代理等方法进行分流引导
无非就是黄牛数量多证明你的业务能力强,该投放入的硬件成本也高,这个时候你已经成功啦,也不会在乎这些成本!如果是别人恶意刷你的活动,相信他们刷的成本也不会低,那就硬抗,抗不过的咱就先洗洗睡休息先不玩啦。