PHP使用数据库的并发问题(转)
在并行系统中并发问题永远不可忽视。尽管PHP语言原生没有提供多线程机制,那并不意味着所有的操作都是线程安全的。尤其是在操作诸如订单、支付等业务系统中,更需要注意操作数据库的并发问题。 接下来我通过一个案例分析一下PHP操作数据库时并发问题的处理问题。
首先,我们有这样一张数据表:
mysql> select * from counter;
+----+-----+
| id | num |
+----+-----+
| 1 | 0 |
+----+-----+
1 row in set (0.00 sec)
这段代码模拟了一次业务操作:
<?php
function dummy_business() {
$conn = mysqli_connect(‘127.0.0.1‘, ‘public‘, ‘public‘) or die(mysqli_error());
mysqli_select_db($conn, ‘test‘);
for ($i = 0; $i < 10000; $i++) {
mysqli_query($conn, ‘UPDATE counter SET num = num + 1 WHERE id = 1‘);
}
mysqli_close($conn);
} for ($i = 0; $i < 10; $i++) {
$pid = pcntl_fork(); if($pid == -1) {
die(‘can not fork.‘);
} elseif (!$pid) {
dummy_business();
echo ‘quit‘.$i.PHP_EOL;
break;
}
}
?>
上面的代码模拟了10个用户同时并发执行一项业务的情况,每次业务操作都会使得num的值增加1,每个用户都会执行10000次操作,最终num的值应当是100000。
运行这段代码,num的值和我们预期的值是一样的:
mysql> select * from counter;
+----+--------+
| id | num |
+----+--------+
| 1 | 100000 |
+----+--------+
1 row in set (0.00 sec)
这里不会出现问题,是因为单条UPDATE语句操作是原子的,无论怎么执行,num的值最终都会是100000。 然而很多情况下,我们业务过程中执行的逻辑,通常是先查询再执行,并不像上面的自增那样简单:
<?php
function dummy_business() {
$conn = mysqli_connect(‘127.0.0.1‘, ‘public‘, ‘public‘) or die(mysqli_error());
mysqli_select_db($conn, ‘test‘);
for ($i = 0; $i < 10000; $i++) {
$rs = mysqli_query($conn, ‘SELECT num FROM counter WHERE id = 1‘);
mysqli_free_result($rs);
$row = mysqli_fetch_array($rs);
$num = $row[0];
mysqli_query($conn, ‘UPDATE counter SET num = ‘.$num.‘ + 1 WHERE id = 1‘);
}
mysqli_close($conn);
} for ($i = 0; $i < 10; $i++) {
$pid = pcntl_fork(); if($pid == -1) {
die(‘can not fork.‘);
} elseif (!$pid) {
dummy_business();
echo ‘quit‘.$i.PHP_EOL;
break;
}
}
?>
改过的脚本,将原来的原子操作UPDATE换成了先查询再更新,再次运行我们发现,由于并发的缘故程序并没有按我们期望的执行:
mysql> select * from counter;
+----+------+
| id | num |
+----+------+
| 1 | 21495|
+----+------+
1 row in set (0.00 sec)
入门程序员特别容易犯的错误是,认为这是没开启事务引起的。现在我们给它加上事务:
<?php
function dummy_business() {
$conn = mysqli_connect(‘127.0.0.1‘, ‘public‘, ‘public‘) or die(mysqli_error());
mysqli_select_db($conn, ‘test‘);
for ($i = 0; $i < 10000; $i++) {
mysqli_query($conn, ‘BEGIN‘);
$rs = mysqli_query($conn, ‘SELECT num FROM counter WHERE id = 1‘);
mysqli_free_result($rs);
$row = mysqli_fetch_array($rs);
$num = $row[0];
mysqli_query($conn, ‘UPDATE counter SET num = ‘.$num.‘ + 1 WHERE id = 1‘);
if(mysqli_errno($conn)) {
mysqli_query($conn, ‘ROLLBACK‘);
} else {
mysqli_query($conn, ‘COMMIT‘);
}
}
mysqli_close($conn);
} for ($i = 0; $i < 10; $i++) {
$pid = pcntl_fork(); if($pid == -1) {
die(‘can not fork.‘);
} elseif (!$pid) {
dummy_business();
echo ‘quit‘.$i.PHP_EOL;
break;
}
}
?>
依然没能解决问题:
mysql> select * from counter;
+----+------+
| id | num |
+----+------+
| 1 | 16328|
+----+------+
1 row in set (0.00 sec)
请注意,数据库事务依照不同的事务隔离级别来保证事务的ACID特性,也就是说事务不是一开启就能解决所有并发问题。通常情况下,这里的并发操作可能带来四种问题:
- 更新丢失:一个事务的更新覆盖了另一个事务的更新,这里出现的就是丢失更新的问题。
- 脏读:一个事务读取了另一个事务未提交的数据。
- 不可重复读:一个事务两次读取同一个数据,两次读取的数据不一致。
- 幻象读:一个事务两次读取一个范围的记录,两次读取的记录数不一致。
通常数据库有四种不同的事务隔离级别:
| 隔离级别 | 脏读 | 不可重复读 | 幻读 |
| Read uncommitted | √ | √ | √ |
| Read committed | × | √ | √ |
| Repeatable read | × | × | √ |
| Serializable | × | × | × |
大多数数据库的默认的事务隔离级别是提交读(Read committed),而MySQL的事务隔离级别是重复读(Repeatable read)。对于丢失更新,只有在序列化(Serializable)级别才可得到彻底解决。不过对于高性能系统而言,使用序列化级别的事务隔离,可能引起死锁或者性能的急剧下降。因此使用悲观锁和乐观锁十分必要。 并发系统中,悲观锁(Pessimistic Locking)和乐观锁(Optimistic Locking)是两种常用的锁:
- 悲观锁认为,别人访问正在改变的数据的概率是很高的,因此从数据开始更改时就将数据锁住,直到更改完成才释放。悲观锁通常由数据库实现(使用SELECT...FOR UPDATE语句)。
- 乐观锁认为,别人访问正在改变的数据的概率是很低的,因此直到修改完成准备提交所做的的修改到数据库的时候才会将数据锁住,完成更改后释放。
上面的例子,我们用悲观锁来实现:
<?php
function dummy_business() {
$conn = mysqli_connect(‘127.0.0.1‘, ‘public‘, ‘public‘) or die(mysqli_error());
mysqli_select_db($conn, ‘test‘);
for ($i = 0; $i < 10000; $i++) {
mysqli_query($conn, ‘BEGIN‘);
$rs = mysqli_query($conn, ‘SELECT num FROM counter WHERE id = 1 FOR UPDATE‘);
if($rs == false || mysqli_errno($conn)) {
// 回滚事务
mysqli_query($conn, ‘ROLLBACK‘);
// 重新执行本次操作
$i--;
continue;
}
mysqli_free_result($rs);
$row = mysqli_fetch_array($rs);
$num = $row[0];
mysqli_query($conn, ‘UPDATE counter SET num = ‘.$num.‘ + 1 WHERE id = 1‘);
if(mysqli_errno($conn)) {
mysqli_query($conn, ‘ROLLBACK‘);
} else {
mysqli_query($conn, ‘COMMIT‘);
}
}
mysqli_close($conn);
} for ($i = 0; $i < 10; $i++) {
$pid = pcntl_fork(); if($pid == -1) {
die(‘can not fork.‘);
} elseif (!$pid) {
dummy_business();
echo ‘quit‘.$i.PHP_EOL;
break;
}
}
?>
可以看到,这次业务以期望的方式正确执行了:
mysql> select * from counter;
+----+--------+
| id | num |
+----+--------+
| 1 | 100000 |
+----+--------+
1 row in set (0.00 sec)
由于悲观锁在开始读取时即开始锁定,因此在并发访问较大的情况下性能会变差。对MySQL Inodb来说,通过指定明确主键方式查找数据会单行锁定,而查询范围操作或者非主键操作将会锁表。 接下来,我们看一下如何使用乐观锁解决这个问题,首先我们为counter表增加一列字段:
mysql> select * from counter;
+----+------+---------+
| id | num | version |
+----+------+---------+
| 1 | 1000 | 1000 |
+----+------+---------+
1 row in set (0.01 sec)
实现方式如下:
<?php
function dummy_business() {
$conn = mysqli_connect(‘127.0.0.1‘, ‘public‘, ‘public‘) or die(mysqli_error());
mysqli_select_db($conn, ‘test‘);
for ($i = 0; $i < 10000; $i++) {
mysqli_query($conn, ‘BEGIN‘);
$rs = mysqli_query($conn, ‘SELECT num, version FROM counter WHERE id = 1‘);
mysqli_free_result($rs);
$row = mysqli_fetch_array($rs);
$num = $row[0];
$version = $row[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‘);
$i--;
continue;
} else {
mysqli_query($conn, ‘COMMIT‘);
}
}
mysqli_close($conn);
} for ($i = 0; $i < 10; $i++) {
$pid = pcntl_fork(); if($pid == -1) {
die(‘can not fork.‘);
} elseif (!$pid) {
dummy_business();
echo ‘quit‘.$i.PHP_EOL;
break;
}
}
?>
这次,我们也得到了期望的结果:
mysql> select * from counter;
+----+--------+---------+
| id | num | version |
+----+--------+---------+
| 1 | 100000 | 100000 |
+----+--------+---------+
1 row in set (0.01 sec)
由于乐观锁最终执行的方式相当于原子化UPDATE,因此在性能上要比悲观锁好很多。 在使用Doctrine ORM框架的环境中,Doctrine原生提供了对悲观锁和乐观锁的支持。具体的使用方式请参考手册:http://docs.doctrine-project.org/projects/doctrine-orm/en/latest/reference/transactions-and-concurrency.html#locking-support
Hibernate框架中同样提供了对两种锁的支持,在此不再赘述了。 在高性能系统中处理并发问题,受限于后端数据库,无论何种方式加锁性能都无法高效处理如电商秒杀抢购量级的业务。使用NoSQL数据库、消息队列等方式才能更有效地完成业务的处理。
PHP使用数据库的并发问题(转)的更多相关文章
- PHP使用数据库的并发问题
在并行系统中并发问题永远不可忽视.尽管PHP语言原生没有提供多线程机制,那并不意味着所有的操作都是线程安全的.尤其是在操作诸如订单.支付等业务系统中,更需要注意操作数据库的并发问题. 接下来我通过一个 ...
- 【巨杉数据库SequoiaDB】巨杉Tech | 巨杉数据库的并发 malloc 实现
本文由巨杉数据库北美实验室资深数据库架构师撰写,主要介绍巨杉数据库的并发malloc实现与架构设计.原文为英文撰写,我们提供了中文译本在英文之后. SequoiaDB Concurrent mallo ...
- MySQL数据库高并发优化配置
在Apache, PHP, mysql的体系架构中,MySQL对于性能的影响最大,也是关键的核心部分.对于Discuz!论坛程序也是如此,MySQL的设置是否合理优化,直接 影响到论坛的速度和承载量! ...
- hibernate5学习之理解数据库级并发
本文作者:苏生米沿 本文地址:http://blog.csdn.net/sushengmiyan/article/details/50551741 当我们谈起隔离的时候,我们总是假定两个物体直接要么隔 ...
- mysql解决数据库高并发
分表分库 数据库索引 redis缓存数据库 读写分离 负载均衡: 将大量的并发请求分担到多个处理节点,由于单个处理节点的故障不影响服务,负载均衡集群同事也实现了高可用性.
- spring学习笔记---数据库事务并发与锁详解
多事务运行并发问题 在实际应用中,往往是一台(或多台)服务器向无数客户程序提供服务,当服务器查询数据库获取数据时,如果没有采用必要的隔离机制,可能会存在数据库事务的并发问题,下面是一些常见的并发问题分 ...
- 多事务运行并发问题spring学习笔记——数据库事务并发与锁详解
多事务运行并发问题 在实际应用中,往往是一台(或多台)服务器向无数客户程序提供服务,当服务器查询数据库获取数据时,如果没有采用必要的隔离机制,可能会存在数据库事务的并发问题,下面是一些常见的并发问题分 ...
- 一段经典的node.js 数据库高并发实现
var proxy = new EventProxy();var status = "ready";var select = function(callback){ proxy.o ...
- powershel连接数据库监控数据库状态并发报警邮件
function Get-DatabaseData { [CmdletBinding()] param ( [string]$connectionString, ...
随机推荐
- 【POJ 2528】Mayor’s posters(线段树+离散化)
题目 给定每张海报的覆盖区间,按顺序覆盖后,最后有几张海报没有被其他海报完全覆盖.离散化处理完区间端点,排序后再给相差大于1的相邻端点之间再加一个点,再排序.线段树,tree[i]表示节点i对应区间是 ...
- 【LintCode】转换字符串到整数
问题描述: 实现atoi这个函数,将一个字符串转换为整数.如果没有合法的整数,返回0.如果整数超出了32位整数的范围,返回INT_MAX(2147483647)如果是正整数,或者INT_MIN(-21 ...
- Oracle触发器反向调用Java程序
导入jar包 在oracle中导入需要的jar包,我们把编辑好的java类打成jar包,直接在oarcle里面写简单的调用就可以了, 1.操作系统需要拥有支持loadjava命令的jdk. 2.加载j ...
- Linux 输出重定向>和>>的区别
>是定向输出到文件,如果文件不存在,就创建文件:如果文件存在,就将其清空:一般我们备份清理日志文件的时候,就是这种方法:先备份日志,再用`>`,将日志文件清空(文件大小变成0字节): &g ...
- BZOJ1010 [HNOI2008]玩具装箱toy
本文版权归ljh2000和博客园共有,欢迎转载,但须保留此声明,并给出原文链接,谢谢合作. 本文作者:ljh2000作者博客:http://www.cnblogs.com/ljh2000-jump/转 ...
- Shell变量的定义与赋值操作注意事项
1 shell变量是弱类型 * 声明变量不用声明类型 * 可以存储不同类型的内容 * 使用时要明确变量的类型 * 区分大小写 2 变量声明及赋值格式 2.1 格式 变量名=变量值 # 注意等号两侧不 ...
- CF 115B Lawnmower(贪心)
题目链接: 传送门 Lawnmower time limit per test:2 second memory limit per test:256 megabytes Description ...
- 三角形问题的解决复杂度O(n^3)和O(nlogn)的比较
问题描述: n条棍子组成一个三角形,使得三角形周少最大. 方法一: 暴力解则算法复杂度为O(n^3) #include<stdio.h> const int MAX_N=105 int m ...
- InteropBitmap指定内存,绑定WPF的Imag控件时刷新问题。
1.InteropBitmap指定内存,绑定WPF的Imag控件的Source属性 创建InteropBitmap的时候,像素的格式必须为PixelFormats.Bgr32, 如果不是的话在绑定到I ...
- 【Beta版本】冲刺-Day2
队伍:606notconnected 会议时间:12月10日 目录 一.行与思 二.站立式会议图片 三.燃尽图 四.代码Check-in 一.行与思 张斯巍(433) 今日进展:对登录界面做了相应的修 ...