前言:
秒杀功能不外乎就是解决下面两个问题,
- 第一个是高并发对数据库产生的压力,
- 第二个是竞争状态下如何解决库存的正确减少,则超卖问题。
使用redis是最优方式,文件锁和数据库锁都不太好,因为redis可以方便实现分布式锁,而且redis支持的并发量远远大于文件锁和数据库锁。redis使用乐观锁(共享锁),悲观锁(排它锁)都可以,不过悲观锁有个问题就是锁等待的时间会占用大量内存,秒杀一般是少量的数据,所以是读多写少场景,使用乐观锁更加合适。另外redis实现悲观锁不太友好,会产生一些问题,这些问题需要结合lua脚本才能解决。使用队列也可以,但是并发量会使队列的内存瞬间占慢。
redis使用乐观锁非常简单,就是事务结合watch()方法实现监控,因为悲观锁是进程阻塞的,为了用户体验更好,可以手动模拟把阻塞改成非阻塞。以下是实现的代码:
//初始化redis
$redis = Cache::factory('Redis');
$stock = 3; //抢购货存数量
$i = 0;
$rebeat = 5; //重复执行次数(如果锁冲突了,允许重复执行,直到没有出现锁冲突为止,把阻塞模拟成非阻塞)
while($i < $rebeat) {
$watchkey = $redis->handle()->get("watchkey");
if(!$watchkey)
$watchkey = 0;
if ($watchkey < $stock) {
//1.监控watchlist,因为虽然redis有原子性,但是事务没有原子性,所以watch这里起到一个事务锁(乐观锁)的功能.
//2.举个例子,假如有两个并发进程去竞争购买权,redis在监控一个key,第一个先到,第二个后到,先到的抢先一步更新了key,后到的虽然也进来的,但是得知key已被更新,不得已只能中断自己当前的执行事务。
//3.这里监控watchlist和watchkey都可以。
$redis->handle()->watch("watchlist");
//$redis->watch("watchkey");
//开启事务
$redis->handle()->multi();
//插入抢购数据(先放入redis,等抢购完成之后再异步入库)
$redis->handle()->hSet("watchlist", "user_id_" . mt_rand(1, 9999), time());
$redis->handle()->set("watchkey", $watchkey + 1);
//执行事务
$result = $redis->handle()->exec();
if ($result) {
$watchlist = $redis->handle()->hGetAll("watchlist");
return array(
'status'=> 1,
'msg'=>'抢购成功,剩余数量:'.($stock - $watchkey - 1),
'data'=>$watchlist
);
break;
} else {
//设置一个循环时间防止性能损耗过大
usleep(5000);
$i++;
}
} else {
return array(
'status'=> 0,
'msg'=>'抢购失败,商品已经抢购完毕!',
'data'=>[]
);
break;
}
}
return array(
'status'=> 0,
'msg'=>'抢购失败,锁冲突了!',
'data'=>[]
);
结果分析:
执行结果如下,使用ab工具模拟多个进程执行也没有出现超卖情况