本文提到的完整源码地址:https://github.com/rosedblabs/rust-practice
数据库的事务是一个经久不衰的话题,相信大家都已经耳熟能详了,事务是指数据库中单一逻辑工作单元的操作集合。这些操作要么全部成功执行,要么全部失败,从而确保数据库的一致性和完整性。
事务有 ACID 四个特性,分别是:
其中原子性一般是通过预写日志来保证,持久性是通过预写日志和存储管理完成,隔离性常用的方式有多版本并发控制(MVCC)、两阶段锁等等。
隔离性是这其中稍微复杂的,其他的几个特性其实都不难理解,这篇文章将会使用 Rust 代码,实现一个最基础的 MVCC 事务,让一些重要概念不仅仅存在于理论层面,而是让大家上手实践,这样才能够加深理解并完全掌握。
我们知道数据库的隔离性其实又分为了四种,分别是
总体来说 MVCC 是实现事务隔离性的手段,通过 MVCC 可以很方便的实现可重复读。 MVCC 是建立在数据多版本的基础之上的,当写入一条数据的时候,会带上一个版本号,这个版本号一般是事务的唯一标识,修改数据的时候,不会直接原地去修改数据,而是新增一条新的数据,并且带上一个新的版本号。 这样一条数据实际上就会在物理存储上存在多个版本,当读取数据的时候,会找到第一个满足条件的数据并返回。 这样的好处是:读不会阻塞写,写也不会阻塞读, 最大限度提升了数据库的并发性能。
如下所示,横轴表示数据库中的 key,竖轴表示事务开始的时间顺序。
Time
5
4 a4
3 b3 x
2
1 a1 c1 d1
a b c d Keys
事务 T2 启动的时候,它看到的值是 a=a1,c=c1,d=d1。 随后又有几个事务修改了 a(将 a1 改为 a4),新增了 b=b3,并且删除了 d。 这时候事务 T5 开始,它看到的值是 a=a4,b=b3,c=c1。 可以看到对数据的修改并不是原地的,而是新增加一个版本的数据,删除数据的时候,其实也不是真正的将其删除掉,而是通过标记的方式。
接下来通过具体的代码来进行更进一步的了解。
需要说明的是,出于演示的便利性,代码全部使用了基于内存的数据结构,所以严格意义上来说并不满足事务的原子性、持久性,但这并不妨碍我们去理解 MVCC 的精髓。
事务的定义使用一个单独的结构体来表示,分别包含了存储数据的底层 KV,事务的版本号,以及活跃事务列表。 active_xid 是一个事务私有的数据结构,保存了在这个事务启动时,其他活跃的(未提交)事务集合,这是实现可重复读的关键,保证就算其他事务提交了,也不会影响到当前的事务。
// MVCC 事务
pub struct Transaction {
// 底层 KV 存储引擎
kv: Arc<Mutex<KVEngine>>,
// 事务版本号
version: u64,
// 事务启动时的活跃事务列表
active_xid: HashSet<u64>,
}
再来看一下事务的具体操作。
首先是开启事务,这里是申请一个全局唯一的事务版本号,并且保存当前活跃事务列表。
// 开启事务
pub fn begin(kv: Arc<Mutex<KVEngine>>) -> Self {
// 获取全局事务版本号
let version = acquire_next_version();
let mut active_txn = ACTIVE_TXN.lock().unwrap();
// 这个 map 的 key 就是当前所有活跃的事务
let active_xid = active_txn.keys().cloned().collect();
// 添加到当前活跃事务 id 列表中
active_txn.insert(version, vec![]);
// 返回结果
Self {
kv,
version,
active_xid,
}
}
然后是写入数据,需要判断当前写入的 key 是否和其他的事务发生了冲突,如果是的话,则需要返回错误,通知调用者进行重试。 这里判断冲突主要的逻辑是扫描最后一个 key 及其对应的版本,并判断其可见性:
如果没有冲突,则写入数据,也分为了两个步骤,一是记录了当前 version 写入了哪些 key(主要保证回滚时可以将数据删除掉),二是实际向存储引擎写入对应的数据。
fn write(&self, key: &[u8], value: Option<Vec<u8>>) {
// 判断当前写入的 key 是否和其他的事务冲突
// key 是按照 key-version 排序的,所以只需要判断最近的一个 key 即可
let mut kvengine = self.kv.lock().unwrap();
for (enc_key, _) in kvengine.iter().rev() {
let key_version = decode_key(enc_key);
if key_version.raw_key.eq(key) {
if !self.is_visible(key_version.version) {
panic!("serialization error, try again.");
}
break;
}
}
// 写入 TxnWrite
let mut active_txn = ACTIVE_TXN.lock().unwrap();
active_txn
.entry(self.version)
.and_modify(|keys| keys.push(key.to_vec()))
.or_insert_with(|| vec![key.to_vec()]);
// 写入数据
let enc_key = Key {
raw_key: key.to_vec(),
version: self.version,
};
kvengine.insert(enc_key.encode(), value);
}
可以看到写入数据的时候,需要将原始的 key 加上当前版本号。
提交事务的逻辑比较简单,因为所有需要写入的数据实际上都已经写入完成了,这时候只需要将这个事务的版本号从全局的活跃事务列表中删除就可以了。
// 提交事务
pub fn commit(&self) {
// 清除活跃事务列表中的数据
let mut active_txn = ACTIVE_TXN.lock().unwrap();
active_txn.remove(&self.version);
}
这样后续新开启的事务可以看到这个事务的修改,而其他未提交的事务,仍然看不到这个事务的修改。
回滚事务的逻辑和提交事务有些类似,唯一的区别是,需要将写入的数据重新删除掉。 这样做的目的是确保事务的一致性,因为事务回滚之后,原来已经写入的数据不能对后续的事务可见,所以需要全部删除掉。
// 回滚事务
pub fn rollback(&self) {
// 清除写入的数据
let mut active_txn = ACTIVE_TXN.lock().unwrap();
if let Some(keys) = active_txn.get(&self.version) {
let mut kvengine = self.kv.lock().unwrap();
for k in keys {
let enc_key = Key {
raw_key: k.to_vec(),
version: self.version,
};
let res = kvengine.remove(&enc_key.encode());
assert!(res.is_some());
}
}
// 清除活跃事务列表中的数据
active_txn.remove(&self.version);
}
读取数据的逻辑比较简单,因为数据 key 是按照 version 排序的,所以只需要从后往前遍历,找到第一个可见的记录,就是当前事务能够得到的对应 key 的结果。 需要说明的是,这里的处理逻辑稍微有点冗余,因为直接遍历了整个 map 去查找对应的 key,但实际上因为我们写入的 key 都是有序的,所以只需要前缀遍历即可。
// 读取数据,从最后一条数据进行遍历,找到第一条可见的数据
pub fn get(&self, key: &[u8]) -> Option<Vec<u8>> {
let kvengine = self.kv.lock().unwrap();
for (k, v) in kvengine.iter().rev() {
let key_version = decode_key(k);
if key_version.raw_key.eq(key) && self.is_visible(key_version.version) {
return v.clone();
}
}
None
}
MVCC 的核心是“多版本”,即同一个 key 在写入、更新、查询的时候,都会和当前版本号相关,通过事务的版本号来判断数据的可见性。 并且存储的时候,同一个 key 在物理上会有多份,分别对应不同的版本,本质上是一种通过空间冗余的方式来提升并发性能,让每一个事务都“看起来像”是单独执行,不受其他并发事务的影响,做到了读写互不阻塞。
通过上面的理论知识讲解和代码实战,相信你能够对 MVCC 理论有了一个更加深入的理解了。
完整源码地址:
https://github.com/rosedblabs/rust-practice
本文分享自 roseduan写字的地方 微信公众号,前往查看
如有侵权,请联系 cloudcommunity@tencent.com 删除。
本文参与 腾讯云自媒体同步曝光计划 ,欢迎热爱写作的你一起参与!