数据库事务隔离级别

事务隔离级别主要用于事务之间的并发控制,保证事务并发执行时数据正确性。

事务并发交互中容易出现以下几种问题:

脏读(Dirty Read)
一个事务读取到另一个并发事务修改但尚未提交的数据,此数据未来可能提交也可能回滚。

不可重复读(Nonrepeatable Read)
一个事务重复读取以前曾经读取过的数据,发现数据发生了变化,两次读取结果不一致(被其他并发事务修改并提交)。

幻读(Phantom Read)
一个事务按给定条件两次请求的结果集合发生变化,比原来的结果集多了几条数据或少了几条数据(被其他并发事务修改并提交)。

隔离级别

ANSI/ISO的SQL标准中,定义了四个事务隔离级别:

隔离级别

肮脏读取

不可重复读取

幻象读取

读未提交

可能发生

可能发生

可能发生

读已提交

-

可能发生

可能发生

可重复读取

-

-

可能发生

可序列化

-

-

-

最高的隔离级别是可序列化级别,这个级别保证任何并发执行的事务就像按序执行一样产生确定的结果,这是最严格的隔离级别,但其并发性能是最差的。

更新丢失问题
第一类更新丢失(回滚丢失):
在事务A期间,事务B对数据进行了更新;在事务A回滚之后,覆盖了事务B已经提交的数据。
SQL标准没有定义这种现象,标准定义的所有隔离级别都不允许第一类丢失更新发生。

第二类更新丢失(覆盖丢失):
在事务A期间,事务B对数据进行了更新;在事务A提交之后,覆盖了事务B已经提交的数据。主要的问题在于”读取,计算,写回”这种操作方式,事务A读取并缓存了事务B修改之前的数据,在事务A提交的时候并没有校验缓存的数据是否仍然有效就直接提交了,导致事务B的更新丢失了。
这种更新丢失只有最高的可序列化隔离级别可以防止发生,其他级别都无法保证(PostgreSQL可以在Repeatable Read级别防止此类更新丢失,但有些数据库是不可以的),应该由客户程序自己加锁来杜绝此类问题的发生。

第二类更新丢失其实就是不可重复读问题,就是缓存了不可重复读的结果从而导致的问题

悲观锁(pessimistic locking)与乐观锁(optimistic locking)
为了防止不可重复读和幻读问题,客户程序可以通过加锁来解决。但是加锁是有代价的,会影响并发性能,所以又有了悲观锁和乐观锁之分。

悲观锁
在整个数据处理过程中,将数据处于锁定状态,其他需要这些数据的事务只能等待解锁后才能继续处理。悲观并发控制实际上是“先取锁再访问”的保守策略,为数据处理的安全提供了保证。但是在效率方面,处理加锁的机制会让数据库产生额外的开销,还有增加产生死锁的机会。在长事务中使用悲观锁会对系统的影响比较大,特别是在等待用户输入的时候不要持有悲观锁,因为用户的行为是不可预测的。

select * from sometable for update用于显示显式锁定结果集进行后续更新,锁定期间其他事务无法修改这些数据。也可以使用locak table锁表,但这样锁定的粒度太大,会严重影响系统性能,一般不要使用。

乐观锁
事务在提交数据更新之前,再次检查所缓存的数据有没有被其他事务修改,如果数据没有被修改则可以直接提交,如果数据已经被其他事务修改了,则当前事务需要回滚,然后重新开始当前事务。
一种可靠的乐观锁的实现是使用“多版本控制(multi-version control)”,即在每一行加一个version属性。修改这一行时将version增加1,写回数据库要检查当前的version值是否还是获取时的那个值了。如果还是,说明期间没有其他事务对其修改,直接提交即可,如果已经不是了,说明期间已经有别的事务修改了这一行,当前事务获取的数据已经过期了,事务失败回滚。
也可以通过为记录添加高精度时间戳来实现乐观锁。

死锁

事务A锁定了表的第m行
事务B锁定了表的第n行
然后事务A请求锁定表的第n行,而事务B请求锁定表的第的m行,这样死锁就产生了。数据库会检测此类死锁并使其中一个事务失败,但不要依赖这种行为。
应用程序中应该尽量保持一致的顺序来请求资源,从而减少死锁的发生。

PostgreSQL

PostgreSQL支持四个标准的事务隔离级,但实际上其内部只有三个隔离级:读已提交,可重复读,可序列化。如果设置PostgreSQL事务隔离级为读未提交,其实际上的隔离级设置为读已提交。而且在PostgreSQL的可重复读隔离级,幻读是不会发生的。PostgreSQL提供了比ANSI/ISO要求更高的隔离级。PostgreSQL内部使用MVCC(MultiVersion concurrency control)并发架构。
postgreSQL默认的隔离级别为读已提交。