可串行化¶
可串行化(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 中的可重复读隔离级别