文档章节

PHP使用数据库的并发问题

烫烫烫烫烫烫
 烫烫烫烫烫烫
发布于 2014/11/15 00:10
字数 1949
阅读 1494
收藏 56
点赞 2
评论 7

原载于我的博客 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数据库、消息队列等方式才能更有效地完成业务的处理。 

参考文章

© 著作权归作者所有

共有 人打赏支持
烫烫烫烫烫烫
粉丝 9
博文 7
码字总数 9897
作品 2
大连
技术主管
加载中

评论(7)

烫烫烫烫烫烫
烫烫烫烫烫烫

引用来自“水人”的评论

学习了!作为一个菜鸟,我想问的,这些成熟框架,应该已经处理了这些问题了吧。

引用来自“名字字数不够长”的评论

Doctrine ORM 和 Hibernate都提供了支持悲观锁和乐观锁的机制,用起来就可以了。

引用来自“水人”的评论

yii和thinkphp有此功能不。。。
这两个框架不了解
水人
水人

引用来自“水人”的评论

学习了!作为一个菜鸟,我想问的,这些成熟框架,应该已经处理了这些问题了吧。

引用来自“名字字数不够长”的评论

Doctrine ORM 和 Hibernate都提供了支持悲观锁和乐观锁的机制,用起来就可以了。
yii和thinkphp有此功能不。。。
烫烫烫烫烫烫
烫烫烫烫烫烫

引用来自“水人”的评论

学习了!作为一个菜鸟,我想问的,这些成熟框架,应该已经处理了这些问题了吧。
Doctrine ORM 和 Hibernate都提供了支持悲观锁和乐观锁的机制,用起来就可以了。
水人
水人
学习了!作为一个菜鸟,我想问的,这些成熟框架,应该已经处理了这些问题了吧。
钱途无梁
钱途无梁
谢谢分享,,一般会用悲观锁
无羁
无羁
码一个
五毛钱的饼
五毛钱的饼
mark
inhere/php-queue

php 的队列实现 php的队列使用包装, 默认自带支持 三个级别的队列操作。 基于数据库(mysql/sqlite)的队列实现 基于 php 实现 基于 redis 实现 - 操作具有原子性,并发操作不会有问题 基于共享...

inhere ⋅ 2017/06/13 ⋅ 0

异步 mysql 客户端--async-mysql-php

PHP异步并发访问mysql简单实现。 在实际的开发过程中,我们常常会遇到需要操作多张表,多个库的情况。有时因为一些限制我们不能进行连表(例如,异地数据库),所以只能用php串行访问后再在p...

呼延平 ⋅ 2015/07/23 ⋅ 1

mysql中的LAST_INSERT_ID()分析

今天跟人讨论php高并发下的LASTINSERTID的正确性问题,一开始大家都比较模糊,后来经过大家共同查询资料,对这个问题有了比较清晰的了解,特发此文,以为纪念。 首先看mysql中的LASTINSERTI...

赵开锦 ⋅ 2012/06/21 ⋅ 6

高并发处理方案

时常看到高并发的问题,但高并发其实是最不需要考虑的东西。为何,他虚无缥缈,很少有网站真的需要这些东西,而且其中很多技术,其实你已经在用了。有这个意识就够了,不需要时刻盯着这个问题...

hengfeng_su ⋅ 2012/09/15 ⋅ 0

Nginx 第三方模块-漫谈缘起

本文的部分内容和图片摘录于http://tengine.taobao.org/download/nginx@taobao.pdf 为什么要使用nginx模块?我个人觉得taobao这个ppt来做说明是最好不过了 从web服务器结构开始说起: 第一个...

王二狗子11 ⋅ 01/08 ⋅ 0

用php实现异步执行任务的队列(二)

六、队列具体实现三:写执行队列的程序 根据设计,执行队列的程序文件是 doqueue.php , 它的主要功能是把任务从队列表里取出来,并且在后台执行。 doqueue.php部分代码: 七、具体任务的业务...

_c_q ⋅ 2016/09/08 ⋅ 0

PHP进程锁--phplock

PHP在多进程模式下(并发的web访问)由于没有内置的锁支持,在处理一些资源的之后,很容易出现并发性问题。 在web开发中我们经常对我们的数据库耗时操作做缓存,但是可能出现一个陷阱,在缓存...

草屋主人 ⋅ 2009/12/02 ⋅ 0

PHP沉思录-第六篇-Drupal的性能问题-左轻侯-《程序员》2008年11月号

创建时间:2008-11-09 01:12:51 最后修改时间:2008-11-09 01:12:51 本文发表在《程序员》杂志2008年第11期 PHP沉思录之六:Drupal的性能问题 左轻侯 Drupal是一个基于PHP的开源CMS系统,也是...

一配 ⋅ 2015/10/16 ⋅ 1

PHP开发环境搭建

PHP开发环境搭建(win7版 2011年7月) 最近由于要学习PHP,小弟便决定自己动手搭建PHP开发 环境。不得不说,搭建PHP环境的伤不起啊!就俩字儿:苦逼!所以在此分享下我的苦逼经历,一是想让搭...

爱上绝对路径 ⋅ 2012/02/08 ⋅ 4

数据库连接到底该在什么时候关闭?

之前一直是用PHP,PHP是一次请求之后就会释放所有资源,所以也没有考虑过这样的问题。 那时候我基本上是写一个单例模式获取数据库的连接,一次页面请求都只用一次数据库连接。 这些天,学习p...

血荐轩辕 ⋅ 2013/11/16 ⋅ 4

没有更多内容

加载失败,请刷新页面

加载更多

下一页

前台对中文编码,后台解码

前台:encodeURI(sbzt) 后台:String param = URLDecoder.decode(sbzt,"UTF-8");

west_coast ⋅ 25分钟前 ⋅ 0

VS2015配置并运行汇编(一步一步照图做)【vs2017的链接在最后】

前言 我是上学期学的汇编,因为有vs又不想用课上教的麻烦的dosbox以及masm32,但是一直没找到高亮插件和能调试的(难在运行不了而找不到答案上,出现的错误在最后放出,还请先达们不吝指点)...

simpower ⋅ 35分钟前 ⋅ 0

一起读书《深入浅出nodejs》-node模块机制

node 模块机制 前言 说到node,就不免得提到JavaScript。JavaScript自诞生以来,经历了工具类库、组件库、前端框架、前端应用的变迁。通过无数开发人员的努力,JavaScript不断被类聚和抽象,...

小草先森 ⋅ 38分钟前 ⋅ 0

Java桌球小游戏

其实算不上一个游戏,就是两张图片,不停的重画,改变ball图片的位置。一个左右直线碰撞的,一个有角度碰撞的。 左右直线碰撞 package com.bjsxt.test;import javax.swing.*;import j...

森林之下 ⋅ 45分钟前 ⋅ 0

你真的明白RPC 吗?一起来探究 RPC 的实质

你真的明白RPC 吗?一起来探究 RPC 的实质 不论你是科班出身还是半路转行,这么优秀的你一定上过小学语文,那么对扩句和缩句你一定不陌生。缩句就是去除各种修饰提炼出一句话的核心,而不失基...

AI9o後 ⋅ 47分钟前 ⋅ 0

z-index设置失效?

今天碰到了一个问题,就是在给li设置提示框的时候,有用到遮罩效果,本来想把对应的出现在最顶层,可是不管将li设置的z-index值设为多大,li都没有出现在遮罩层之上。 我在网上查了z-index设...

IrisHunag ⋅ 54分钟前 ⋅ 0

CyclicBarrier、CountDownLatch以及Semaphore使用及其原理分析

CyclicBarrier、CountDownLatch以及Semaphore是Java并发包中几个常用的并发组件,这几个组件特点是功能相识很容易混淆。首先我们分别介绍这几个组件的功能然后再通过实例分析和源码分析其中设...

申文波 ⋅ 58分钟前 ⋅ 0

Java对象的序列化与反序列化

Java对象的序列化与反序列化

Cobbage ⋅ 今天 ⋅ 0

Sqoop

1.Sqoop: 《=》 SQL to Hadoop 背景 1)场景:数据在RDBMS中,我们如何使用Hive或者Hadoop来进行数据分析呢? 1) RDBMS ==> Hadoop(广义) 2) Hadoop ==> RDBMS 2)原来可以通过MapReduce I...

GordonNemo ⋅ 今天 ⋅ 0

全量构建和增量构建的区别

1.全量构建每次更新时都需要更新整个数据集,增量构建只对需要更新的时间范围进行更新,所以计算量会较小。 2.全量构建查询时不需要合并不同Segment,增量构建查询时需要合并不同Segment的结...

无精疯 ⋅ 今天 ⋅ 0

没有更多内容

加载失败,请刷新页面

加载更多

下一页

返回顶部
顶部