之前我们介绍了几种弱隔离性的概念和实现方式,本节我们继续讨论强隔离性保证:串行化。它保证了即使事务是以并行方式执行的,但结果和串行执行是一致的,也就是这种隔离性的保证可以避免所有可能的竞争状态。下面我们会分别介绍几种串行化的实现方式,以及它们各自的性能表现等。
真正的串行执行
最简单的实现方式是:使用一个线程,同一时间只执行一个事务。这种方式在很早就被提出,但最近被真正采用的原因主要有以下两个:
- RAM变得越来越便宜,可以保留更多的数据在内存中;
- 数据库设计者意识到,OLTP的事务通常非常快,而运行时间长的OLAP可以使用snapshot isolation实现。
使用单一线程处理所有事务,可能避免使用锁,它的瓶颈在只有单一CPU核工作,因此事务的结构需要进行针对性的调整。
在存储过程中封装事务
数据库事务设计的意图,是能够封装用户行为的全部流程。比如,预定一张航空机票的行为包括:查找路线和票价、可选的座位,确定行程,预定行程中每次飞行的座位,输入乘客信息,支付。数据库的设计者希望所有的行为在一次提交中完成,以保证原子性等。
但由于事务的过程需要人类的输入,如果每个事务都等待用户输入的话,数据库需要支持大量的并发事务,并且大部分是空闲的。因此,大部分数据库并不采用这种方式,而采用OLTP的方式,缩短事务的保持时间,一个HTTP请求就等于一个事务。
采用这种交互式风格的事务,在应用程序和数据库之间,请求和查询结果会反复进行网络传输。并且如果我们不允许事务并发处理,数据库需要等待应用程序提交下一次查询,造成吞吐量的大幅下降。
为了解决这个问题,数据库提出了存储过程(stored precedure)的概念,应用程序将所有的数据库操作封装在存储过程中,并且编译后存储在数据库中,应用程序通过传递参数使用该存储过程,完成事务的处理,这里并不需要任何和网络和磁盘I/O。交互式事务和存储过程的区别如下图所示:
存储过程的优缺点
存储过程在一些数据库中实现,并且已经成为SQL-1999标准的一部分,但它是有一些缺点的:
- 每个数据库厂商有自己的存储过程语言,并不是使用的通用编程语言;
- 运行在数据库的代码难以管理,更难debug、进行版本控制和发布,以及测试等;
- 数据库的性能难以控制,一个数据库有多个应用程序使用,一个书写不好的存储过程会影响数据库的整体性能。
不过存储过程也在改进中,比如对通用编程语言的支持:VoltDB支持Java或者Groovy,Datomic支持Java或者Clojure,Redis支持Lua。
分区
为了能够提高串行化的CPU利用率,从而提高事务的处理效率,可以将数据进行分区。可以为每个分区分配一个CPU核,如果这个分区的读写操作不影响其他分区的话,是可以在单独执行该事务的。
如果是跨分区的事务,仍然需要是全局串行的。因此对于单纯的key-value型的数据库来说,分区是比较简单的,但如果是包括多个二级索引的话,就需要很多跨分区的协调了。
小结
串行化执行事务有以下的一些特点:
- 每个事务比如小且快,一个慢的事务会阻塞其他所有的事务;
- 数据最好在内存中,不常用的数据写在磁盘中,如果用到磁盘的数据,会影响事务的处理速度;
- 写吞吐量必须能可以在单核处理,或者通过分区方式提升吞吐量;
- 跨分区的事务是可行的,但会有串行的限制。