原载于我的博客 http://starlight36.com/post/php-db-concurrency
在并行系统中并发问题永远不可忽视。尽管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数据库、消息队列等方式才能更有效地完成业务的处理。
转自:http://my.oschina.net/starlight36/blog/344986
相关推荐
EasyExcel 并发读取文件字段并进行校验,数据写入到新文件,批量插入数据到数据库 demo
第7章介绍了使用基于Web的数据库处理,包括开放数据库连接(ODBC)和PHP脚本语言的使用。本章也讨论了可扩展标记语言(XML)的出现和基本概念。 第8章介绍了商业智能(BI)系统和支持它们的数据仓库体系结构,还讨论了多维...
最近在研究PHP,很喜欢,碰到PHP并发查询MySQL的问题,研究了一下,顺便留个笔记: 同步查询 这是我们最常的调用模式,客户端调用Query[函数],发起查询命令,等待结果返回,读取结果;再发送第二条查询命令,等待...
最近在处理项目的充值接口时遇到了并发操作,导致数据库重复插入充值记录,找了很多并发处理方法都没奏效,最后整理出一套成功的解决方案,用php文件锁定的方法来处理并发事件。亲测:非常好用,希望可以为你们的问题...
本文实例讲述了PHP使用文件锁解决高并发问题。分享给大家供大家参考,具体如下: 新建一个.txt文件,文件中什么都不用写。 【一】.阻塞(等待)模式:(只要有其他进程已经加锁文件,当前进程会一直等其他进程解锁文件) ...
我们可以使用C语言,C++,DELPHI写UDF,使用UDF(用户定义函数库)可以很容易的挂入数据库引擎中以扩展我们需要的功能 字符集:Firebird实现了很多国际标准的字符集,包括Unicode。 SQL标准兼容:Firebird 实现了全部...
不知是否能解决文本数据库并发数问题??? PHP ACCESS 单文件版 不太跨平台的一对组合 因PHP不能生成ACCESS的数据库 所以其实是两个文件 一个程序文件 一个数据库文件 对于要求不高的...
教你如何编写一份合格的技术简历 面试题 数据库缓存篇 面试题Linux,mvc,mysql,redis缓存,高并发常见面试题
不仅仅是CDN/Redis数据库索引等常用优化方法更有程序逻辑细节调整,产品策略技巧全新的优化思维4.从单机到web集群,从多服务器到多机房数据中心,服务器资源可随业务规模扩展,不局限于系统极限容量5.加入机器人服务...
同步器 用于同步 API 和数据库的 Laravel 包。... 从 ERP 接收并发送到数据库 从数据库接收并发送到 ERP 从数据库接收并作为 API 发送 什么? 很快。 Ps:这个包正在开发中,可能包含严重的错误。
对于第一个问题,已经很容易想到用缓存来处理抢购,避免直接操作数据库,例如使用Redis。 重点在于第二个问题 常规写法: 查询出对应商品的库存,看是否大于0,然后执行生成订单等操作,但是在判断库存是否大于0处,...
我们知道数据库处理sql...1.用额外的单进程处理一个队列,下单请求放到队列里,一个个处理,就不会有并发的问题了,但是要额外的后台进程以及延迟问题,不予考虑。 2.数据库乐观锁,大致的意思是先查询库存,然后立马将
多个进程,或者同一进程的多个线程可同时使用数据库,有如各自单独使用,底层的服务如加锁、事务日志、共享缓冲区管理、内存管理等等都由程序库透明地执行。 轻便灵活:可以运行于几乎所有的UNIX和Linux系统及其变种...
事物与并发控制,以及备份与恢复,并且掌握使⽤ SQL语句在数据库(例如MySQL)中实现这些技术的⽅法。 7. 了解数据库应⽤软件的设计与开发过程,理解和掌握关系数据库设计与实现的过程,初步掌握使⽤⼀种应⽤软件...
php结合redis实现高并发下的抢购
对于一些有一定用户量的电商网站,如果只是单纯的使用关系型数据库(如MySQL、Oracle)来做抢购,对数据库的压力是非常大的,而且如果不使用好数据库的锁机制,还会导致商品、优惠券超卖的问题。我所在的公司也遇到了...
假设在一个并发量较高的场景,数据库中num的值为1时,可能同时会有多个进程读取到num为1,程序判断符合条件,抢购成功,num减一。这样会导致商品超发的情况,本来只有10件可以抢购的商品,可能会有超过10个人抢到,...