事务并发和事务隔离级别

一、事务并发问题

1. 丢失更新

撤销一个事务时,把其他事务已提交的更新数据覆盖(A和B事务并发执行,A事务执行更新之后提交;B事务在A事务更新之后,B事务结束之前也做了对该行数据的更新操作,然后回滚,则两次更新操作都丢失了)。

2. 脏读

一个事务读取到另一个事务未提交的更新数据(A和B事务并发执行,B事务执行更新之后,A事务查询B事务没有提交的数据,B事务回滚,则A事务得到的数据不是数据库中的真实数据。也就是脏数据,即和数据库中不一致的数据)。

可以在修改数据时加排他锁,直到事务提交之后才释放;在读取时加共享锁,直到事务读取完之后才释放。例如:事务A读取数据时加上共享锁之后(这样在事务A读取数据的过程中,任何其他事务就不会修改该数据),不允许任何其他事务操作该数据,只能读取,之后事务A如果有更新操作,那么便会转换为排他锁,任何其他事务也就无权读写该数据,这样就防止了脏读问题。但是,在事务A读取数据的过程中,有可能其他事务也读取了该数据,读取完毕后释放共享锁,此时事务A正在修改数据,修改 完毕之后提交事务,其他事务再次读取数据时会发现数据不一致,这样便会出现不可重复读问题。

3. 不可重复读

一个事务读取到另一个事务已提交的更新数据(A和B事务并发执行,A事务查询数据,然后B事务更新该数据,A再次查询该数据时,发现该数据变化了)。

可以在读取数据时加共享锁,在写数据时加排他锁,这两种锁都是直到事务提交之后才释放。事务A读取数据时不允许其他事务修改该数据,不管该数据在事务过程中读取多少次,数据都是一致的,避免了不可重复读问题。

4. 覆盖更新

这是不可重复读中的特例,一个事务覆盖另一个事务已提交的更新数据(即A事务更新数据,然后B事务更新该数据,A事务查询发现自己更新的数据变了)。

5. 虚读(幻读)

一个事务读取到另一个事务已提交的新插入的数据(A和B事务并发执行,A事务查询数据,B事务插入或者删除数据,A事务再次查询发现结果集中有以前没有的数据或者以前有的数据消失了)。

可以采用范围锁(Range Lock)机制,锁定检索范围内的数据为只读,这样就避免了幻读问题。

二、事务隔离级别

锁机制是防止其他事务访问特定资源的一种手段。锁机制是实现并发控制的主要方法,是多个用户能够同时操纵同一个数据库中的数据而不发生数据不一致现象的重要保障。一般来说,锁可以防止脏读、不可重复读和幻读,锁机制在数据库系统中的表现方式便是用户可选的四种事务隔离级别(注意,事务的隔离级别受到数据库的限制,不同的数据库支持的的隔离级别不一定相同):

1. 序列化(SERIALIZABLE)

添加范围锁(也就是Range Lock,比如表锁、页锁等等),直到事务A结束。以此阻止事务B(或其他事务)对此范围内数据的insert、update等操作。

可能发生的问题:幻读、脏读、不可重复读等问题都不会发生。

2. 可重复读(REPEATABLE READ)

对于读出的记录,添加共享锁直到事务A结束。事务B(或其他事务)对这个记录的修改尝试会一直等待直到事务A结束之后才会开始。

可能发生的问题:当执行一个范围查询时,可能会发生幻读。

3. 已提交读(READ COMMITTED)

大多数数据库系统默认采用的事务隔离级别是已提交读。在事务A中读取数据时对记录添加共享锁,但读取结束之后立即释放。事务B(或其他事务)对这个记录的修改尝试会一直等待直到事务A中的读取过程结束之后才会开始,而不需要整个事务A的结束。所以,在事务A的不同阶段对同一记录的读取结果可能是不同的。

可能发生的问题:不可重复读。

4. 未提交读(READ UNCOMMITTED)

不添加共享锁。所以事务B(或其他事务)可以在事务A对记录的读取过程中修改同一记录,可能会导致A读取的数据是一个被破坏的或者说不完整、不正确的数据。

另外,在事务A中可以读取到事务B(未提交)中修改的数据。例如:事务B对R记录修改了,但未提交;此时,在事务A中读取R记录,读出的是被B修改过的数据。

可能发生的问题:脏读。

注意,对数据库使用何种隔离级别要审慎分析,因为:

  1. 维护一个最高的隔离级别虽然会防止数据的出错,但是却导致了并行度的损失,以及导致死锁出现的可能性增加。
  2. 然而,降低隔离级别,却会引起一些难以发现的bug。

综上所述,事务并发问题和事务隔离级别的对应关系如下表所示:

http://ghoulich.xninja.org/wp-content/uploads/sites/2/2016/01/image-1_transaction-relationship.png

三、事务并发问题和事务隔离级别示例

当执行不同的隔离级别时,可能会发生各种各样不同的并发问题。下面对它们进行总结并举例说明:

1. 幻读

  • 问题描述:

    当事务中两次完全相同的查询执行时,第二次查询所返回的结果集跟第一次查询的不相同。

  • 发生条件:

    没有使用范围锁(Range Lock)机制。

  • 示例:

    http://ghoulich.xninja.org/wp-content/uploads/sites/2/2016/01/image-2_phantom-read-example.png

  • 解决方案:

    一般解决幻读的方法是增加范围锁(Range Lock),将检索范围内的数据锁定为只读,这样便能避免幻读。四种事务隔离级别中,只有最高隔离级别序列化(SERIALIZABLE READ)才可以保证不出现幻读的问题,在任何一个低级别的隔离中都可能会发生幻读。

2. 不可重复读

  • 问题描述:

    在下面的示例中,事务B提交成功,它所做的修改已经可见。然而,事务A的第一次读取会取得该数据的旧值,第二次读取会取得该数据的新值,这就导致事务A中的两次读取结果不一致。在序列化和可重复读的隔离级别中,数据库管理系统会返回旧值,即在被事务B修改之前的值。在提交读和未提交读隔离级别下,可能会返回被更新的值,这就是“不可重复读”。

  • 发生条件:

    ① 在基于锁的并行控制方法中,如果在执行select时不添加读锁(共享锁),则会发生不可重复读问题。
    ② 在多版本并行控制机制中,当一个遇到提交冲突的事务需要回滚但却被释放时,则会发生不可重复读问题。

  • 示例:

    http://ghoulich.xninja.org/wp-content/uploads/sites/2/2016/01/image-3_unrepeatable-read-example.png

  • 解决方法:

    ① 推迟事务B的执行,直至事务A提交或者回滚,这种策略在使用锁时应用。
    ② 在多版本并行控制中,事务B可以先被提交,而事务A继续执行在旧版本的数据上。当事务A终于尝试提交时,数据库会检验它的结果是否和事务A、事务B顺序执行时一样。如果是,则事务A提交成功;如果不是,事务A会被回滚。

3. 脏读

  • 问题描述:

    事务A读取了被另一个事务B修改,但是还尚未提交的数据。假如事务B回滚,则事务A读取的是无效的数据。这跟不可重复读类似,但是第二个事务不需要执行提交。

  • 发生条件:

    一个事务可读取另一个事务的未提交数据,然后后者事务进行回滚。

  • 示例:

    http://ghoulich.xninja.org/wp-content/uploads/sites/2/2016/01/image-4_dirty-read-example.png

  • 解决方法:

    可以在修改数据时加排他锁,直到事务提交之后才释放;在读取时加共享锁,直到事务读取完之后才释放。