php防止库存超卖非秒杀,(案例讲解)

一、普通下单

并发测试时product表id=1name=稻花香大米store=15

请求总数30每次10个并发

ab-n30-c10http://xxxxx.top/code/the_limit/add_order.php

结果:

有15次库存减少成功store库存出现了负数-78次判断为库存不够(库存负数是不正确的不允许的)

phpkucun

二、unsigned模式

调整回product表id=1name=稻花香大米store=15

请求总数30每次10个并发

ab-n30-c10http://xxxxx.top/code/the_limit/unsigned.php

结果:

有15次库存减少成功store库存出现了负数-69次判断为库存不够(库存负数是不正确的不允许的)

仅在查询语句上加forupdate加锁效果不大

三、mysql的事务,锁住操作的行

调整回product表id=1name=稻花香大米store=15

请求总数30每次10个并发

ab-n30-c10http://xxxxx.top/code/the_limit/ACID.php

结果:

有15次库存减少成功store库存未出现负数15次判断为库存不够(库存负数是不正确的不允许的)

加事务效果不错ab-n3000-c1000并发也能扛住

四、非阻塞的文件排他锁

阻塞形式

非阻塞形式

效果也没有出现负数但性能上:事务>阻塞>非阻塞

五、redis队列

代码与之前略有调整观锁版

<?php

$redis=newRedis();

$redis->connect(“127.0.0.1″,6379);

$redis->auth(‘PASSWORD’);

$redis->watch(‘sales’);//乐观锁监视作用set()初始值0

$sales=$redis->get(‘sales’);

//var_dump($sales);exit;

db();

global$con;

//查询商品信息

//$product_id=1;

//$sql=”select*fromproductswhereid={$product_id}”;

//$result=mysqli_query($con,$sql);

//$row=mysqli_fetch_assoc($result);

//$store=$row[‘store’];

//库存

$n=15;

if($sales>=$n){

insertLog(‘库存为0秒杀失败’);

exit(‘秒杀结束’);

}

//redis开启事务

$redis->multi();

$redis->incr(‘sales’);//将key中储存的数字值增一,如果key不存在,那么key的值会先被初始化为0,然后再执行INCR操作。

$res=$redis->exec();//成功1失败0

if($res){

//秒杀成功

$con=newmysqli(‘localhost’,’root’,’PASSWORD’,’dev’);

if(!$con){

echo”数据库连接失败”;

}

$product_id=1;//商品ID

$buy_num=1;//购买数量

sleep(1);

$sql=”updateproductssetstore=store-{$buy_num}whereid={$product_id}”;

if(mysqli_query($con,$sql)){

echo”秒杀完成”;

insertLog(‘恭喜秒杀成功’);

}

}else{

insertLog(‘抱歉秒杀失败’);

exit(‘抢购失败’);

}

functiondb()

{

global$con;

$con=newmysqli(‘localhost’,’root’,’WOrd1234.*’,’dev’);

if(!$con){

echo”数据库连接失败”;

}

}

/**

*记录日志

*/

functioninsertLog($content)

{

global$con;

$sql=”INSERTINTO`order_log`(content)values(‘$content’)”;

mysqli_query($con,$sql);

}

ab-n30-c10http://xxxxxx.top/code/the_limit/optimistic\_redis_lock.php

最终结论并发挑战优先redis性能好

关于PHP商城秒杀防止超卖问题

  1. 在同样对数据操作的代码下,redis事务比lua脚本还要慢上许多,会偶尔出现1-10单超卖的现象。
  2. 如果想要使用redis事务,删减库存的情况,用redis->decr递减库存,不要用程序自带的加减法,这样效果会好一些。
  3. 推荐使用lua脚本加redis。
  4. 注意redis事务与mysql的事务不一样,缺少了原子性。
  5. lua+redis:Redis会将整个脚本作为一个整体执行,中间不会被其他命令插入。

实现思路:

  1. 在设置秒杀活动的时候,把秒杀商品库存存入redis,在redis里面进行删减库存,秒杀成功在同步到mysql。
  2. 秒杀开始,取出redis商品库存,然后让用户进入redis队列,如果队列不存在则创建,队列如果存在,判断用户是否在队列中,如果在队列中则提示以参加过秒杀。
  3. 判断redis商品库存是否大于0,如果大于0则秒杀继续,否则提示商品已卖完。
  4. 设置好lua脚本,在lua脚本中,再次判商品库存是否大与0,如果是,则库存自动减少1个,因为秒杀商品每人限购1,自动减少成功后返回true,否则返回false。
  5. 判断lua脚本返回的状态,如果是true则进行用户队列抢购,如果是false则提示商品已被抢空。
  6. 其中因为用户进入了队列,所以是排队的模式进行抢购下单,这样比较公平,秒杀场景都是一瞬间的事情。
  7. 这六点最作为参考,不作为实际业务场景。

一.方案一使用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;

总结:php防止库存秒杀压力过大导致后端奔溃的解决办法,有什么疑问可以留言讨论哦。

本文来自投稿,不代表(钦钦技术栈)立场,如若转载,请注明出处:https://www.qin1qin.com/catagory/519/

(0)
上一篇 2022-06-27 11:58:25
下一篇 2022-06-27 12:41:37

软件定制开发公司

相关阅读

发表回复

登录后才能评论
通知:禁止投稿所有关于虚拟货币,币圈类相关文章,发现立即永久封锁账户ID!