数据库事务的隔离级别

来源:互联网 发布:积木式编程 编辑:程序博客网 时间:2024/05/04 12:11

事务(Transaction)是并发控制的基本单位。所谓的事务,它是一个操作序列,这些操作要么都执行,要么都不执行,它是一个不可分割的工作单位。例如,银行转账工作:从一个账号扣款并使另一个账号增款,这两个操作要么都执行,要么都不执行。所以,应该把它们看成一个事务。事务是数据库维护数据一致性的单位,在每个事务结束时,都能保持数据一致性。

针对上面的描述可以看出,事务的提出主要是为了解决并发情况下保持数据一致性的问题。

事务具有以下4个基本特征。

l  Atomic(原子性):事务中包含的操作被看做一个逻辑单元,这个逻辑单元中的操作要么全部成功,要么全部失败。

l  Consistency(一致性):只有合法的数据可以被写入数据库,否则事务应该将其回滚到最初状态。

l  Isolation(隔离性):事务允许多个用户对同一个数据进行并发访问,而不破坏数据的正确性和完整性。同时,并行事务的修改必须与其他并行事务的修改相互独立。

l  Durability(持久性):事务结束后,事务处理的结果必须能够得到固化。

数据库肯定是要被广大客户所共享访问的,那么在数据库操作过程中很可能出现以下几种不确定情况。

l  更新丢失(Lost update):两个事务都同时更新一行数据,但是第二个事务却中途失败退出,导致对数据的两个修改都失效了。这是因为系统没有执行任何的锁操作,因此并发事务并没有被隔离开来。

l  脏读取(Dirty Reads):一个事务开始读取了某行数据,但是另外一个事务已经更新了此数据但没有能够及时提交。这是相当危险的,因为很可能所有的操作都被回滚。

l  不可重复读取(Non-repeatable Reads):一个事务对同一行数据重复读取两次,但是却得到了不同的结果。例如,在两次读取的中途,有另外一个事务对该行数据进行了修改,并提交。

l  两次更新问题(Second lost updates problem):无法重复读取的特例。有两个并发事务同时读取同一行数据,然后其中一个对它进行修改提交,而另一个也进行了修改提交。这就会造成第一次写操作失效。

l  虚读(Phantom Reads):事务在操作过程中进行两次查询,第二次查询的结果包含了第一次查询中未出现的数据(这里并不要求两次查询的SQL语句相同)。这是因为在两次查询过程中有另外一个事务插入数据造成的。

l  数据库的隔离级别

为了避免上面出现的几种情况,在标准SQL规范中,定义了4个事务隔离级别,不同的隔离级别对事务的处理不同。

ü  未授权读取(Read Uncommitted):允许脏读取,但不允许更新丢失。如果一个事务已经开始写数据,则另外一个数据则不允许同时进行写操作,但允许其他事务读此行数据。该隔离级别可以通过“排他写锁”实现。

ü  授权读取(Read Committed):允许不可重复读取,但不允许脏读取。这可以通过“瞬间共享读锁”和“排他写锁”实现。读取数据的事务允许其他事务继续访问该行数据,但是未提交的写事务将会禁止其他事务访问该行。

ü  可重复读取(Repeatable Read):禁止不可重复读取和脏读取,但是有时可能出现幻影数据。这可以通过“共享读锁”和“排他写锁”实现。读取数据的事务将会禁止写事务(但允许读事务),写事务则禁止任何其他事务。

ü  序列化(Serializable):提供严格的事务隔离。它要求事务序列化执行,事务只能一个接着一个地执行,但不能并发执行。如果仅仅通过“行级锁”是无法实现事务序列化的,必须通过其他机制保证新插入的数据不会被刚执行查询操作的事务访问到。

       隔离级别越高,越能保证数据的完整性和一致性,但是对并发性能的影响也越大。对于多数应用程序,可以优先考虑把数据库系统的隔离级别设为Read Committed,它能够避免脏读取,而且具有较好的并发性能。尽管它会导致不可重复读、虚读和第二类丢失更新这些并发问题,在可能出现这类问题的个别场合,可以由应用程序采用悲观锁或乐观锁来控制。

       通过前面的介绍已经知道,通过选用不同的隔离等级就可以在不同程度上避免前面所提及的在事务处理中所面临的各种问题。所以,数据库隔离级别的选取就显得尤为重要,在选取数据库的隔离级别时,应该注意以下几个处理的原则:

       首先,必须排除“未授权读取”,因为在多个事务之间使用它将会是非常危险的。事务的回滚操作或失败将会影响到其他并发事务。第一个事务的回滚将会完全将其他事务的操作清除,甚至使数据库处在一个不一致的状态。很可能一个已回滚为结束的事务对数据的修改最后却修改提交了,因为“未授权读取”允许其他事务读取数据,最后整个错误状态在其他事务之间传播开来。

       其次,绝大部分应用都无须使用“序列化”隔离(一般来说,读取幻影数据并不是一个问题),此隔离级别也难以测量。目前使用序列化隔离的应用中,一般都使用悲观锁,这样强行使所有事务都序列化执行。

       剩下的也就是在“授权读取”和“可重复读取”之间选择了。我们先考虑可重复读取。如果所有的数据访问都是在统一的原子数据库事务中,此隔离级别将消除一个事务在另外一个并发事务过程中覆盖数据的可能性(第二个事务更新丢失问题)。这是一个非常重要的问题,但是使用可重复读取并不是解决问题的唯一途径。

       假设使用了“版本数据”,Hibernate会自动使用版本数据。Hibernate的一级Session缓存和版本数据已经为你提供了“可重复读取隔离”绝大部分的特性。特别是,版本数据可以防止二次更新丢失的问题,一级Session缓存可以保证持久载入数据的状态与其他事务对数据的修改隔离开来,因此如果使用对所有的数据库事务采用授权读取隔离和版本数据是行得通的。

       “可重复读取”为数据库查询提供了更好的效率(仅对那些长时间的数据库事务),但是由于幻影读取依然存在,因此没必要使用它(对于Web应用来说,一般也很少在一个数据库事务中对同一个表查询两次)。

       也可以同时考虑选择使用Hibernate的二级缓存,它可以如同底层的数据库事务一样提供相同的事务隔离,但是它可能弱化隔离。假如在二级缓存大量使用缓存并发策略,它并不提供重复读取语义(例如,后面章节中将要讨论的读写,特别是非严格读写),很容易可以选择默认的隔离级别:因为无论如何都无法实现“可重复读取”,因此就更没有必要拖慢数据库了。另一方面,可能对关键类不采用二级缓存,或者采用一个完全的事务缓存,提供“可重复读取隔离”。那么在业务中需要使用到“可重复读取”吗?如果你喜欢,当然可以那样做,但更多的时候并没有必要花费这个代价。

SQL99关于事务

 

要想让多个用户共享同一个数据库,首先要解决的问题是用户之间 
的相互隔离,即每个用户在访问数据库时,都感觉自己是在独占数据库 
似的,这有点象我们在操作系统中说的分时 
SQL99中,为了实现事务的完全隔离,定义了三种必须避免的现象: 
1
、读数据(dirty read):即事务在运行中读到了其它事务 
未提交的数据。 
2
、不可重复读(unrepeatable read):即事务在运行中再次读取 
同一数据时,发现其它事务的更新。 
3
幻象读(phantom read):即事务在运行中再次执行同一查 
询时,发现其它事务的更新。 
只有避免上面三种现象,才能实现真正意义上的隔离,或者称是可 
串行的。但是,此时事务之间因为冲突而等待的机会非常高,系统的性能 
可能难以达到预期的目标。 
为了实现隔离性与系统性能之间的平衡,SQL99定义了四种隔离级别, 
允许应用根据实际需要选择合适的隔离级别,这四种隔离级别的含义如下: 
数据 不可重复读幻象 
未提交读 N N N 
提交读 Y N N 
可重复读 Y Y N 
可串行 Y Y Y 
其中“N”表示在这种隔离级别下,对应的现象不可避免,而“Y” 
表示可以避免。 
当然,不管怎样,必须保证事务是可恢复的,所以SQL99还规定在未提 
交读这种隔离级别下,事务必须是只读的。 
DB2中,隔离级别是面向应用的,用户只有在应用被预编译(PREP 
或联编(BIND)时才能设置隔离级别;而ORACLE的隔离级别则是面向用户 
的,用户可以在每次事务开始时重新设置隔离级别。由于后者更为灵活, 
SQL99
最终采用了这种方式。 

相比之下,DB2的并发控制显得火候不足,仅仅是最基本的二阶段 
锁协议,并发度之低可以预料;按照我们做数据库的人的想法,最起码也得 
做个多版本什么的,这样至少可以实现只读事务不等待(在实际数据库系统 
中,只读事务出现的概率要远远高于更新事务)。真不明白IBM是怎么想的。 
ORACLE的并发控制则感觉是做过了头,通过实现读一致性,不仅 
只读事务不必等待,连只读操作都不必等待,当然由此而付出的代价是在可 
串行这种隔离级别(默认的隔离级别)下,更新操作因为冲突而使得整个事 
务被滚回的几率大大增加。搞不清楚为什么到现在还没人提意见,大概是等 
到他们发现这个问题时,已经无路可走了。 
不过话又说回来,想我们这种无名小辈,又有什么资格对这些大公司说 
三道四呢?人家纵然有千般不是,毕竟还是个实实在在可用的东西。而中国 
人搞研究的最大缺点就是:老是觉得一些事情没有理论研究的价值,而不愿 
去做一些具体的工作。 

 

原创粉丝点击