可串行化

可串行化(Serializability) 隔离通常被认为是最强的隔离级别。

目前大多数提供可串行化的数据库都使用了三种技术之一:

  • 字面意义上地串行顺序执行事务(真的串行执行)

  • 两阶段锁定(2PL, two-phase locking),几十年来唯一可行的选择

  • 乐观并发控制技术,例如 可串行化快照隔离(serializable snapshot isolation)

真正的串行执行

在单个线程上按顺序一次只执行一个事务。这样做就完全绕开了检测 / 防止事务间冲突的问题,由此产生的隔离,正是可串行化的。

尽管这似乎是一个明显的主意,但数据库设计人员只是在 2007 年左右才决定,单线程循环执行事务是可行的。主要是因为:

  • RAM 足够便宜了,事务需要访问的所有数据都在内存中时,执行速度足够块;

  • 数据库设计人员意识到 OLTP 事务通常很短,而且只进行少量的读写操作;

VoltDB/H-Store、Redis 和 Datomic 中实现了串行执行事务 。

在特定约束条件下,真的串行执行事务,已经成为一种实现可串行化隔离等级的可行办法。

  • 每个事务都必须小而快,只要有一个缓慢的事务,就会拖慢所有事务处理。

  • 仅限于活跃数据集可以放入内存的情况。很少访问的数据可能会被移动到磁盘,但如果需要在单线程执行的事务中访问这些磁盘中的数据,系统就会变得非常慢 [^x]

  • 写入吞吐量必须低到能在单个 CPU 核上处理,如若不然,事务需要能划分至单个分区,且不需要跨分区协调。

  • 跨分区事务是可能的,但是它们能被使用的程度有很大的限制。

备注

存储过程

存储过程在关系型数据库中已经存在了一段时间了,自 1999 年以来它们一直是 SQL 标准(SQL/PSM)的一部分。存储过程可以在数据库上真正的串行执行,但是出于各种原因,它们的名声有点不太好:

  • 每个数据库厂商都有自己的存储过程语言(Oracle 有 PL/SQL,SQL Server 有 T-SQL,PostgreSQL 有 PL/pgSQL,等等)。

    • 这些语言并没有跟上通用编程语言的发展,所以从今天的角度来看,它们看起来相当丑陋和陈旧,而且缺乏大多数编程语言中能找到的库的生态系统。

  • 在数据库中运行的代码难以管理

    • 与应用服务器相比,它更难调试,更难以保持版本控制和部署,更难测试,并且难以集成到指标收集系统来进行监控。

  • 数据库通常比应用服务器对性能敏感的多,因为单个数据库实例通常由许多应用服务器共享。

    • 数据库中一个写得不好的存储过程(例如,占用大量内存或 CPU 时间)会比在应用服务器中相同的代码造成更多的麻烦。

但是这些问题都是可以克服的。现代的存储过程实现放弃了 PL/SQL,而是使用现有的通用编程语言:VoltDB 使用 Java 或 Groovy,Datomic 使用 Java 或 Clojure,而 Redis 使用 Lua。

存储过程与内存存储,使得在单个线程上执行所有事务变得可行。由于不需要等待 I/O,且避免了并发控制机制的开销,它们可以在单个线程上实现相当好的吞吐量。

两阶段锁定

大约 30 年来,在数据库中只有一种广泛使用的串行化算法:两阶段锁定(2PL,two-phase locking)

注意

请注意,虽然两阶段锁定(2PL)听起来非常类似于两阶段提交(2PC),但它们是完全不同的东西。

在两阶段锁定中,只要没有写入,就允许多个事务同时读取同一个对象。但对象只要有写入(修改或删除),就需要 独占访问(exclusive access) 权限:

  • 如果事务 A 读取了一个对象,并且事务 B 想要写入该对象,那么 B 必须等到 A 提交或中止才能继续(这确保 B 不能在 A 底下意外地改变对象)。

  • 如果事务 A 写入了一个对象,并且事务 B 想要读取该对象,则 B 必须等到 A 提交或中止才能继续(像 图 7-1 那样读取旧版本的对象在 2PL 下是不可接受的)。

备注

2PL 用于 MySQL(InnoDB)和 SQL Server 中的可串行化隔离级别,以及 DB2 中的可重复读隔离级别

可串行化快照隔离