PHP使用数据库的并发问题
作者:网络转载 发布时间:[ 2015/8/21 13:52:51 ] 推荐标签:数据库
改过的脚本,将原来的原子操作UPDATE换成了先查询再更新,再次运行我们发现,由于并发的缘故程序并没有按我们期望的执行:
1 mysql> select * from counter;
2 +----+------+
3 | id | num |
4 +----+------+
5 | 1 | 21495|
6 +----+------+
7 1 row in set (0.00 sec)
程序员特别容易犯的错误是,认为这是没开启事务引起的。现在我们给它加上事务:
1 <?php
2 function dummy_business() {
3 $conn = mysqli_connect('127.0.0.1', 'public', 'public') or die(mysqli_error());
4 mysqli_select_db($conn, 'test');
5 for ($i = 0; $i < 10000; $i++) {
6 mysqli_query($conn, 'BEGIN');
7 $rs = mysqli_query($conn, 'SELECT num FROM counter WHERE id = 1');
8 mysqli_free_result($rs);
9 $row = mysqli_fetch_array($rs);
10 $num = $row[0];
11 mysqli_query($conn, 'UPDATE counter SET num = '.$num.' + 1 WHERE id = 1');
12 if(mysqli_errno($conn)) {
13 mysqli_query($conn, 'ROLLBACK');
14 } else {
15 mysqli_query($conn, 'COMMIT');
16 }
17 }
18 mysqli_close($conn);
19 }
20
21 for ($i = 0; $i < 10; $i++) {
22 $pid = pcntl_fork();
23
24 if($pid == -1) {
25 die('can not fork.');
26 } elseif (!$pid) {
27 dummy_business();
28 echo 'quit'.$i.PHP_EOL;
29 break;
30 }
31 }
32 ?>
依然没能解决问题:
1 mysql> select * from counter;
2 +----+------+
3 | id | num |
4 +----+------+
5 | 1 | 16328|
6 +----+------+
7 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语句)。
乐观锁认为,别人访问正在改变的数据的概率是很低的,因此直到修改完成准备提交所做的的修改到数据库的时候才会将数据锁住,完成更改后释放。
上面的例子,我们用悲观锁来实现:
1 <?php
2 function dummy_business() {
3 $conn = mysqli_connect('127.0.0.1', 'public', 'public') or die(mysqli_error());
4 mysqli_select_db($conn, 'test');
5 for ($i = 0; $i < 10000; $i++) {
6 mysqli_query($conn, 'BEGIN');
7 $rs = mysqli_query($conn, 'SELECT num FROM counter WHERE id = 1 FOR UPDATE');
8 if($rs == false || mysqli_errno($conn)) {
9 // 回滚事务
10 mysqli_query($conn, 'ROLLBACK');
11 // 重新执行本次操作
12 $i--;
13 continue;
14 }
15 mysqli_free_result($rs);
16 $row = mysqli_fetch_array($rs);
17 $num = $row[0];
18 mysqli_query($conn, 'UPDATE counter SET num = '.$num.' + 1 WHERE id = 1');
19 if(mysqli_errno($conn)) {
20 mysqli_query($conn, 'ROLLBACK');
21 } else {
22 mysqli_query($conn, 'COMMIT');
23 }
24 }
25 mysqli_close($conn);
26 }
27
28 for ($i = 0; $i < 10; $i++) {
29 $pid = pcntl_fork();
30
31 if($pid == -1) {
32 die('can not fork.');
33 } elseif (!$pid) {
34 dummy_business();
35 echo 'quit'.$i.PHP_EOL;
36 break;
37 }
38 }
39 ?>
可以看到,这次业务以期望的方式正确执行了:
1 mysql> select * from counter;
2 +----+--------+
3 | id | num |
4 +----+--------+
5 | 1 | 100000 |
6 +----+--------+
7 1 row in set (0.00 sec)
由于悲观锁在开始读取时即开始锁定,因此在并发访问较大的情况下性能会变差。对MySQL Inodb来说,通过指定明确主键方式查找数据会单行锁定,而查询范围操作或者非主键操作将会锁表。
接下来,我们看一下如何使用乐观锁解决这个问题,首先我们为counter表增加一列字段:
1 mysql> select * from counter;
2 +----+------+---------+
3 | id | num | version |
4 +----+------+---------+
5 | 1 | 1000 | 1000 |
6 +----+------+---------+
7 1 row in set (0.01 sec)
实现方式如下:
1 <?php
2 function dummy_business() {
3 $conn = mysqli_connect('127.0.0.1', 'public', 'public') or die(mysqli_error());
4 mysqli_select_db($conn, 'test');
5 for ($i = 0; $i < 10000; $i++) {
6 mysqli_query($conn, 'BEGIN');
7 $rs = mysqli_query($conn, 'SELECT num, version FROM counter WHERE id = 1');
8 mysqli_free_result($rs);
9 $row = mysqli_fetch_array($rs);
10 $num = $row[0];
11 $version = $row[1];
12 mysqli_query($conn, 'UPDATE counter SET num = '.$num.' + 1, version = version + 1 WHERE id = 1 AND version = '.$version);
13 $affectRow = mysqli_affected_rows($conn);
14 if($affectRow == 0 || mysqli_errno($conn)) {
15 // 回滚事务重新提交
16 mysqli_query($conn, 'ROLLBACK');
17 $i--;
18 continue;
19 } else {
20 mysqli_query($conn, 'COMMIT');
21 }
22 }
23 mysqli_close($conn);
24 }
25
26 for ($i = 0; $i < 10; $i++) {
27 $pid = pcntl_fork();
28
29 if($pid == -1) {
30 die('can not fork.');
31 } elseif (!$pid) {
32 dummy_business();
33 echo 'quit'.$i.PHP_EOL;
34 break;
35 }
36 }
37 ?>
这次,我们也得到了期望的结果:
1 mysql> select * from counter;
2 +----+--------+---------+
3 | id | num | version |
4 +----+--------+---------+
5 | 1 | 100000 | 100000 |
6 +----+--------+---------+
7 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数据库、消息队列等方式才能更有效地完成业务的处理。
参考文章
数据库事务隔离级别
MySQL数据库事务的隔离级别介绍
数据库事务并发带来的问题
乐观锁与悲观锁
乐观锁和悲观锁说的什么意思
本文内容不用于商业目的,如涉及知识产权问题,请权利人联系SPASVO小编(021-61079698-8054),我们将立即处理,马上删除。
相关推荐
在测试数据库性能时,需要注意哪些方面的内容?测试管理工具TC数据库报错的原因有哪些?怎么解决?数据库的三大范式以及五大约束编程常用的几种时间戳转换(java .net 数据库)优化mysql数据库的几个步骤数据库并行读取和写入之Python实现深入理解数据库(DB2)缓冲池(BufferPool)国内三大云数据库测试对比预警即预防:6大常见数据库安全漏洞数据库规划、设计与管理数据库-事务的概念SQL Server修改数据库物理文件存在位置使用PHP与SQL搭建可搜索的加密数据库用Python写一个NoSQL数据库详述 SQL 中的数据库操作详述 SQL 中的数据库操作Java面试准备:数据库MySQL性能优化

sales@spasvo.com