`
wusuoya
  • 浏览: 637699 次
  • 性别: Icon_minigender_2
  • 来自: 成都
社区版块
存档分类
最新评论

PHP使用数据库的并发问题

 
阅读更多
摘要: 在并行系统中并发问题永远不可忽视。尽管PHP语言原生没有提供多线程机制,那并不意味着所有的操作都是线程安全的。尤其是在操作诸如订单、支付等业务系统中,更需要注意操作数据库的并发问题。 接下来我通过一个案例分析一下PHP操作数据库时并发问题的处理问题。

 

原载于我的博客 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

分享到:
评论

相关推荐

    PHP轻量级数据库框架PHP更简单高效的数据库操作方式

    从提供的压缩包文件名"upfor-juggler-7b1f55f"来看,这可能是一个名为Juggler的PHP数据库框架的版本。尽管没有具体代码可供分析,但可以推测Juggler可能包含了一些上述特性,如查询构建、ORM、事务处理等,旨在简化...

    通过缓存数据库结果提高PHP性能

    要使用“数据库更改通知”,必须确保OE用户拥有必要的权限,包括CHANGE NOTIFICATION和EXECUTE ON DBMS_CHANGE_NOTIFICATION。可以通过SQL命令行工具,如SQL*Plus,执行相应的GRANT命令来授予这些权限。同时,需要...

    EasyExcel 并发读取文件字段并进行校验,数据写入到新文件,批量插入数据到数据库

    下面我们将详细探讨如何使用EasyExcel实现并发读取文件字段、进行数据校验、将数据写入新文件以及批量插入到数据库的操作。 1. **EasyExcel介绍** EasyExcel是一款轻量级的Java Excel处理框架,它基于NIO和内存...

    php 办公管理系统 带数据库

    同时,可以使用事务处理来保证数据一致性,避免并发问题。 五、系统优化与安全 1. 数据库优化:合理设计表结构,避免冗余数据;使用索引提高查询速度;定期进行数据库维护,如清理无用数据、优化表结构。 2. 安全性...

    tp5(ThinkPHP 5.1)连接达梦数据库源码

    而达梦数据库(DAMENG DBMS)则是一种高性能的关系型数据库管理系统,尤其在处理大数据量和高并发场景下表现出色。本文将详细探讨如何使用ThinkPHP 5.1连接达梦数据库,以及相关的源码实现。 首先,我们需要了解...

    php中将session保存到数据库的函数类代码

    为了解决这个问题,我们可以将Session数据保存到数据库中,这样不仅可以实现分布式Session管理,还能提高数据的安全性和可靠性。 标题所提及的"php中将session保存到数据库的函数类代码",就是用来实现这一功能的。...

    php_redis并发insert

    由于Redis的`RPOP/LPOP`(右/左出队)操作是原子性的,这意味着即使有多个客户端同时尝试从队列中取出数据,它们也会按照FIFO(先进先出)的顺序依次执行,避免了并发问题。 此外,“pop操作是原子的”这一特点使得...

    PHP开源中医诊所后台管理【带数据库】.zip

    此项目使用PHP编写后端逻辑,处理HTTP请求,与数据库交互,并生成动态网页内容。 2. **MySQL数据库**:预配置的数据库可能使用MySQL或兼容的SQL数据库系统,如MariaDB。数据库用于存储诊所的患者信息、预约记录、...

    网页游戏五子棋Flash+PHP+文本数据库

    网页游戏五子棋Flash+PHP+文本数据库是一个基于经典五子棋游戏的在线平台,它巧妙地利用了PHP编程语言和简单的文本数据库来实现游戏功能,而无需依赖大型的SQL数据库系统。这样的设计使得游戏搭建变得简单且成本低廉...

    PHP数据库操作类

    在使用PHP数据库操作类时,开发者需要注意以下几点: - **安全性**: 使用参数化查询或预处理语句可以有效防止SQL注入攻击,这是非常重要的安全实践。 - **错误处理**: 应该适当地处理数据库操作可能出现的错误,...

    PHP并发查询MySQL的实例代码

    在PHP编程中,当涉及到与MySQL数据库的交互时,通常会遇到如何提高查询效率的问题。在上述描述中,主要讨论了两种查询方式:同步查询和并发查询。 **同步查询**是最常见的模式,它遵循顺序执行的逻辑。客户端通过...

    数据库考试试题数据库考试试题

    10. **数据库应用开发**:这部分可能涉及到如何在编程环境中(如Java、Python、PHP等)连接和操作数据库,使用ORM框架(如Hibernate、MyBatis)简化数据库操作。 以上是数据库考试可能涵盖的主要知识点,考生在准备...

    php并发解决案例(代码)

    在PHP编程中,处理并发请求是一项挑战,尤其是在高并发环境下,如何确保数据的一致性和系统性能是关键问题。本文将详细探讨在标题“php并发解决案例(代码)”中提到的并发控制策略,包括前端的并发curl请求和后端的...

    laravel分布式并发锁

    在 Laravel 中,我们可以使用各种类型的锁,如 Redis 锁、数据库锁等,来实现分布式环境中的并发控制。 1. Redis 锁:Redis 是一个内存数据库,提供高效的数据存储和检索功能,同时支持多种数据结构,如字符串、...

    PHP中Session可能会引起并发问题

    值得注意的是,PHP提供了多种Session存储机制,如使用数据库或特定的存储服务(例如AWS DynamoDB)。这些替代方案可以提供更好的并发控制和扩展性。例如,配置DynamoDB作为Session存储后,可以利用其内置的并发控制...

    一个mysql数据库连接类

    MySQL数据库是世界上最受欢迎的开源关系型数据库之一,而PHP是一种广泛使用的服务器端脚本语言,尤其在构建Web应用程序时。当结合使用时,PHP可以提供一个强大的平台来创建、读取、更新和删除数据库中的数据。这个...

    PHP秒杀系统 高并发高性能的极致挑战-

    - **数据库优化**:合理设计数据库表结构,使用索引提高查询效率。 - **缓存策略**:利用Redis等内存数据库存储热门数据,减少对后端数据库的压力。 - **消息队列**:引入RabbitMQ或Kafka等消息队列服务,异步处理...

    使用PHP和ASP创建数据库网络应用(CHM)

    2. PHP数据库连接:讲解如何使用PHP的mysqli或PDO扩展连接到不同的数据库,执行SQL查询,处理结果集。 3. ASP数据库连接:讲解如何使用ADO(ActiveX Data Objects)在ASP中建立数据库连接,执行SQL命令。 4. 动态...

Global site tag (gtag.js) - Google Analytics