1.2 事务的性质

从数据库的应用层面来看,事务把一组操作看作一个逻辑单元,比如转账操作,它实际上是由两个操作组成的一个逻辑单元,A账户向B账户转账100元可以转换为如下两个操作。

• A账户有1000元,减去100元。

• B账户有1000元,增加100元。

假设A账户减去100元之后数据库所在的主机突然断电,那么在主机加电之后,数据库重新启动的时候,A账户刚刚减去的100元就需要重新“加回来”,这样看起来就好像转账这件事情没有发生过一样。也就是说,如果把转账这一组操作看作一个事务,这一组操作要么全做,要么全不做,这就是事务要满足的第一个性质:原子性

所谓原子性,就是将一组操作看作一个原子操作,这一组操作和原子一样不可再分,这就需要数据库能够提供一种机制:事务如果没有正常完成,那么数据应该能够回滚到事务开始之前的状态。事务回滚的方法各不相同,有些数据库通过记录Undo日志的方式对异常终止的事务进行回滚,有些数据库采用保留旧版本和获取快照的方式来判断数据的可见性。无论如何,为了事务能够回滚,都需要保留被修改元组的前像,无非是保存的位置不同。

事务提交之后,数据应该是事务执行之后的正确反映,除非有其他事务改变当前的状态,否则数据库就应该维持当前的数据状态。即使在软件错误或硬件故障的情况下,都应该能保证数据的状态不变,即数据不错、不丢,也就是保证数据的持久性,目前主要通过记录WAL(Write Ahead Log,预写式日志)的方式来保证事务的持久性

多个事务按照不同的顺序交叉执行会对数据造成不同的影响,如图1-1所示,和转账事务(事务1)同时进行的还有一个事务2,它想查看A和B两个账户的金额总和。事务2计算A和B两个账户的时机恰好是事务1的“中间状态”,这个状态是一个不一致的状态,因为无论转账是否发生,A和B账户中的钱的总额应该是不变的,事务2看到这种中间状态违背了事务的另一个性质,也就是隔离性

图1-1 事务的隔离性和事务的中间状态

事务的隔离性要求在多个事务并发执行的情况下,事务2要么能够看到事务1发生之前的状态,要么能够看到事务1提交之后的状态,不能看到事务1在执行过程中的状态。也就是说,对于事务2而言,它应该感觉不到其他事务的存在。而图1-1中事务2看到了事务1的中间状态,也就是事务1的执行对事务2的结果产生了影响,这违背了事务之间的隔离性。数据库通常采用并发控制方法来实现事务的隔离性。

事务还需要保证一致性,例如A账户和B账户的总额是2000元,如果给其中任何一个账户凭空减去100元,都会导致总额的降低。在应用层面,这种降低可能是不符合常识的,或者说是不一致的,因此通常需要借助完整性约束检查来保证数据的一致性。

需要说明的是,在图1-1中,事务2获得的是一种不一致状态,由于事务是由一组操作组成的一个逻辑单元,是一个原子操作,因此事务在执行过程中,必然会出现不一致状态,这种不一致状态不能被其他事务发现,但是不代表它不存在。这种不一致状态是事务的内部状态,它会随着事务中各个操作的执行最终回归到一致状态。

总之,事务需要满足以下4个性质。

原子性(Atomicity):一个事务要么全做,要么全不做。

一致性(Consistency):在应用层面,通过完整性约束保证数据一致。

隔离性(Isolation):不同的事务之间不会互相影响。

持久性(Durability):事务在提交之后,它对数据库的改变不会消失。

数据库需要通过并发控制(Concurrency Control)机制和故障恢复技术来支持事务的ACID性质,如表1-1所示。

表1-1 事务的性质和实现的方法

常用的并发控制技术有基于锁的并发控制和基于时间戳的并发控制,PostgreSQL数据库针对DDL语句采用两阶段锁技术,而针对DML语句则采用多版本控制技术(Multi-Version Concurrency Control,MVCC)。PostgreSQL数据库的故障恢复采用WAL日志的方式来实现,目前主要支持Redo日志,通过Redo日志和MVCC可以保证事务读写的一致性。

当前,PostgreSQL社区也在研发一个新的存储引擎——Zheap,它通过记录Undo日志来回滚异常终止的事务,这样就可以把元组的历史版本从原来的Heap页面中转移到Undo日志中,避免了在Heap中保留历史版本导致占用磁盘空间膨胀的问题。