在php与数据库的交互中,如果并发量大,并且都去进行数据库的修改的话,就有一个问题需要注意.数据的锁问题.就会牵扯数据库的事务跟隔离机制
数据库事务依照不同的事务隔离级别来保证事务的ACID特性,也就是说事务不是一开启就能解决所有并发问题。通常情况下,这里的并发操作可能带来四种问题:- 更新丢失:一个事务的更新覆盖了另一个事务的更新,这里出现的就是丢失更新的问题。
- 脏读:一个事务读取了另一个事务未提交的数据。
- 不可重复读:一个事务两次读取同一个数据,两次读取的数据不一致。
- 幻象读:一个事务两次读取一个范围的记录,两次读取的记录数不一致。
通常数据库有四种不同的事务隔离级别:隔离级别 | 脏读 | 不可重复读 | 幻读 | Read uncommitted | √ | √ | √ | Read committed | × | √ | √ | Repeatable read | × | × | √ | Serializable | × | × | × |
大多数数据库的默认的事务隔离级别是提交读(Read
committed),而MySQL的事务隔离级别是重复读(Repeatable
read)。对于丢失更新,只有在序列化(Serializable)级别才可得到彻底解决。不过对于高性能系统而言,使用序列化级别的事务隔离,可能引起死锁或者性能的急剧下降。因此使用悲观锁和乐观锁十分必要。
并发系统中,悲观锁(Pessimistic Locking)和乐观锁(Optimistic Locking)是两种常用的锁: - 悲观锁认为,别人访问正在改变的数据的概率是很高的,因此从数据开始更改时就将数据锁住,直到更改完成才释放。悲观锁通常由数据库实现(使用SELECT...FOR UPDATE语句)。
- 乐观锁认为,别人访问正在改变的数据的概率是很低的,因此直到修改完成准备提交所做的的修改到数据库的时候才会将数据锁住,完成更改后释放
***
以mysql为例子:
myisam存储引擎使用表缩
innodb使用行锁(明确指定了主键的情况下,否则也是表锁)与表锁
一般的做法是:
1 开启事务
2 进行数据更改
3 回滚或者提交
在具体的业务逻辑中,由于隔离机制的不同,导致结果的不同.
乐观锁与悲观锁使用的也比较多.
***有这么一张表
- 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, '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 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;
- }
- }
- ?>
复制代码 由于乐观锁最终执行的方式相当于原子化UPDATE,因此在性能上要比悲观锁好很多 |