转载于: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监听值变化
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 | $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
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 | $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 ; |
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· winform 绘制太阳,地球,月球 运作规律
· AI与.NET技术实操系列(五):向量存储与相似性搜索在 .NET 中的实现
· 超详细:普通电脑也行Windows部署deepseek R1训练数据并当服务器共享给他人
· 【硬核科普】Trae如何「偷看」你的代码?零基础破解AI编程运行原理
· 上周热点回顾(3.3-3.9)