事务锁
不用事务的时候并发的结果
# 库存表
CREATE TABLE `storage` (
`id` int(11) unsigned NOT NULL AUTO_INCREMENT,
`number` int(11) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=latin1
# 订单表
CREATE TABLE `order` (
`id` int(11) unsigned NOT NULL AUTO_INCREMENT,
`number` int(11) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=latin1
测试:
$pdo = new PDO('mysql:host=127.0.0.1;port=3306; dbname=test','root','test');
# 未使用事务
$sql = "select `number` from storage where id=1 limit 1";
$res = $pdo->query($sql)->fetch();
$number = $res['number'];
if ($number > 0) {
$sql = "insert into `order` VALUES (null,$number)";
$order_id = $pdo->query($sql);
if ($order_id) {
$sql = "update storage set `number`=`number`-1 WHERE id=1";
$pdo->query($sql);
}
}
预设库存是10,执行ab测试查看结果
Apache自带的ab测试工具的使用
Apache安装目录下的bin目录下 运行 命令窗口
输入 ab -V 回车
会列出ApacheBench工具的版本,注意V大写。看到如下界面
说明ab工具可以正常使用
使用ab对网站进行测试
ab -n 12 -c 10 xxx.xxx.com/xxx/xx/x.html
-n 请求的次数 -c 并发数
查看结果
mysql> select * from storage
-> ;
+----+--------+
| id | number |
+----+--------+
| 1 | -2 |
+----+--------+
1 row in set (0.00 sec)
mysql> select * from `order`;
+----+--------+
| id | number |
+----+--------+
| 22 | 10 |
| 23 | 10 |
| 24 | 8 |
| 25 | 8 |
| 26 | 7 |
| 27 | 6 |
| 28 | 4 |
| 29 | 3 |
| 30 | 2 |
| 31 | 2 |
| 32 | 2 |
| 33 | 1 |
+----+--------+
12 rows in set (0.00 sec)
得到订单12个,库存减到了-2,显然不符合实际
使用数据库行锁来解决问题
代码修改如下:
$pdo = new PDO('mysql:host=127.0.0.1;port=3306; dbname=test','root','test');
# 使用事务
$pdo->beginTransaction(); // 开启事务
$sql = "select `number` from storage where id=1 for UPDATE "; // 利用for update 开启行锁
$res = $pdo->query($sql)->fetch();
$number = $res['number'];
if ($number > 0) {
$sql = "insert into `order` VALUES (null,$number)";
$order_id = $pdo->query($sql);
if ($order_id) {
$sql = "update storage set `number`=`number`-1 WHERE id=1";
if ($pdo->query($sql)) {
$pdo->commit(); // 提交事务
} else {
$pdo->rollBack(); // 回滚
}
} else {
$pdo->rollBack(); // 回滚
}
}
查看结果
mysql> select * from storage;
+----+--------+
| id | number |
+----+--------+
| 1 | 0 |
+----+------
--+
1 row in set (0.00 sec)
mysql> select * from `order`;
+----+--------+
| id | number |
+----+--------+
| 1 | 10 |
| 2 | 9 |
| 3 | 8 |
| 4 | 7 |
| 5 | 6 |
| 6 | 5 |
| 7 | 4 |
| 8 | 3 |
| 9 | 2 |
| 10 | 1 |
+----+--------+
10 rows in set (0.00 sec)
文件锁
新建一个空的.txt文件
一. 阻塞(等待)模式:(只要有其他进程已经加锁文件,当前进程会一直等其他进程解锁文件)
<?php
// 连接数据库
$con = mysqli_connect("127.0.0.1", "test", "root", "test");
// 查询商品数量是否大于0,大于0才能下单,并减少库存
$fp = fopen("lock.txt", "r");
// 加锁
if (flock($fp, LOCK_EX)) {
$res = mysqli_fetch_assoc(mysqli_query($con, 'SELECT total FROM shop WHERE id=1 LIMIT 1'));
if ($res['total'] > 0) {
mysqli_query($con, 'UPDATE shop SET total=total-1 WHERE id=1');
}
// 执行完成解锁
flock($fp, LOCK_UN);
}
// 关闭文件
fclose($fp);
unset($res);
mysqli_close($con);
?>
二. 非阻塞(等待)模式:(只要有其他进程已经加锁文件,当前进程不会等其他进程解锁文件直接返回)
// 连接数据库
$con = mysqli_connect("127.0.0.1", "test", "root", "test");
// 查询商品数量是否大于0,大于0才能下单,并减少库存
$fp = fopen("lock.txt", "r");
// 加锁
if (flock($fp, LOCK_EX | LOCK_NB)) {
$res = mysqli_fetch_assoc(mysqli_query($con, 'SELECT total FROM shop WHERE id=1 LIMIT 1'));
if ($res['total'] > 0) {
mysqli_query($con, 'UPDATE shop SET total=total-1 WHERE id=1');
}
// 执行完成解锁
flock($fp, LOCK_UN);
}
// 关闭文件
fclose($fp);
unset($res);
mysqli_close($con);
如果连接数据库耗时的话,下面有个简单的小demo,直观的理解下。
demo.php
<?php
$fp = fopen("file_lock.txt", "r");
// 加锁
if (flock($fp, LOCK_EX)) {
sleep(10);
echo 1;
//执行完成解锁
flock($fp, LOCK_UN);
} else {
echo 2;
}
// 关闭文件
fclose($fp);
demo2.php
<?php
$fp = fopen("file_lock.txt", "r");
// 加锁(如果改成flock($fp, LOCK_EX | LOCK_NB),demo2.php会直接返回2,否则会等待demo.php执行完返回1)
if (flock($fp, LOCK_EX)) {
echo 1;
} else {
echo 2;
}
// 关闭文件
fclose($fp);
同时运行两个文件,然后修改demo2中加锁机制,就可以看出来阻塞(等待)模式和非阻塞(等待)模式的区别了。
但是这样会导致队列堵塞,假如10个人同一秒写入数据库,那就堵塞了,第10个人会等待前9个都执行完才会执行!