《设计数据密集型应用/DDIA》精要翻译(五) :事务

1. 事务的ACID

虽然ACID我们已经说滥了,这里我想再说一下一致性和隔离性。

一致性

一致性在不同的术语中有不同的含义:

  • 在前面那篇博客中,我们讨论了副本之间的一致性(比如最终一致性、读已之写一致性等)
  • 在CAP中,一致性表示可线性化(即只要有一个客户端成功写入,别的客户端后续的读取必须能看到刚刚写入的值。 详见本系列第七篇。)
  • 在ACID中,指数据库在事务前后保持正确(比如转账前后两人总余额不变)

隔离性

在我N年前的一篇博客中,我列了数据库不同事务隔离级别以及在多个事务并发时可能遇到的问题:

脏读:事务A修改了一个数据,但未提交,事务B读到了事务A未提交的更新结果,如果事务A提交失败,事务B读到的就是脏数据。

不可重复读:在同一个事务中,对于同一份数据读取到的结果不一致。比如,事务B在事务A提交前读到的结果,和提交后读到的结果可能不同。不可重复读出现的原因就是事务并发修改记录,要避免这种情况,最简单的方法就是对要修改的记录加锁,这回导致锁竞争加剧,影响性能。另一种方法是通过MVCC可以在无锁的情况下,避免不可重复读。

幻读:在同一个事务中,同一个查询多次返回的结果不一致。事务A新增了一条记录,事务B在事务A提交前后各执行了一次查询操作,发现后一次比前一次多了一条记录。幻读可以通过加间隙锁的方式来解决。

2. 事务隔离级别与其实现

隔离级别

Read Uncommitted:最低的隔离级别,什么都不需要做,一个事务可以读到另一个事务未提交的结果。所有的并发事务问题都会发生。

Read Committed:只有在事务提交后,其更新结果才会被其他事务看见。可以解决脏读问题。

Repeated Read:在一个事务中,对于同一份数据的读取结果总是相同的,无论是否有其他事务对这份数据进行操作,以及这个事务是否提交。可以解决脏读、不可重复读。

Serialization:事务串行化执行,隔离级别最高,牺牲了系统的并发性。可以解决并发事务的所有问题。

实现

读已提交的实现

  1. 对于写操作: 加上行锁 (防止脏写)
  2. 对于读操作:数据库记住旧值,和持有写入锁的事务写入的新值。在另一个事务提交前,别的事务读到旧值;在另一个事务提交后,别的事务读到新值

可重复读的实现

通常使用MVCC(多版本并发控制)来实现, 比如mysql innodb引擎中的实现方式:
参考这篇文章

事务操作:

  • 每个事务在开始的时候都有一个唯一的且递增的id, 这个事务对数据库的任何写入,被写入的数据都会被标记上这个事务id
  • 数据库表中的每行都有一个created_by字段以及一个deleted_by字段,created_by的值是插入这行的事务id,deleted_by的值初始为空,如果有事务执行delete操作,就赋值为该事务id。数据库的垃圾回收机制会在没有事务访问被删除事务的时候执行回收操作
  • update操作在数据库中被解析成create和delete操作,比如事务tx1 update id = 1的行,那么数据库中实际上有两行:一行是delete by tx1, 一行是create by tx1

事务与快照的可见性:

  • 事务开始时,数据库会列出当时所有其他未提交或还在运行的事务,在整个事务过程中,忽略这些事务的写入(尽管他们可能在后面被提交)
  • 忽略所有被中止的事务的写入。
  • 忽略任何事务id大于自己的事务的写入

具体以InnoDB存储引擎来说:在事务隔离级别READ COMMITTED和REPEATABLE READ(InnoDB存储引擎的默认事务隔离级别)下,InnoDB存储引擎使用非锁定的一致性读。然而,对于快照数据的定义却不相同。在READ COMMITTED事务隔离级别下,对于快照数据,非一致性读总是读取被锁定行的最新一份快照数据。而在REPEATABLE READ事务隔离级别下,对于快照数据,非一致性读总是读取事务开始时的行数据版本。

在默认的事务隔离级别下,即REPEATABLE READ下,InnoDB存储引擎采用Next-Key Locking机制来避免Phantom Problem(幻像问题)。这点可能不同于与其他的数据库,如Oracle数据库,因为其可能需要在SERIALIZABLE的事务隔离级别下才能解决Phantom Problem。

序列化的实现

要实现序列化,有以下几种方式:

  • 真的序列化,一个接一个(ps: 应该不会有系统沦落到要用这种方式的)
  • 两阶段锁的方式(2PL, 不同于2PC!在数据库中被广泛使用。)
  • 可序列化的快照隔离(serializable snapshot isolation, SSI), 一种乐观并发控制技术
2PL

两阶段锁协议是指所有事务必须分两个阶段对数据项加锁和解锁:

  • 在对任何数据进行读、写操作之前,首先要申请并获得对该数据的*

所谓”两段”锁的含义是,事务分为两个阶段,第一阶段是获得*,也称为扩展阶段。在这阶段,事务可以申请获得任何数据项上的任何类型的锁但是不能释放任何锁。第二阶段是释放*,也称为收缩阶段。在这阶段,事务可以释放任何数据项上的任何类型的锁,但是不能再申请任何锁。

参考: 两阶段锁

SSI

SSI于2008年才被提出,它实现了完整的可序列化隔离级别,与MVCC相比只有很小的性能损失。虽然现在还处于实践中证明自己的阶段,但是前途无量。

2PL是一种悲观并发控制,而SSI是一种乐观并发控制。
SSI基于快照隔离(即上文MVVC),在它的基础上,SSI加入了一种检查写入冲突的算法,来决定终止哪些事务。

举个例子,如下图: 事务42先提交,并且成功了。当事务43提交时,发现事务42已经提交了与自己相冲突的写入,所以必须中止事务43。
《设计数据密集型应用/DDIA》精要翻译(五) :事务