PHP:关于PHP商城秒杀防止超卖问题
转载于:https://blog.csdn.net/weixin_43356354/article/details/122688020
关于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;