PHP 使用数据库的并发问题

本文涉及的产品
云数据库 RDS MySQL,集群系列 2核4GB
推荐场景:
搭建个人博客
Redis 开源版,标准版 2GB
推荐场景:
搭建游戏排行榜
云数据库 Tair(兼容Redis),内存型 2GB
简介: 在秒杀,抢购等并发场景下,可能会出现超卖的现象;如:我们一共只有100个商品,在最后一刻,我们已经消耗了99个商品,仅剩最后一个。这个时候,系统发来多个并发请求,这批请求读取到的商品余量都是1个,然后都通过了这一个余量判断,最终导致超发。

背景

在秒杀,抢购等并发场景下,可能会出现超卖的现象;

如:我们一共只有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 |
+-----+

悲观锁解决

悲观锁的实现,往往依靠数据库提供的锁机制。在数据库中,悲观锁的流程如下:

  1. 在对记录进行修改前,先尝试为该记录加上排他锁(exclusive locks)。
  2. 如果加锁失败,说明该记录正在被修改,那么当前查询可能要等待或者抛出异常。具体响应方式由开发者根据实际需要决定。
  3. 如果成功加锁,那么就可以对记录做修改,事务完成后就会解锁了。
  4. 期间如果有其他对该记录做修改或加排他锁的操作,都会等待解锁或直接抛出异常。修改后端代码
<?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来说,通过指定明确主键方式查找数据会单行锁定,而查询范围操作或者非主键操作将会锁表。

乐观锁解决

主要就是两个步骤:

  1. 冲突检测
  2. 数据更新

使用乐观锁解决这个问题,首先我们为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 分布式锁等等…

此处列举皆为伪代码

相关实践学习
基于Redis实现在线游戏积分排行榜
本场景将介绍如何基于Redis数据库实现在线游戏中的游戏玩家积分排行榜功能。
云数据库 Redis 版使用教程
云数据库Redis版是兼容Redis协议标准的、提供持久化的内存数据库服务,基于高可靠双机热备架构及可无缝扩展的集群架构,满足高读写性能场景及容量需弹性变配的业务需求。 产品详情:https://www.aliyun.com/product/kvstore &nbsp; &nbsp; ------------------------------------------------------------------------- 阿里云数据库体验:数据库上云实战 开发者云会免费提供一台带自建MySQL的源数据库&nbsp;ECS 实例和一台目标数据库&nbsp;RDS实例。跟着指引,您可以一步步实现将ECS自建数据库迁移到目标数据库RDS。 点击下方链接,领取免费ECS&amp;RDS资源,30分钟完成数据库上云实战!https://developer.aliyun.com/adc/scenario/51eefbd1894e42f6bb9acacadd3f9121?spm=a2c6h.13788135.J_3257954370.9.4ba85f24utseFl
目录
相关文章
|
26天前
|
存储 SQL 关系型数据库
PHP与数据库交互:从基础到进阶
【10月更文挑战第9天】在编程的世界里,数据是流动的血液,而数据库则是存储这些珍贵资源的心脏。PHP作为一门流行的服务器端脚本语言,其与数据库的交互能力至关重要。本文将带你从PHP与数据库的基本连接开始,逐步深入到复杂查询的编写和优化,以及如何使用PHP处理数据库结果。无论你是初学者还是有一定经验的开发者,这篇文章都将为你提供宝贵的知识和技巧,让你在PHP和数据库交互的道路上更加从容不迫。
|
2月前
|
NoSQL 关系型数据库 MySQL
不是 PHP 不行了,而是 MySQL 数据库扛不住啊
【9月更文挑战第8天】这段内容讨论了MySQL在某些场景下面临的挑战及其原因,并指出这些问题不能完全归咎于MySQL本身。高并发读写压力、数据量增长以及复杂查询和事务处理都可能导致性能瓶颈。然而,应用程序设计不合理、系统架构不佳以及其他数据库选择和优化策略不足也是重要因素。综合考虑这些方面才能有效解决性能问题,而MySQL通过不断改进和优化,仍然是许多应用场景中的可靠选择。
124 9
|
2月前
|
NoSQL 关系型数据库 PHP
php连接数据库
要使用PHP连接PolarDB或MongoDB数据库,需先准备连接信息,并编写相应代码。对于PolarDB,需设置主机地址、端口、数据库名及凭据,使用`pg_connect`函数建立连接;而对于MongoDB副本集,需安装MongoDB PHP驱动,通过`MongoDB\Client`连接指定的副本集实例。请确保替换示例代码中的占位符为实际值,并正确配置副本集名称和主机信息。更多详细信息与示例代码,请参考相关链接。
152 72
|
8天前
|
Java 数据库连接 数据库
如何构建高效稳定的Java数据库连接池,涵盖连接池配置、并发控制和异常处理等方面
本文介绍了如何构建高效稳定的Java数据库连接池,涵盖连接池配置、并发控制和异常处理等方面。通过合理配置初始连接数、最大连接数和空闲连接超时时间,确保系统性能和稳定性。文章还探讨了同步阻塞、异步回调和信号量等并发控制策略,并提供了异常处理的最佳实践。最后,给出了一个简单的连接池示例代码,并推荐使用成熟的连接池框架(如HikariCP、C3P0)以简化开发。
22 2
|
17天前
|
存储 监控 关系型数据库
MySQL并发控制与管理:优化数据库性能的关键
【10月更文挑战第17天】MySQL并发控制与管理:优化数据库性能的关键
74 0
|
2月前
|
SQL 关系型数据库 数据库连接
php连接数据库之PDO,PDO的简单使用和预定义占位符的使用以及PDOStatement对象的使用,占位符的不同形式,bindValue和bindParam绑定预定义占位符参数的区别
本文介绍了PHP中PDO(PHP Data Objects)扩展的基本概念和使用方法。内容包括PDO类和PDOStatement类的介绍,PDO的简单使用,预定义占位符的使用方法,以及PDOStatement对象的使用。文章还讨论了绑定预定义占位符参数的不同形式,即bindValue和bindParam的区别。通过具体示例,展示了如何使用PDO进行数据库连接、数据查询、数据插入等操作。
php连接数据库之PDO,PDO的简单使用和预定义占位符的使用以及PDOStatement对象的使用,占位符的不同形式,bindValue和bindParam绑定预定义占位符参数的区别
|
2月前
|
SQL 关系型数据库 MySQL
php学习笔记-连接操作mysq数据库(基础)-day08
本文介绍了PHP中连接操作MySQL数据库的常用函数,包括连接服务器、设置字符集、关闭连接、选择数据库、结果集释放、获取影响行数以及遍历结果集等操作。通过书籍查询的实例演示了如何使用这些函数进行数据库操作,并提供了一个PHP操纵MySQL数据库的模板。
php学习笔记-连接操作mysq数据库(基础)-day08
|
3月前
|
SQL 关系型数据库 MySQL
PHP与数据库交互的艺术:深入探讨PDO扩展
【8月更文挑战第28天】在数字信息时代的海洋里,PHP作为一艘灵活的帆船,承载着无数网站和应用的梦想。而PDO扩展,则是这艘帆船上不可或缺的导航仪,指引着数据安全与效率的航向。本文将带你领略PHP与数据库交互的艺术,深入浅出地探索PDO的世界,从连接数据库到执行复杂的查询,每一步都清晰可见。我们将一起航行在这段奇妙的旅程上,解锁数据的奥秘,体验编程的乐趣。
45 1
|
3月前
|
SQL 缓存 数据库连接
拯救php性能的神器webman-数据库
Webman 框架与这些最佳数据库管理实践的结合,可为应用程序提供快速响应的用户体验,高吞吐量,提升应用程序的整体性能表现。在对数据库交互进行设计和开发时,持续关注性能指标和优化,确保数据库层面不会成为应用程序的瓶颈,这样便能充分利用 Webman 来提升 PHP 应用的性能。
143 4
|
3月前
|
关系型数据库 MySQL 数据库连接
PHP数据库连接
PHP数据库连接
31 2