1.3 事务的隔离级别

要遵守事务的ACID性质,就会影响数据库的执行效率,例如事务的持久性需要通过记录事务Redo日志来保证,这会带来写放大的问题。因此,我们可以考虑放松一些ACID性质来提高数据库的执行效率。

仍然以银行转账为例,试想在转账过程中出现如下异常。

• 如果A账户中减去了转账金额,而B账户没有增加对应的金额,这破坏了事务的原子性。

• 如果要求A账户和B账户的总额为2000元,但是转账后账户的总额大于或小于2000元,这破坏了事务的一致性。

• 如果A账户或B账户在转账成功之后,B账户收到的金额突然丢失,这破坏了事务的持久性。

从这些异常可以看出,事务的原子性、一致性、持久性被破坏是不可容忍的(PostgreSQL的事务异步提交属于对事务持久性的放松),但可以考虑在事务的隔离性上做一些妥协。ANSI SQL标准中将事务的隔离性分成了不同的级别,分别是读未提交、读已提交、可重复读和可串行化,不同的隔离级别允许不同的异常现象发生。事务的隔离级别如表1-2所示。

表1-2 事务的隔离级别

PostgreSQL数据库默认的隔离级别是读已提交,同时可以支持可重复读和可串行化。用户可以在事务开始时通过设置ISOLATION LEVEL来指定隔离级别,也可以通过GUC参数transaction_isolation来指定,下面通过几个示例来说明不同的隔离级别在PostgreSQL中的实现。

1. 读未提交(Read Uncommitted)

通常来说,数据库系统不支持读未提交。当两个事务并发运行时,事务T1对元组t进行了更改,修改成了t',但是事务T1还没有把修改提交。假设事务T2去读这条元组,它不应该看到事务T1修改后的元组t',而应该看到事务T1修改之前的元组t。

假设事务T2看到了事务T1修改后的元组t',会怎样呢?事务T2对应的应用程序就可能使用t'来继续进行一些数据操作,但是最终如果事务T1没有提交,而是异常终止了,那么事务T2对这份数据的操作就是错误的,因为事务T1异常终止后,元组会回滚回t,这时候t'就“不存在”了。读未提交会导致脏读、不可重复读和幻读等异常现象的产生。

2. 读已提交(Read Committed)

目前,大部分商业数据库的默认隔离级别是读已提交,这个隔离级别既能满足大部分用户的需求,也能最大限度地提高并发度,如表1-3所示。

表1-3 读已提交示例

3. 可重复读(Repeatable Read)

PostgreSQL的可重复读隔离级别在事务开始时即获得快照,在事务的执行过程中都使用这个快照作为基准来读取数据,因此即使其他并发的事务已经提交,可重复读也保证不会读到这些数据,如表1-4所示。

表1-4 可重复读示例

4. 可串行化(Serializable)

老版本的PostgreSQL使用SI(Snapshot Isolation)隔离级别来替代可串行化隔离级别,但SI隔离有写偏序异常,直到SSI(Serializable Snapshot Isolation)实现之后才解决了该问题。SSI的实现可以参考第7章。