您当前的位置: 首页 > 慢生活 > 程序人生 网站首页程序人生
php竞拍抢购系统中的超买超卖的复现与mysql悲观锁解决方案
发布时间:2021-09-18 20:58:11编辑:雪饮阅读()
基于上篇https://www.gaojiupan.cn/manshenghuo/chengxurensheng/4009.html
实现了pthreads扩展的安装。
那么接下来就复现下php竞拍系统中超买超卖的场景(高并发)。
这里就用demo.php来模拟这个过程:
首先是我们要有两个表,一个是商品表:
create table goods (
id int unsigned not null auto_increment primary key,
goodname varchar(50) not null default '',
total int not null default 0
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
insert into goods(goodname,total) values('火车票',100);
当然我们这里只用一个商品来测试。
就当时抢购这一个商品。
然后我们有一个抢购成功的记录表。
CREATE TABLE `goods_transaction` (
`tid` int(11) NOT NULL,
PRIMARY KEY (`tid`) USING BTREE
) ENGINE = MyISAM CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Fixed;
然后这个demo.php主要实现就是先初始化将库存设置如100,然后初始化抢购成功记录表(每次进来肯定都是0条数据)
然后再模拟100个用户去抢购,抢购成功减少库存并增加对应客户的抢购的这个商品记录于抢购成功的记录表中。
那么demo.php:
<?php
//解决cmd命令中中文乱码
exec('chcp 65001');
error_reporting(E_ALL ^ E_DEPRECATED);
class Conf {
public static $host = 'localhost';
public static $port = '3306';
public static $user = 'root';
public static $passwd = '123456';
public static $dbname = 'test';
}
class NoLock extends Thread {
public function run() {
//模拟真实环境,连接数据库,每次都返回一个新的数据库连接
$mysql = mysql_connect(Conf::$host.':'.Conf::$port,Conf::$user,Conf::$passwd,true);
mysql_select_db(Conf::$dbname);
//从数据库中取出库存
$sql = "select total from goods where id=1";
$result = mysql_query($sql,$mysql);
$info = mysql_fetch_assoc($result);
mysql_free_result($result);
//获取库存余量
$total = $info['total'];
$tid=self::getCurrentThreadId();
echo '本次线程:tid='.$tid.' 本次total='.$total."\n";
//判断库存是否还有
if($total > 0) {
//扣库存
mysql_query("update goods set total='".($total-1)."' where id=1",$mysql);
//mysql_query("update goods set total=total-1 where id=1",$mysql);
//增加交易
mysql_query("insert into goods_transaction values(".$tid.")",$mysql);
}
mysql_close($mysql);
}
}
$mysql = mysql_connect(Conf::$host.':'.Conf::$port,Conf::$user,Conf::$passwd,true);
mysql_select_db(Conf::$dbname);
//清空之前的交易记录
mysql_query("delete from goods_transaction",$mysql);
//设置库存为100
$sql = "update goods set total=100 where id=1";
mysql_query($sql,$mysql);
mysql_close($mysql);
//用100个模拟用户去抢购
$clientArr = [];
for ($i=0;$i<100;++$i) {
$clientArr[$i] = new NoLock();
$clientArr[$i]->start();
}
//获取结果
$mysql = mysql_connect(Conf::$host.':'.Conf::$port,Conf::$user,Conf::$passwd,true);
mysql_select_db(Conf::$dbname);
$sql = "select total from goods where id=1";
$result = mysql_query($sql,$mysql);
$info = mysql_fetch_assoc($result);
mysql_free_result($result);
mysql_close($mysql);
echo 'end total='.$info['total']."\n";
?>
接下来这个demo.php要运行在cli中,如果直接运行在网页中则有可能出现如下错误:
Internal Server Error
The server encountered an internal error or misconfiguration and was unable to complete your request.
Please contact the server administrator at admin@example.com to inform them of the time this error occurred, and the actions you performed just before this error.
More information about this error may be available in the server error log.
Additionally, a 500 Internal Server Error error was encountered while trying to use an ErrorDocument to handle the request.
然后apache错误日志频繁出现类型这样的错误:
[Sat Sep 18 20:09:19.525557 2021] [authz_core:debug] [pid 20464:tid 8812] mod_authz_core.c(817): [client ::1:58031] AH01626: authorization result of Require all granted: granted, referer: http://localhost/demo.php
这里的原因大概就是在apache中开启php子线程原因导致。
而这种需求我反正目前是没有遇到过,那么所以这里不用管这个问题,直接在cli中运行:
php D:\phpstudy_pro\blog\public\demo.php
那么跑完之后就会发现,库存竟然没有减完?不是100个线程吗?
实际上这是因为这里的逻辑是每次先读取mysql中商品库存,假如第一个线程读取时候是100,然后他准备减少库存并增加抢购记录时候(但未执行),此时由于并发原因第二个线程可能读取到的也是100,因为第一个还没有来的及修改,此时第二个线程减库存也是以100来进行减少库存的。这里就是因为读取快写入慢,那么按照这个逻辑下去自然就是最后库存剩余的多着呢,这样下去就算库存还多着,而100个线程都跑完了,则对应也就创建了100条抢购记录。
库存没有消完,如果单纯从库存判断则肯定不足100个人购买,但实际上就是有100条记录。
按正常逻辑,实际抢到的应该正好是把库存消完,但是这里没有消耗完,那么假如抢购的人非常多,也就是说线程数超过100个,那么最后这些线程也都跑完了的,就相当于超过库存量的抢购记录了,不就是超卖了,对于线程(用于)来说也就是超买了。
Mysql悲观锁解决方案
所谓mysql悲观锁,在这里就是将程序中读取到的库存去减1,然后赋值给mysql中库存字段这种方式直接交给mysql的自减操作。
<?php
//解决cmd命令中中文乱码
exec('chcp 65001');
error_reporting(E_ALL ^ E_DEPRECATED);
class Conf {
public static $host = 'localhost';
public static $port = '3306';
public static $user = 'root';
public static $passwd = '123456';
public static $dbname = 'test';
}
class NoLock extends Thread {
public function run() {
//模拟真实环境,连接数据库,每次都返回一个新的数据库连接
$mysql = mysql_connect(Conf::$host.':'.Conf::$port,Conf::$user,Conf::$passwd,true);
mysql_select_db(Conf::$dbname);
//从数据库中取出库存
$sql = "select total from goods where id=1";
$result = mysql_query($sql,$mysql);
$info = mysql_fetch_assoc($result);
mysql_free_result($result);
//获取库存余量
$total = $info['total'];
$tid=self::getCurrentThreadId();
echo '本次线程:tid='.$tid.' 本次total='.$total."\n";
//判断库存是否还有
if($total > 0) {
//扣库存
//mysql_query("update goods set total='".($total-1)."' where id=1",$mysql);
mysql_query("update goods set total=total-1 where id=1",$mysql);
//增加交易
mysql_query("insert into goods_transaction values(".$tid.")",$mysql);
}
mysql_close($mysql);
}
}
$mysql = mysql_connect(Conf::$host.':'.Conf::$port,Conf::$user,Conf::$passwd,true);
mysql_select_db(Conf::$dbname);
//清空之前的交易记录
mysql_query("delete from goods_transaction",$mysql);
//设置库存为100
$sql = "update goods set total=100 where id=1";
mysql_query($sql,$mysql);
mysql_close($mysql);
//用100个模拟用户去抢购
$clientArr = [];
for ($i=0;$i<100;++$i) {
$clientArr[$i] = new NoLock();
$clientArr[$i]->start();
}
//获取结果
$mysql = mysql_connect(Conf::$host.':'.Conf::$port,Conf::$user,Conf::$passwd,true);
mysql_select_db(Conf::$dbname);
$sql = "select total from goods where id=1";
$result = mysql_query($sql,$mysql);
$info = mysql_fetch_assoc($result);
mysql_free_result($result);
mysql_close($mysql);
echo 'end total='.$info['total']."\n";
?>
这段代码多次使劲执行,最后库存都是0,所以这个方法原理上可以,也只是原理上可以,不建议直接用在高并发系统上,主要因为它会大幅度增加数据库负载。我们对系统优化一般首先着手的都是减少数据库的直接操作,因此这个方法不建议,真要用还需要看具体情况。
关键字词:php,竞拍,抢购,超买,超卖,mysql,悲观锁
上一篇:(解决 class Thread not found )php中安装pthreads扩展(php-5.4.45-Win32-VC9-x86+php_pthreads-2.0.8-5.4-ts-vc9-
相关文章
- (解决 class Thread not found )php中安装pthreads扩展(
- phpunit使用testdox的testdox-text与testdox-html参数
- phpunit敏捷文档testdox的带参情况
- phpunit使用testdox情况下多个测试方法的名字互相之间
- phpunit中filter的使用(匹配命名空间、类、方法以及数
- phpunit中expectException的使用
- phpunit中NOTICE、WARNING、ERROR的断言支持
- phpunit中expectDeprecationMessageMatches的使用
- phpunit中expectDeprecationMessage的使用
- phpunit中的expectDeprecation的使用