关于PHP商城秒杀防止超卖问题
序言:
在同样对数据操作的代码下,redis事务比lua脚本还要慢上许多,会偶尔出现1-10单超卖的现象。
如果想要使用redis事务,删减库存的情况,用redis->decr递减库存,不要用程序自带的加减法,这样效果会好一些
推荐使用lua脚本加redis
注意redis事务与mysql的事务不一样,缺少了原子性
lua+redis:Redis会将整个脚本作为一个整体执行,中间不会被其他命令插入。
实现思路:
在设置秒杀活动的时候,把秒杀商品库存存入redis,在redis里面进行删减库存,秒杀成功在同步到mysql
秒杀开始,取出redis商品库存,然后让用户进入redis队列,如果队列不存在则创建,队列如果存在,判断用户是否在队列中,如果在队列中则提示以参加过秒杀
判断redis商品库存是否大于0,如果大于0则秒杀继续,否则提示商品已卖完
设置好lua脚本,在lua脚本中,再次判商品库存是否大与0,如果是,则库存自动减少1个,因为秒杀商品每人限购1,自动减少成功后返回true,否则返回false
判断lua脚本返回的状态,如果是true则进行用户队列抢购,如果是false则提示商品已被抢空。
其中因为用户进入了队列,所以是排队的模式进行抢购下单,这样比较公平,秒杀场景都是一瞬间的事情。
这六点最作为参考,不作为实际业务场景
一.方案一使用redis事务和watch监听值变化
$goods_total = 20;
// Redis::set("goods_stock", $goods_total);
// die;
// 测试商品秒杀
$redis_stock = Redis::get("goods_stock");
if (empty($redis_stock) && $redis_stock == 0) {
return "商品已被抢空";
}
$user_id = mt_rand(1,999);
$redis_list = Redis::lRange("user_list",0, -1);
// 限定只抢购一次
if (empty($redis_list)) {
Redis::lPush("user_list", $user_id);
} else {
if (in_array($user_id, $redis_list)) {
return "您已经抢购过啦,用户id:" . $user_id;
}
Redis::lPush("user_list", $user_id);
}
if ($redis_stock > 0) {
// 方案1
Redis::Watch("goods_stock");
Redis::Multi(); // 开启事务
Redis::decr("goods_stock");
$is_ok = Redis::exec();
if ($is_ok) {
$user_id = Redis::rPop("user_list");
DB::beginTransaction();
try {
$data = [
"user_id" => $user_id,
"orders_num" => time() . mt_rand(10, 999),
];
$res = DB::table("test_table")->lockForUpdate()->insert($data);
echo "抢购成功,用户id:" . $user_id;
DB::commit();
return;
} catch (\Exception $e) {
DB::rollBack();
Redis::Discard();
echo "抢购失败,用户id:" . $user_id . "," . $e->getMessage();
return;
}
} else {
echo "商品已被抢空,用户id:" . $user_id;
Redis::Discard();
return;
}
}
echo "商品已被抢空,用户id:" . $user_id;
return;
二.方案二使用lua脚本+redis
$goods_total = 20;
// Redis::set("goods_stock", $goods_total);
// die;
// 测试商品秒杀
$redis_stock = Redis::get("goods_stock");
if (empty($redis_stock) && $redis_stock == 0) {
return "商品已被抢空";
}
$user_id = mt_rand(1,999);
$redis_list = Redis::lRange("user_list",0, -1);
// 限定只抢购一次
if (empty($redis_list)) {
Redis::lPush("user_list", $user_id);
} else {
if (in_array($user_id, $redis_list)) {
return "您已经抢购过啦,用户id:" . $user_id;
}
Redis::lPush("user_list", $user_id);
}
if ($redis_stock > 0) {
// 方案2
// lua脚本
$str = <<<Lua
local key = KEYS[1];
local redis_stock = redis.call('get', key);
if (tonumber(redis_stock) > 0)
then
redis.call('decr', key);
return true;
else
return false;
end
Lua;
$res = Redis::eval($str, 1, "goods_stock");
if ($res) {
$user_id = Redis::rPop("user_list");
DB::beginTransaction();
try {
$data = [
"user_id" => $user_id,
"orders_num" => time() . mt_rand(10, 999),
];
$res = DB::table("test_table")->lockForUpdate()->insert($data);
echo "抢购成功,用户id:" . $user_id;
DB::commit();
return;
} catch (\Exception $e) {
DB::rollBack();
// Redis::Discard();
echo "抢购失败,用户id:" . $user_id . "," . $e->getMessage();
return;
}
} else {
echo "商品已被抢空,用户id:" . $user_id;
// Redis::Discard();
return;
}
}
echo "商品已被抢空,用户id:" . $user_id;
return;