一、数据库事务
事务是数据库系统执行过程中最小的逻辑单位。当事务被提交时,数据库管理系统要确保一个事务中的所有操作都成功完成,并且在数据库中永久保存操作结果。如果一个事务中的一部分操作没有成功完成,则数据库管理系统会把数据库回滚到操作执行之前的状态。
二、事务并发时产生的问题
如果所有的事务都按照顺序执行,所有事务的执行时间没有重叠交错就不会存在事务并发性。如果以不受控制的方式允许具有交织操作的并发事务,则可能发生不期望的结果。这些不期望的结果可能被并发地写入和并发地读取而得到非预期的数据。可以把这些非预期的现象总结为:脏读(Dirty read)、不可重复读(Non-repeatable read)、幻读(Phantom Read)和序列化异常(Serialization Anomaly)。下面我们会分别举例演示这几种非预期的读现象。
1.脏读
当第一个事务读取了第二个事务中已经修改但还未提交的数据,包括INSERT、UPDATE、DELETE,当第二个事务不提交并执行ROLLBACK后,第一个事务所读取到的数据是不正确的,这种读现象称作脏读。
下面演示一个脏读的例子。首先创建一张测试表并插入测试数据,如下所示:
-- 创建一个测试用表
CREATE TABLE `t_table_test` (
`id` int not null auto_increment,
`ival` int,
PRIMARY KEY(`id`)
);
-- 插入一条数据
INSERT INTO `t_table_test`(`ival`) VALUES(1);
按照从上到下的顺序分别执行事务T1和事务T2如下所示:
T1 | T2 |
mysql> set session transaction isolation level read uncommitted; Query OK, 0 rows affected
mysql> start transaction; Query OK, 0 rows affected
mysql> select * from `t_table_test` where id=1; +----+------+ | id | ival | +----+------+ | 1 | 1 | +----+------+ 1 row in set |
|
| mysql> start transaction; Query OK, 0 rows affected
mysql> update `t_table_test` set `ival`=10 where id=1; Query OK, 1 row affected Rows matched: 1 Changed: 1 Warnings: 0 |
mysql> select * from `t_table_test` where id=1; +----+------+ | id | ival | +----+------+ | 1 | 10 | +----+------+ 1 row in set
mysql> |
|
| mysql> rollback; Query OK, 0 rows affected
mysql> |
上面的例子中,事务T1在t_table_test表中查询数据,得到id=1, ival=1的行,这时事务T2更新表中id=1的行的ival值为10,此时事务T1查询t_table_test表,而事务T2此时并未提交,ival预期的值理应等于1,但是事务T1却得到了ival等于10的值。事务T2最终进行了ROLLBACK操作,很显然,事务T1将得到错误的值,引发了脏读现象。
2、不可重复读
当一个事务第一次读取数据之后,被读取的数据被另一个已提交的事务进行了修改,事务再次读取这些数据时发现数据已经被另一个事务修改,两次查询的结果不一致,这种读现象称为不可重复读。下面演示一个不可重复读的例子。首先创建一张测试表并插入测试数据,如下所示:
-- 创建一个测试用表
CREATE TABLE `t_table_test` (
`id` int not null auto_increment,
`ival` int,
PRIMARY KEY(`id`)
);
-- 插入一条数据
INSERT INTO `t_table_test`(`ival`) VALUES(1);
按照从上到下的顺序分别执行T1和T2,如下所示:
T1 | T2 |
mysql> set session transaction isolation level read committed; Query OK, 0 rows affected
mysql> start transaction; Query OK, 0 rows affected
mysql> select * from `t_table_test` where id=1; +----+------+ | id | ival | +----+------+ | 1 | 1 | +----+------+ 1 row in set |
|
| mysql> BEGIN; Query OK, 0 rows affected
mysql> update `t_table_test` set `ival`=10 where id=1; Query OK, 1 row affected Rows matched: 1 Changed: 1 Warnings: 0
mysql> commit; Query OK, 0 rows affected |
mysql> select * from `t_table_test` where id=1; +----+------+ | id | ival | +----+------+ | 1 | 10 | +----+------+ 1 row in set
mysql> |
|
|
|
在上面的例子中,事务T1在t_table_test表中第一次查询数据,得到id=1, ival=1的行,这时事务T2更新表中id=1的行的ival值为10,并且事务T2成功地进行了COMMIT操作。此时事务T1查询t_table_test表,得到ival的值等于10,我们的预期是数据库在第二次SELECT请求的时候,应该返回事务T2更新之前的值,但实际查询到的结果与第一次查询得到的结果不同,由于事务T2的并发操作,导致事务T1不能重复读取到预期的值,这就是不可重复读的现象。
3、幻读
指一个事务的两次查询的结果集记录数不一致。例如一个事务第一次根据范围条件查询了一些数据,而另一个事务却在此时插入或删除了这个事务的查询结果集中的部分数据,这个事务在接下来的查询中,会发现有一些数据在它先前的查询结果中不存在,或者第一次查询结果中的一些数据不存在了,两次查询结果不相同,这种读现象称为幻读。幻读可以认为是受INSERT和DELETE影响的不可重复读的一种特殊场景。
首先创建一张测试表并插入测试数据,如下所示:
-- 创建一个测试用表
CREATE TABLE `t_table_test` (
`id` int not null auto_increment,
`ival` int,
PRIMARY KEY(`id`)
);
-- 插入一条数据
INSERT INTO `t_table_test`(`ival`) VALUES(1);
INSERT INTO `t_table_test`(`ival`) VALUES(2);
INSERT INTO `t_table_test`(`ival`) VALUES(3);
INSERT INTO `t_table_test`(`ival`) VALUES(4);
下面演示一个幻读的例子,使用创建的测试表t_table_test和表中的测试数据,如下所示:
T1 | T2 |
mysql> set session transaction isolation level read committed; Query OK, 0 rows affected
mysql> start transaction; Query OK, 0 rows affected
mysql> select * from `t_table_test` where id>3 and id<10; +----+------+ | id | ival | +----+------+ | 4 | 4 | +----+------+ 1 row in set
mysql> |
|
| mysql> BEGIN; Query OK, 0 rows affected
mysql> INSERT INTO `t_table_test`(`ival`) VALUES(5); Query OK, 1 row affected
mysql> commit; Query OK, 0 rows affected
mysql> |
mysql> select * from `t_table_test` where id>3 and id<10; +----+------+ | id | ival | +----+------+ | 4 | 4 | | 5 | 5 | +----+------+ 2 rows in set
mysql> |
|
|
|
在上面的例子中,事务T1在t_table_test表中第一次查询id大于3并且小于10的数据,得到两行数据,这时事务T2在表中插入了一条id等于5的数据,这条数据正好满足事务T1的WHERE条件中,id大于3并且小于10的查询条件,事务T1再次查询时,查询结果会多一条非预期的数据,像产生了幻觉。不可重复读和幻读很相似,它们之间的区别主要在于不可重复读主要受到其他事务对数据的UPDATE操作,而幻读主要受到其他事务INSERT和DELETE操作的影响。
4、序列化异常
序列化异常是指成功提交的一组事务的执行结果与这些事务按照串行执行方式的执行结果不一致。MySQL数据库会出现写SQL卡住,而PostgreSQL会报错。下面演示一个序列化异常的例子。
首先创建一张测试表并插入测试数据,如下所示:
-- 创建一个测试用表
CREATE TABLE `t_table_test` (
`id` int not null auto_increment,
`ival` int,
PRIMARY KEY(`id`)
);
-- 插入一条数据
INSERT INTO `t_table_test`(`ival`) VALUES(1);
按照从上到下的顺序分别执行T1和T2,如下所示:
T1 | T2 |
mysql> set session transaction isolation level repeatable read; Query OK, 0 rows affected
mysql> start transaction; Query OK, 0 rows affected
mysql> select * from `t_table_test` where id=1; +----+------+ | id | ival | +----+------+ | 1 | 1 | +----+------+ 1 row in set |
|
| mysql> BEGIN; Query OK, 0 rows affected
mysql> UPDATE `t_table_test` set ival = ival * 10 where id=1; Query OK, 1 row affected Rows matched: 1 Changed: 1 Warnings: 0 |
mysql> UPDATE `t_table_test` set ival = ival +2 where id=1; 1205 - Lock wait timeout exceeded; try restarting transaction |
|
| mysql> COMMIT; Query OK, 0 rows affected
|
mysql> select * from `t_table_test` where id=1; +----+------+ | id | ival | +----+------+ | 1 | 1 | +----+------+ 1 row in set
mysql> |
|
三、ANSI SQL标准的事务隔离级别
为了避免事务与事务之间并发执行引发的副作用,最简单的方法是串行化地逐个执行事务,但是串行化地逐个执行事务会严重降低系统吞吐量,降低硬件和系统的资源利用率。为此,ANSI SQL标准定义了四类隔离级别,每一个隔离级别都包括了一些具体规则,用来限定事务内外的哪些改变对其他事务是可见的,哪些是不可见的,也就是允许或不允许出现脏读、不可重复读,幻读的现象。通过这些事务隔离级别规定了一个事务必须与其他事务所进行的资源或数据更改相隔离的程度。这四类事务隔离级别包括:
- ❑ Read Uncommitted(读未提交):这是MySQL的默认事务隔离级别,在该隔离级别,所有事务都可以看到其他未提交事务的执行结果,在多用户数据库中,脏读是非常危险的,在并发情况下,查询结果非常不可控,即使不考虑结果的严谨性只追求性能,它的性能也并不比其他事务隔离级别好多少,可以说脏读没有任何好处。所以未提交读这一事务隔离级别很少用于实际应用。
- ❑ Read Committed(读已提交):这是PostgreSQL的默认隔离级别,它满足了一个事务只能看见已经提交事务对关联数据所做的改变的隔离需求。
- ❑ Repeatable Read(可重复读):确保同一事务的多个实例在并发读取数据时,会看到同样的数据行。
- ❑ Serializable(可序列化):这是最高的隔离级别,它通过强制事务排序,使之不可能相互冲突,从而解决幻读问题。简言之,它是在每个读的数据行上加上共享锁。在这个级别,可能导致大量的超时现象和锁竞争。
说明:可以采用下面这种方法查询Mysql中的当前事务隔离级别:
mysql> select @@tx_isolation;
+-----------------+
| @@tx_isolation |
+-----------------+
| REPEATABLE-READ |
+-----------------+
1 row in set
下面的表格是ANSI SQL标准定义的事务隔离级别与读现象的关系:
对于同一个事务来说,不同的事务隔离级别执行结果可能会不同。隔离级别越高,越能保证数据的完整性和一致性,但是需要更多的系统资源,增加了事务阻塞其他事务的概率,对并发性能的影响也越大,吞吐量也会更低;低级别的隔离级别一般支持更高的并发处理,并拥有更低的系统开销,但增加了并发引发的副作用的影响。对于多数应用程序,优先考虑Read Committed隔离级别。它能够避免脏读,而且具有较好的并发性能。尽管它会导致不可重复读、幻读和丢失更新这些并发问题,在可能出现这类问题的个别场合,可以由应用程序采用悲观锁或乐观锁来控制。