PHP mysql经典问题,防止库存把控不足问题

在目前这家公司做的第一个项目抽奖项目,要求每人每天可以有20次抽奖机会,抽奖机会可以通过多种方式获取,那么就要求每次入库增加抽奖机会的时候检测当前拥有的抽奖机会是否达到了20次,如果达到了,就不再增加机会。这里需要两个步骤

<?php
$count=query_sql("select count(*) as num from table where user_id=用户id");//统计拥有了多少机会
if($count>=20)
{
    die('不增加机会');
}
query_sql("insert into table (……)");#增加一次抽奖机会
?>

很显然,先获取用户有了多少条记录,如果达到上限,不再增加,如果没达到,则增加。很简单的逻辑,可是在测试的过程中,发现并发高的情况下,总是能插入22条 23条等等,超过20条记录的。

这个问题在很多业务逻辑中都遇到了,比如商品库存控制中,获取库存数量如果数量>0则能够扣库存,然后就对库存自减,可是多个并发的情况下,就把库存扣为负数了。当然也是不合理的,说明大家都下单成功了,按道理讲不应该出现超卖的情况的。可是这样的代码总是会出现

<?php
$stock_num=query_sql("select stock_num from goods where id=商品id");//获取库存数量
if($stock_num>$buy_num)
{
    query_sql("update goods  set stock_num=stock_num-$buy_num where id=商品id");//数量进行扣库存
}

屡见不鲜的代码,我想各位读者也遇到过这样的逻辑的,毕竟资源总是有限的,我们肯定要多做一些判断,然而如果过分依赖这种读取的数量比较,就会造成超卖的问题。

当多个进程同时读取数据库得到的是极限的边缘的时候,大家都走进了判断分支条件中,然后发现我们都满足条件,于是大家一起入库或者操作数据库扣库存,然后呢?很明显,就超卖了!

这个问题在我的同事做的项目中也遇到了,本来每天100个库存的,前几天都是在100这个边缘就结束了很正常,结果某一天出现了121条记录,显然,大家突破了这个限制,这就让人很尴尬了,库存控制问题,非常严重,会给公司带来巨大的损失。非实物道具能够撤回或者处理,实物商品可以拒绝发货,可是仍然免不了被投诉。

这个问题如此严重,那么后来是怎么解决的呢?

我在做抽奖项目的时候,因为数据库担心并发问题,采用了redis记录获取的次数,每获取一次,redis+1,然后到达20次的时候,入库之前,先判断redis中的数量 再判断数据库中的数量,两者有一个不满足,则立即告知不可添加记录。这样就是等于把统计数量这个逻辑从数据库的统计交给了redis的统计,因为redis在原子性的操作,可以实现计数器功能应对并发的问题,这也是一个办法,只是来说减轻了数据库的责任,降低风险。

然而真的数据库层面就没有办法了吗?

我想起了平台组组长曾经说过的update语句中加where条件

<?php
$stock_num=query_sql("select stock_num from goods where id=商品id");//获取库存数量
if($stock_num>$buy_num)
{
    query_sql("update goods  set stock_num=stock_num-$buy_num where id=商品id and stock_num>=$buy_num");//数量进行扣库存
}

使用where子句进行对库存数量的控制,避免达到负数,这种方式能够使得SQL语句在库存不够的情况下返回false,这样就能正确的得到下单失败了。然而,对于count出来的数据如何在SQL语句中实现查询之后再更新呢?

这个问题就复杂了很多,毕竟我从来没实现过在update的语句中使用select count语句,这样的语句令人匪夷所思,但是SQL语句之所以强大,就是强大在这里,子句可以有很强大的分析功能,帮我们实现。

采用where设定子句,将子句设为表达式,避免理解为select语句,试试看

<?php
query_sql("update table set 要改字段='要改的值' where id=要改的记录id and (select count(*) from table where 条件语句)<10");//在子查询中设定某个查询条件记录总数小于上限则进行更新
?>

代码虽然可以这样写,但是会报错  ERROR 1093 (HY000): You can't specify target table  for update in FROM clause

就是说你在用update的时候不能用这样的子查询作为条件,那么我们还有其他的方法解决。给这个子查询换一个临时表不就行了吗?

update table set 要改字段='要改的值' where id=要改的记录id and (select count(*) from (select * from table table where 条件语句) as tmp)<10

这样就不会被立即为更新当前表的时候查询当前表了,因为被临时表进行了转换。或者采用网友的写法

update table,(select count(*) as num from table where 条件) as b set 要改字段='要改的值' where id=要改的记录id and b.num<10

这种方式可能更加清晰一点,能够容易理解一些,不过我还是比较喜欢我自己写出的那种方式,通过子句作为条件进行理解。

posted @ 2018-06-19 17:15  李照耀  阅读(2092)  评论(0编辑  收藏  举报