建一张测试表
首先我们来创建一张库存表
CREATE TABLE `stock` (`id` INT(10) UNSIGNED NOT NULL AUTO_INCREMENT,`name` VARCHAR(50) NOT NULL DEFAULT '' COMMENT '名称',`stock` INT(10) NOT NULL DEFAULT '0' COMMENT '库存',PRIMARY KEY (`id`),INDEX `stock` (`stock`))COMMENT='库存表'COLLATE='utf8_general_ci'ENGINE=InnoDB;
再随便添两条数据,最后的样子如下
常规操作
下面我们来写php程序
<?php
//连接数据库
$option = [PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC];
$pdo = new PDO("mysql:host=localhost;dbname=test;charset=utf8", 'root', 'root', $option);
//查询
$sql = 'select * from stock where id = :id';
$sth = $pdo->prepare($sql);
$sth->bindValue(':id',1);
$sth->execute();
$res = $sth->fetch();
//如果有库存那么库存减1
if ($res['stock'] > 0) {
//修改
$sql = 'update stock set stock = stock-1 where id = :id';
$sth = $pdo->prepare($sql);
$sth->bindValue(':id',1);
$sth->execute();
}else{
echo '无库存';
}
上面程序,一看没啥问题,先查询库存,库存大于0,进行减库存(添加订单)操作
但是我们测试一下,用Apache自带的工具ab
$ ab -n 1200 -c 1200 http://localhost/test.php
测试几次,总会发现库存为负数的情况,如
在并发情况下这就出现了超卖的问题了,那么怎么解决呢?下面我们来看看
数据库锁 - 悲观锁
把上面的程序改为
<?php
//连接数据库
$option = [PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC];
$pdo = new PDO("mysql:host=localhost;dbname=test;charset=utf8", 'root', 'root', $option);
//开启事务
$pdo->beginTransaction();
//查询 - select ... for update
$sql = 'select * from stock where id = :id for update';
$sth = $pdo->prepare($sql);
$sth->bindValue(':id',1);
$sth->execute();
$res = $sth->fetch();
//如果有库存那么库存减1
if ($res['stock'] > 0) {
//修改
$sql = 'update stock set stock = stock-1 where id = :id';
$sth = $pdo->prepare($sql);
$sth->bindValue(':id',1);
$sth->execute();
}else{
echo '无库存';
}
//提交事务
$pdo->commit();
如上所示,加了一个事务,并且在selest语句上加了 for update
这样我们就给数据表加上了一道锁,我们把这称为悲观锁
select … for update 把此行锁定,在事务提交之前,其他地方是不能修改此行的
锁定后,我们就继续我们的操作减库存的减库存,添加订单的添加订单,最后都完成了后再提交事务
经过上面的改进后,经过多次测试,库存并不会出现负数的情况
文件锁
<?php
//连接数据库
$option = [PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC];
$pdo = new PDO("mysql:host=localhost;dbname=test;charset=utf8", 'root', 'root', $option);
//随意打开/创建一个文件
$fp = fopen("order.lock", "w");
//锁定
if(flock($fp,LOCK_EX)){
//查询
$sql = 'select * from stock where id = :id';
$sth = $pdo->prepare($sql);
$sth->bindValue(':id',1);
$sth->execute();
$res = $sth->fetch();
//如果有库存那么库存减1
if ($res['stock'] > 0) {
//修改
$sql = 'update stock set stock = stock-1 where id = :id';
$sth = $pdo->prepare($sql);
$sth->bindValue(':id',1);
$sth->execute();
}else{
echo '无库存';
}
//解锁
flock($fp,LOCK_UN);
}else{
echo '系统繁忙';
}
//关闭文件
fclose($fp);
总的来说就是,打开一个文件并锁定
这里用的是 阻塞(等待)模式(只要有其他进程已经加锁文件,当前进程会一直等其他进程解锁文件)
也可以用 非阻塞(等待)模式(如果其他进程已经加锁文件,当前进程不会等其他进程解锁文件,直接返回,也就是直接忽略加锁的代码到关闭文件那块)
用非阻塞模式的话,直接把上面的 if(flock($fp,LOCK_EX)) 改为 if(flock($fp,LOCK_EX | LOCK_NB))
怎么样,是不是很简单呢,动手试试看吧
