Design Data-Intensive Applications 读书笔记二十一 第七章:序列化执行

序列化

我们之前看了并发会导致问题的场景,要解决并发问题前景很悲观。

1、隔离等级很难理解,而且各数据库对此的标准不统一。

2、编写应用代码时,很容易出现并发问题,因为很难知道哪里会出问题。

3、没有好的工具来检测竞态条件。理论上使用统计有帮助,但是没有用于实践。

 

这不是新问题,自上世界70年代就有的问题,但是解决方法也简单,就是序列化隔离。

序列化隔离等级一般被认为是最高的隔离等级,即便是事务是并行执行的,但是结果表现为好像事务是顺序执行一样,也就是数据库内部解决了所有竞态条件。

为什么不是所有人都使用序列化隔离?这需要从它的实现方法和性能说起。下面的章节我们会讨论:

1、字面意义上的顺序执行事务。

2、两步加锁,几十年来唯一可行的方法。

3、乐观并发控制技术如序列化快照隔离。

 

真实序列化

要解决并发问题的一个方法就是彻底移除并发,也就是一次执行一个事务,顺序执行。但这是直到2007年,开发者才认为这是可行的。如果说并发时为了性能,现在为什么认为移除并发是可行的?

1、内存现在价格变得便宜了,部分场景下可以将数据存在内存中,这样比起需要等待从磁盘中读取数据再执行的事务,直接执行事务的速度很快。

2、开发者们意识到,OLTP事务经常很短而且只做少量数据的读写。对比长时间执行的分析事务,一般是只读的,可以运行在一个一致性快照上,不需要运行在顺序执行循环中。

VoltDB/H-Store,Redis, 和Datomic实现了顺序执行事务。某些时候,单线程执行事务的效率高于并发执行,因为不需要加锁。但是它的吞吐量被单个CPU限制。为了将大部分工作运行在单线程中,事务需要重新设计。

 

在存储过程中打包事务

在数据库系统早期,事务的意图就是打包用户的活动。例如顶飞机票,需要查找路线,查找航班,优惠,座位,输入乘客信息,付款。设计者们认为如果能打包这些操作为一个事务,然后执行,整个过程很简洁。

但是用户的操作速度是很慢的。如果一个事务等待用户执行完所有的操作,那么数据库中就会有很多事务在等待;数据库的性能就会下降。因此绝大多数OLTP系统会让事务很短,避免在与用户交互的过程中等待用户。在网络中,这意味着一个事务是在一个HTTP请求中,事务不会横跨多个HTTP请求。

尽管在特定模式中移除了用户角色,事务还是以交互式的客户端/服务端模式执行的,一次执行一个语句。

在交互式事务中,大部分时间都花在了客户端和服务端的网络通信上了。如果数据库不允许并发,一次只执行一个事务,那么吞吐量会很糟糕。因为数据库需要将绝大部分时间花在等待当前事务给出下一个查询语句上。这种数据库就需要为了性能而考虑多事务并发了。

因此,单线程序列化执行事务的进程不允许交互式的多语句的事务。只允许提前向数据库提交完整的事务代码,作为一个存储过程。差别如图7-9,事务一次提供了内存中的所有数据,不需要等待网络通信和磁盘IO,执行很快。

Design Data-Intensive Applications 读书笔记二十一 第七章:序列化执行

 

存储过程的优劣

关系型数据库中存储过程已经存在一段时间了,从1999年成为SQL规范的一部分。它们有不少非议,原因如下:

1、每个数据库提供商都有自己的存储过程语言,但是一直落后于编程语言的发展,以今天的眼光看,它们很落后;并且绝大部分编程语言没有为它们提供依赖包来扩大生态。

2、在数据库上运行代码很难管理:对比于应用代码,很难调试,版本控制和部署,而且难以集成到监控系统中。

3、数据库比应用更关注性能,因为一个数据库会给多个应用提供服务。如果编写了质量低下的存储过程(运行时间长,或者占用内存),那么造成的影响数倍于应用端的劣质代码。

但是这些问题已经克服了。现在的存储过程已经废弃了PL/SQL,使用已有的广泛使用的编程语言  VoltDB 使用 Java 或者 Groovy, Datomic 使用 Java 或者 Clojure,  Redis使用 Lua。

使用存储过程和内存中数据,单线程执行事务就是可行的。不需要等待I/O,移除了并发机制的消耗,单线程可以获得不错的吞吐量。

VoltDB也使用单线程进行备份,不是将事务从一个节点拷贝到另一个节点,而是直接在每个备份上执行存储过程。因此,这需要存储过程是确定性的(在不同节点上执行要得到相同的结果)。例如:如果要使用当前的时间日期,就要使用特定的确定性API。

 

分区

序列化执行事务很容易,但是吞吐量受限于单个CPU。只读事务可以在任意的节点上运行,只要提供快照隔离。但是对于高写入吞吐的应用,单线程事务处理器会成为瓶颈。

为了扩展至多节点,多个CPU,你可以将数据分区(VoltDB支持分区)。如果你能找到分区方法,让每个事务都只需要读写一个分区上的数据,这样每个分区上的事务处理器都能独立运作,便能线性扩展吞吐量。但是对于任务需要接触多个分区的事务,数据库需要整合它接触的所有分区。存储过程也需要在接触的所有分区上同步执行。

因为跨分区事务需要额外的协调工作,比起单分区要慢上很多。 VoltDB表明跨分区事务的吞吐量大约为1000每秒,比单分区的吞吐量小一个数量级;并且没法通过增加机器来提升。

是否适用跨分区事务取决于应用使用数据结构。简单使用键值结构的数据容易划分区间,但是如果有多个第二索引,就需要很多的跨分区协调工作。

 

序列执行总结

特定条件下,可以使用序列化执行事务来实现序列化隔离。

1、事务需要短而且快,这样就可以执行一个慢的,容纳所有的事务的事务。

2、只适用于所有数据都存在内存中的情况。很少使用的数据可以存至磁盘,但是一旦单线程事务需要访问那些数据,系统还是会变慢。

3、写入吞吐量需要是单CPU能处理的级别,否则需要分区,引入跨分区协调工作。

4、可以使用跨分区事务,但是很难界定使用范围。