导语:计算机硬件在飞速发展,数据规模在急速膨胀,但是数据库仍然使用是十年以前的架构体系,WiredTiger 尝试打破这一切,充分利用多核与大内存时代,开发一种真正满足未来大数据管理所需的数据库。本文由袁荣喜向「高可用架构」投稿,介绍对 WiredTiger 源代码学习过程中对数据库设计的感悟。
袁荣喜,学霸君工程师,2015年加入学霸君,负责学霸君的网络实时传输和分布式系统的架构设计和实现,专注于基础技术领域,在网络传输、数据库内核、分布式系统和并发编程方面有一定了解。
WiredTiger 从被 MongoDB 收购到成为 MongoDB 的默认存储引擎的一年半,得到了迅猛的发展,也逐步被外部熟知。
现代计算机近 20 年来 CPU 的计算能力和内存容量飞速发展,但磁盘的访问速度并没有得到相应的提高,WT 就是在这样的一个情况下研发出来,它设计了充分利用 CPU 并行计算的内存模型的无锁并行框架,使得 WT 引擎在多核 CPU 上的表现优于其他存储引擎。
针对磁盘存储特性,WT 实现了一套基于 BLOCK/Extent 的友好的磁盘访问算法,使得 WT 在数据压缩和磁盘 I/O 访问上优势明显。实现了基于 snapshot 技术的 ACID 事务,snapshot 技术大大简化了 WT 的事务模型,摒弃了传统的事务锁隔离又同时能保证事务的 ACID。WT 根据现代内存容量特性实现了一种基于 Hazard Pointer 的 LRU cache 模型,充分利用了内存容量的同时又能拥有很高的事务读写并发。
在本文中,我们主要针对 WT 引擎的事务来展开分析,来看看它的事务是如何实现的。说到数据库事务,必然先要对事务这个概念和 ACID 简单的介绍。
基本概念:事务与 ACID
什么是事务?
事务就是通过一系列操作来完成一件事情,在进行这些操作的过程中,要么这些操作完全执行,要么这些操作全不执行,不存在中间状态,事务分为事务执行阶段和事务提交阶段。一般说到事务,就会想到它的特性— ACID,那么什么是 ACID 呢?我们先用一个现实中的例子来说明:AB 两同学账号都有 1,000 块钱,A 通过银行转账向 B 转了 100,这个事务分为两个操作,即从 A 同学账号扣除 100,向 B 同学账号增加 100。
原子性(Atomicity)
组成事务的系列操作是一个整体,要么全执行,要么不执行。通过上面例子就是从 A 同学扣除钱和向 B 同学增加 100 是一起发生的,不可能出现扣除了 A 的钱,但没增加 B 的钱的情况。
一致性(Consistency):
在事务开始之前和事务结束以后,数据库的完整性和状态没有被破坏。这个怎么理解呢?就是 A、B 两人在转账钱的总和是 2,000,转账后两人的总和也必须是 2,000。不会因为这次转账事务破坏这个状态。
隔离性(Isolation):
多个事务在并发执行时,事务执行的中间状态是其他事务不可访问的。A 转出 100 但事务没有确认提交,这时候银行人员对其账号查询时,看到的应该还是 1,000,不是 900。
持久性(Durability)
事务一旦提交生效,其结果将永久保存,不受任何故障影响。A 转账一但完成,那么 A 就是 900,B 就是 1,100,这个结果将永远保存在银行的数据库中,直到他们下次交易事务的发生。
WT 如何实现事务
知道了基本的事务概念和 ACID 后,来看看 WT 引擎是怎么来实现事务和 ACID。要了解实现先要知道它的事务的构造和使用相关的技术,WT 在实现事务的时使用主要是使用了三个技术:
snapshot(事务快照)
MVCC(多版本并发控制)
redo log(重做日志)
为了实现这三个技术,它还定义了一个基于这三个技术的事务对象和全局事务管理器。事务对象描述如下
wt_transaction{
transaction_id: 本次事务的全局唯一的ID,用于标示事务修改数据的版本号
snapshot_object: 当前事务开始或者操作时刻其他正在执行且并未提交的事务集合,用于事务隔离
operation_array: 本次事务中已执行的操作列表,用于事务回滚。
redo_log_buf: 操作日志缓冲区。用于事务提交后的持久化
State: 事务当前状态
}
WT 的多版本并发控制
WT 中的 MVCC 是基于 key/value 中 value 值的链表,这个链表单元中存储有当先版本操作的事务 ID 和操作修改后的值。描述如下:
wt_mvcc{
transaction_id: 本次修改事务的ID
value: 本次修改后的值
}
WT 中的数据修改都是在这个链表中进行 append 操作,每次对值做修改都是 append 到链表头上,每次读取值的时候读是从链表头根据值对应的修改事务 transaction_id 和本次读事务的 snapshot 来判断是否可读,如果不可读,向链表尾方向移动,直到找到读事务能都的数据版本。样例如下:
图1&#