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 数据库并发处理的更多相关文章
- 数据库并发处理 - 上的一把好"锁"
为什么要有锁? 我们都是知道,数据库中锁的设计是解决多用户同时访问共享资源时的并发问题.在访问共享资源时,锁定义了用户访问的规则.根据加锁的范围,MySQL 中的锁可大致分成全局锁,表级锁和行锁三类. ...
- Mongodb数据库学习
数据库 MongoDB (芒果数据库) 数据存储阶段 文件管理阶段 (.txt .doc .xls)优点 : 数据可以长期保存 可以存储大量的数据 使用简单 缺点 : 数据一致性差 数据查找修改不方便 ...
- 3、原生jdbc链接数据库之锁与事务
一.锁的概念1.作用:是保证数据的一致性,只能一个人修改数据,不能同时多用户修改2.分类:行级锁和表级锁 乐观锁和悲观锁 二.事务1.为了保证数据的一致性和完整性,让数据库的多项操作合并为一个整体 ...
- MongoDB 数据库(1)
数据库 MongoDB (芒果数据库) 数据存储阶段 文件管理阶段 (.txt .doc .xls) 优点 : 数据可以长期保存 可以存储大量的数据 使用简单 缺点 : 数据一致性差 数据查找修改不方 ...
- JSP应用开发 -------- 电纸书(未完待续)
http://www.educity.cn/jiaocheng/j9415.html JSP程序员常用的技术 第1章 JSP及其相关技术导航 [本章专家知识导学] JSP是一种编程语言,也是一种动 ...
- 一次MySQL(INNODB存储引擎) 死锁捉虫记
前言 任何系统不管在什么阶段都需要关注生产环境错误日志,最近几个月内,发现偶尔会出现数据库死锁情况.以前碰到的数据库类错误大部分是SQL语法造成的错误,来到新东家之后才第一次碰到死锁情况,以前是搞游戏 ...
- 利用java concurrent 包实现日志写数据库的并发处理
一.概述 在很多系统中,往往需要将各种操作写入数据库(比如客户端发起的操作). 最简单的做法是,封装一个公共的写日志的api,各个操作中调用该api完成自己操作日志的入库.但因为入数据库效率比较低,如 ...
- mysql数据库高并发处理
总体思想:短, 少, 分流 短: 1.页面静态化, 2.使用缓存 3.使用存储过程, 对于处理一次请求需要多次访问数据库的操作, 将操作整合到存储过程, 这样只需要一次数据库访问 4.延迟修改, 将修 ...
- C# Entity Framework并发处理
原网站:C# Entity Framework并发处理 在软件开发过程中,并发控制是确保及时纠正由并发操作导致的错误的一种机制.从 ADO.NET 到 LINQ to SQL 再到如今的 ADO.NE ...
随机推荐
- openwrt 的 inittab
文件位于 /etc/inittab, 内容如下: root@hbg:/# cat /etc/inittab ::sysinit:/etc/init.d/rcS S boot::shutdown:/et ...
- lua的string库
lua支持的所有字符类 . 任意字符 %a 字母 %c 控制字符 %d 数字 %l 小写字母 %p 标点字符 %s 空白符 %u 大写字母 %w 字母 ...
- mysql高级查询
高级查询: 1.连接查询 select * from Info,Nation #得出的结果称为笛卡尔积select * from Info,Nation where Info.Nation = Nat ...
- IE6下a标签失效(背景穿透)
background:fixed url(about:blank);有时候做感应区域的时候在ie6下不给背景就会感应不到,比如说当鼠标移到图片的左半边部分,鼠标手势变成向左的箭 头,比如说有些时候a标 ...
- git提交失败
git push "提示:更新被拒绝,因为您当前分支的最新提交落后于其对应的远程分支."的解决办法 本地已存在项目,需要先获取远端更新并与本地合并,再git push具体操作如下: ...
- ASP.NET中的Excel操作(OLEDB方式)
一:OLEDB方式操作Excel的个人理解 就是把要操作的Excel当作一个数据库,所有对Excel的操作,就变成了对“数据库”的操作.那么这时就需要有一个数据库的连接字符串. 代码如下: connS ...
- 使用日志记录功能查看PHP扩展的执行过程
了解过PHP内核的同学都知道,PHP的一次请求的生命周期 1.启动Apache后,PHP解释程序也随之启动.PHP调用各个扩展的MINIT方法,从而使这些扩展切换到可用状态 2.当一个页面请求发生时, ...
- samba连接提示“找不到网络路径”
在运行里面输入services.msc,打开服务管理界面. 根据服务的描述,找到了TCP/IP NetBIOS Helper服务描述中提到TCP/IP NetBIOS Helper服务能使用户能够共享 ...
- app.use(express.static)设置静态文件目录小解
app.use(path, function, [, function]) 功能: 为path注册中间函数,即根据path使用不同函数进行处理,默认path是"/",也就是处理任何 ...
- div盒布局
最近在应用程序中内嵌webkit浏览器显示网页,网页的布局是自适应的,采用盒布局模型,能够实现较好的自适应效果. <style> html,body { height: 100%; mar ...