PHP 使用数据库的并发问题
2023-06-13 09:15:30 时间
背景
在秒杀,抢购等并发场景下,可能会出现超卖的现象; 如:我们一共只有100个商品,在最后一刻,我们已经消耗了99个商品,仅剩最后一个。这个时候,系统发来多个并发请求,这批请求读取到的商品余量都是1个,然后都通过了这一个余量判断,最终导致超发。 在 PHP 语言中并没有原生提供并发的解决方案,因此就需要借助其他方式来实现并发控制,其实方案有很多种。总结下如何并发访问。
代码复现
数据库查询
# 查询库存还有 1
mysql> select * from goods;
+-----+
| num |
| 1 |
+-----+
后端代码
<?php
$conn = mysqli_connect('127.0.0.1', 'root', '123456') or die(mysqli_error());
mysqli_select_db($conn, 'shop');
// 查询出商品量
mysqli_query($conn, 'BEGIN');
$rs = mysqli_query($conn, 'SELECT num FROM goods WHERE id = 1');
$row = mysqli_fetch_array($rs);
$num = $row[0];
// 其他逻辑...
// ...
// 可用库存-1
mysqli_query($conn, 'UPDATE goods SET num = '.$num.' - 1 WHERE id = 1');
$affectRow = mysqli_affected_rows($conn);
if ($affectRow == 0 || mysqli_errno($conn)) {
// 回滚事务重新提交
mysqli_query($conn, 'ROLLBACK');
} else {
// 提交数据库逻辑
mysqli_query($conn, 'COMMIT');
}
mysqli_close($conn);
模拟高并发请求(ab)
# 模拟1000个请求 每次100个并发
ab -n 1000 -c 100 http://shop.com.test/index.php
# 查询库存还有-23 出现超卖
mysql> select * from goods;
+-----+
| num |
| -23 |
+-----+
改数据字段为unsigned类型
当后端库存为0时,因为字段不能为负数,将会返回false
mysql> show create table goods;
+---------+----------------------------------------------+
| Table | Create Table |
+---------+----------------------------------------------+
| goods | CREATE TABLE `counter` (
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
`num` int(11) unsigned NOT NULL DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8mb4 |
+---------+----------------------------------------------+
1 row in set (0.00 sec)
请求
# 模拟1000个请求 每次100个并发
ab -n 1000 -c 100 http://shop.com.test/index.php
# 查询库存还有0 未超卖
mysql> select * from goods;
+-----+
| num |
| 0 |
+-----+
更改隔离级别(不推荐)
Mysql 隔离级别默认为:可重复读(Repeatable read),这也是出现幻读唯一问题; 将隔离级改为更高级的:可串行化(Serializable),但是会牺牲很大的性能
查询全局和会话事务隔离级别:
# 全局
SELECT @@global.tx_isolation;
# 会话
SELECT @@session.tx_isolation;
# 当前
SELECT @@tx_isolation;
设置隔离级别
# 设置mysql的隔离级别:
set session transaction isolation level 设置事务隔离级别
# 设置read uncommitted级别:
set session transaction isolation level read uncommitted;
# 设置read committed级别:
set session transaction isolation level read committed;
# 设置repeatable read级别:
set session transaction isolation level repeatable read;
# 设置serializable级别:
set session transaction isolation level serializable;
完整操作
# 查询
SELECT @@tx_isolation;
+-----------------+
| @@tx_isolation |
+-----------------+
| REPEATABLE-READ |
+-----------------+
# 设置serializable级别:
set session transaction isolation level serializable;
# 查询
SELECT @@tx_isolation;
+----------------+
| @@tx_isolation |
+----------------+
| SERIALIZABLE |
+----------------+
请求
# 模拟1000个请求 每次100个并发
ab -n 1000 -c 100 http://shop.com.test/index.php
# 查询库存还有0 未超卖
mysql> select * from goods;
+-----+
| num |
| 0 |
+-----+
悲观锁解决
悲观锁的实现,往往依靠数据库提供的锁机制。在数据库中,悲观锁的流程如下:
- 在对记录进行修改前,先尝试为该记录加上排他锁(exclusive locks)。
- 如果加锁失败,说明该记录正在被修改,那么当前查询可能要等待或者抛出异常。具体响应方式由开发者根据实际需要决定。
- 如果成功加锁,那么就可以对记录做修改,事务完成后就会解锁了。
- 期间如果有其他对该记录做修改或加排他锁的操作,都会等待解锁或直接抛出异常。 修改后端代码
<?php
$conn = mysqli_connect('127.0.0.1', 'root', '123456') or die(mysqli_error());
mysqli_select_db($conn, 'shop');
// 查询出商品量
mysqli_query($conn, 'BEGIN');
$rs = mysqli_query($conn, 'SELECT num FROM goods WHERE id = 1 FOR UPDATE');
if($rs == false || mysqli_errno($conn)) {
// 回滚事务
mysqli_query($conn, 'ROLLBACK');
}
$row = mysqli_fetch_array($rs);
$num = $row[0];
// 其他逻辑...
// ...
// 可用库存-1
mysqli_query($conn, 'UPDATE goods SET num = '.$num.' - 1 WHERE id = 1');
$affectRow = mysqli_affected_rows($conn);
if ($affectRow == 0 || mysqli_errno($conn)) {
// 回滚事务重新提交
mysqli_query($conn, 'ROLLBACK');
} else {
// 提交数据库逻辑
mysqli_query($conn, 'COMMIT');
}
mysqli_close($conn);
请求
# 模拟1000个请求 每次100个并发
ab -n 1000 -c 100 http://shop.com.test/index.php
# 查询库存还有0 未超卖
mysql> select * from goods;
+-----+
| num |
| 0 |
+-----+
悲观锁在开始读取时即开始锁定,因此在并发访问较大的情况下性能会变差。对MySQL Inodb来说,通过指定明确主键方式查找数据会单行锁定,而查询范围操作或者非主键操作将会锁表。
乐观锁解决
主要就是两个步骤:
- 冲突检测
- 数据更新
使用乐观锁解决这个问题,首先我们为goods表增加一列字段:
mysql> select * from goods;
+------+---------+
| num | version |
+------+---------+
| 1 | 1 |
+------+---------+
修改后端代码
<?php
$conn = mysqli_connect('127.0.0.1', 'root', '123456') or die(mysqli_error());
mysqli_select_db($conn, 'shop');
// 查询出商品量
mysqli_query($conn, 'BEGIN');
$rs = mysqli_query($conn, 'SELECT num, version FROM goods WHERE id = 1');
$row = mysqli_fetch_array($rs);
$num = $row[0];
$version = $row[1];
// 其他逻辑...
// ...
// 可用库存-1
mysqli_query($conn, 'UPDATE counter SET num = '.$num.' - 1, version = version - 1 WHERE id = 1 AND version = '.$version);
$affectRow = mysqli_affected_rows($conn);
if ($affectRow == 0 || mysqli_errno($conn)) {
// 回滚事务重新提交
mysqli_query($conn, 'ROLLBACK');
} else {
// 提交数据库逻辑
mysqli_query($conn, 'COMMIT');
}
mysqli_close($conn);
请求
# 模拟1000个请求 每次100个并发
ab -n 1000 -c 100 http://shop.com.test/index.php
# 查询库存还有0 未超卖
mysql> select * from goods;
+-----+
| num |
| 0 |
+-----+
Redis中也有类似的乐观锁方案的watch
队列解决
直接将请求放入队列中的,采用FIFO(First Input First Output,先进先出),这样的话,我们就不会导致某些请求永远获取不到锁。
<?php
$conn = mysqli_connect('127.0.0.1', 'root', '123456') or die(mysqli_error());
mysqli_select_db($conn, 'shop');
// 查询出商品量
$rs = mysqli_query($conn, 'SELECT num FROM goods WHERE id = 1');
$row = mysqli_fetch_array($rs);
$num = $row[0];
mysqli_close($conn);
// 其他逻辑...
// ...
//队列数组
$user_id = 1;
$redis=new Redis();
$redis->lpush('goods_que',$user_id.'-'.$goods_id.$num)
处理队列
<?php
$conn = mysqli_connect('127.0.0.1', 'root', '123456') or die(mysqli_error());
mysqli_select_db($conn, 'shop');
while (true){
$redis=new Redis();
$str=$redis->rpop('goods_que')
// todo 处理逻辑
}
这只是简单实现,可靠性高的可以用RabbitMQ、kafka等其他强势消息中间件。
其他方案
还有很多好的方案,如:文件锁、Redis 分布式锁等等… 此处列举皆为伪代码
相关文章
- php获取当天的开始时间和结束时间
- PHP和MySQL:构建动态网站的利器(php与mysql)
- PHP操作MySQL:实现数据库写入操作(php写入mysql)
- PHP记录容联通话语音记录和通话时长的方法详解编程语言
- 库展开 PHP 与 Oracle 数据库之间的连接(phporacle类)
- 数据深入了解:PHP查询MySQL数据库(php显示mysql)
- 数据库PHP编程实现从Oracle数据库读取数据(php读取oracle)
- Linux环境下,如何高效、简单地编写PHP代码(linux下写php)
- PHP如何准确获取MySQL数据库中的错误信息?(php获取mysql错误)
- MySQL和PHP编程:构建数据库连接桥梁(mysql与php连接)
- 如何在PHP中启用MSSQL数据库连接(php开启mssql)
- PHP连接MSSQL数据库遇到问题:无法连接的解决方案(php 无法 mssql)
- 服务器深入比较:PHP数据库与MSSQL服务器(php数据库和mssql)
- PHP与MSSQL数据库之间的转义学习(php mssql 转义)
- 用PHP连接MSSQL数据库:轻松搞定!(mssql连接类php)
- 使用Redis需要PHP版本满足特定要求(redis需要php版本)
- Redis实现PHP限流策略(redis限流php)
- 用PHP调用数据库的存贮过程
- php连接mysql数据库代码
- 一步一步学习PHP(4)php函数补充2
- 创建数据库php代码用PHP写出自己的BLOG系统
- PHP常用特殊运算符号和函数总结(php新手入门必看)
- php将字符串转化成date存入数据库的两种方式
- PHP配置文件php.ini中打开错误报告的设置方法