事务(Transaction)是数据库区别于文件系统的重要特性之一。在文件系统中,如果正在写文件,但是操作系统突然崩溃了,这个文件就很有可能被破坏。当然,有一些机制可以把文件恢复到某个时间点。不过,如果需要保证两个文件同步,这些文件系统可能就显得无能为力了。例如,在需要更新两个文件时,更新完一个文件后,在更新完第二个文件之前系统重启了,就会有两个不同步的文件。这正是数据库系统引入事务的主要目的:事务会把数据库从一种一致状态转换为另一种一致状态。在数据库提交工作时,可以确保要么所有修改都已经保存了,要么所有修改都不保存。

ACID

事务必须同时满足四个特性,即通常所说的事务的ACID特性。ACID是以下4个词的缩写: - 原子性(atomicity) - 一致性(consistency) - 隔离性(isolation) - 持久性(durability)

A(Atomicity)原子性

原子性指整个数据库事务是不可分割的工作单位。只有使事务中所有的数据库操作都执行成功,才算整个事务成功。事务中任何一个SQL语句执行失败,已经执行成功的SQL语句也必须撤销,数据库状态应该退回到执行事务前的状态。

如果事务中的操作都是只读的,要保持原子性是很简单的。一旦发生任何错误,要么重试,要么返回错误代码。因为只读操作不会改变系统中的任何相关部分。但是,当事务中的操作需要改变系统中的状态时,例如插入记录或更新记录,那么情况可能就不像只读操作那么简单了。如果操作失败,很有可能引起状态的变化,因此必须要保护系统中并发用户访问受影响的部分数据。

C(consistency)一致性

一致性指事务将数据库从一种状态转变为下一种一致的状态。在事务开始之前和事务结束以后,数据库的完整性约束没有被破坏。例如,在表中有一个字段为姓名,为唯一约束,即在表中姓名不能重复。如果一个事务对姓名字段进行了修改,但是在事务提交或事务操作发生回滚后,表中的姓名变得非唯一了,这就破坏了事务的一致性要求,即事务将数据库从一种状态变为了一种不一致的状态。因此,事务是一致性的单位,如果事务中某个动作失败了,系统可以自动撤销事务——返回初始化的状态。

I(isolation)隔离性。

隔离性还有其他的称呼,如并发控制(concurrency control)、可串行化(serializability)、锁(locking)等。事务的隔离性要求每个读写事务的对象对其他事务的操作对象能相互分离,即该事务提交前对其他事务都不可见,通常这使用锁来实现。当前数据库系统中都提供了一种粒度锁(granular lock)的策略,允许事务仅锁住一个实体对象的子集,以此来提高事务之间的并发度。

隔离级别

出于性能考虑,许多数据库允许使用者选择牺牲隔离属性来换取并发度,从而获得性能的提升。SQL定义了4种隔离级别: - Read Uncommitted(RU):读取未提交的数据,即其他事务已经修改但还未提交的数据,这是最低的隔离级别 - Read Committed(RC):读取已提交的数据,但是,在一个事务中,对同一个项,前后两次读取的结果可能不一样,例如第一次读取时另一个事务的修改还没有提交,第二次读取时已经提交了 - Repeatable Read(RR):可重复读取,在一个事务中,对同一个项,确保前后两次读取的结果一样 - Serializable(S):可序列化,即数据库的事务是可串行化执行的,就像一个事务执行的时候没有别的事务同时在执行,这是最高的隔离级别。

隔离级别的降低可能导致读到脏数据或者事务执行异常,例如:

  • Lost Update(LU):第一类丢失更新:两个事务同时修改一个数据项,但后一个事务中途失败回滚,则前一个事务已提交的修改都可能丢失
  • Dirty Reads(DR):脏读,一个事务读取了另外一个事务更新却没有提交的数据项
  • Non-Repeatable Reads(NRR):不可重复读,一个事务对同一数据项的多次读取可能得到不同的结果
  • Second Lost Updates problem(SLU):第二类丢失更新:两个并发事务同时读取和修改同一数据项,则后面的修改可能使得前面的修改失效;
  • Phantom Reads(PR):事务执行过程中,由于前面的查询和后面的查询的期间有另外一个事务插入数据,后面的查询结果出现了前面查询结果中未出现的数据。

下表说明了隔离级别与读写异常(不一致)的关系。容易发现,所有的隔离级别都保证不会出现第一类丢失更新,另外,在最高隔离级别(Serializable)下,数据不会出现读写的不一致。

LU DR NRR SLU PR
RU N Y Y Y Y
RC N N Y Y Y
RR N N N N Y
S N N N N N

注:在InnoDB的实现中,在RR隔离级别下采用Next-Key Locking机制来避免了PR。

D(durability)持久性

事务一旦提交,其结果就是永久性的。即使发生宕机等故障,数据库也能将数据恢复。需要注意的是,只能从事务本身的角度来保证结果的永久性。例如,在事务提交后,所有的变化都是永久的。即使当数据库因为崩溃而需要恢复时,也能保证恢复后提交的数据都不会丢失。但若不是数据库本身发生故障,而是一些外部的原因,如RAID卡损坏、自然灾害等原因导致数据库发生问题,那么所有提交的数据可能都会丢失。因此持久性保证事务系统的高可靠性(High Reliability),而不是高可用性(High Availability)。对于高可用性的实现,事务本身并不能保证,需要一些系统共同配合来完成。

常见数据库对ACID的支持情况

虽然理论上定义了严格的事务要求,但是数据库厂商出于各种目的,并没有严格去满足事务的ACID标准。例如,对于MySQL的NDB Cluster引擎来说,虽然其支持事务,但是不满足D的要求,即持久性的要求。对于Oracle数据库来说,其默认的事务隔离级别为READ COMMITTED,不满足I的要求,即隔离性的要求。虽然在大多数的情况下,这并不会导致严重的结果,甚至可能还会带来性能的提升,但是用户首先需要知道严谨的事务标准,并在实际的生产应用中避免可能存在的潜在问题。

InnoDB Transaction Implement

对于InnoDB存储引擎而言,其默认的事务隔离级别为READ REPEATABLE,完全遵循和满足事务的ACID特性。事务隔离性由锁和MVCC来实现。原子性、一致性、持久性通过数据库的redo log和undo log来完成。redo log称为重做日志,用来保证事务的原子性和持久性。undo log用来保证事务的一致性。

redo和undo的作用都可以视为是一种恢复操作,redo恢复提交事务修改的页操作,而undo回滚行记录到某个特定版本。因此两者记录的内容不同,redo通常是物理日志,记录的是页的物理修改操作。undo是逻辑日志,根据每行记录进行记录。

References

[1] 姜承尧 《MySQL技术内幕 : InnoDB存储引擎(第2版) 》

[2] 杨传辉 《大规模分布式存储系统》:原理解析与架构实战