持续创作,加速成长!这是我参与「掘金日新计划 · 10 月更文挑战」的第3天,点击查看活动详情
blotdb【etcd底层的kv存储】,本文改编自作者:青藤木鸟 www.qtmuniao.com/2020/11/29/…, 转载请注明出处
事务实现
事务定义:执行:计算层面;逻辑单位:不可分割;操作序列有限:粒度不大;
boltdb 只支持一写多读的事务,即同时至多有一个读写事务,而可以有多个只读事务,算是一种弱化的事务模型,好处在于容易实现,坏处在于牺牲了写并发的性能。也因此,boltdb 适合读多写少的应用。
boltdb 事务实现的主要代码在 tx.go 中,但这个源文件大抵算一个事务实现入口,事务提交时的一些行为,主要在数据库索引逻辑中实现。
1. 持久性
- 改动数据刷盘
-
在一个读写事务中,所有用户的直接改动(增加、删除、改动)都发生在叶子节点,但为了维持 B+ 树的性质,会在
Commit前进行调整,会引起中间节点的级联变动。所有这些节点(Node)在spill阶段通过node.write(p)转化为页(Page),所有变动的页(包括复用 freelist 中的和新申请的)称为脏页(dirty pages)。在spill为 page 后,boltdb 会通过func (tx *Tx) write() error将这些脏页进行刷盘,大体逻辑为:- 将脏页按 page id 排序后逐个遍历
- 将 page id 转化为 offset
- 通过
db.ops.writeAt将脏页在 offset 处刷盘 - 通过 page pool 复用 page size = 1 的脏页,以备 allocate 时复用
- 元信息刷盘
- 元信息包括freelist表和整个db的元信息页刷盘
2.一致性
表示写入的数据必须完全符合所有的预设约束、触发器、级联回滚等[3]。举个例子来说,A 给 B 转账,转账前后,A 和 B 的账户总额应该保持不变。
该性质描述侧重于应用层面,而非数据库本身。boltdb 是一个简单的 KV 引擎,不支持用户自定义约束。
3.原子性
在事务中,原子性其实更侧重于出现问题时的可回滚性(rollback),或者说可丢弃性(abortability),即事务中的操作不能部分执行,要么都成功执行,要么都未执行。
-
主动
- 主动调用tx.Rollback进行回滚,其主要逻辑包括回滚使用的 freelist,释放一些资源(如锁和节点内存引用)。只读事务结束时必须要调用回滚函数,以关闭事务,防止对读写事务的阻塞,之前文章分析过原因(主要是争抢 remap 时候的锁)。
-
被动
-
在读写事务进行到一半时,如果 boltdb 实例意外挂掉重启后,boltdb 如何保证事务的原子性?
- 读写事务执行过程中,所有的改动都是增量改动,不影响其他只读事务
- 最后提交时,元信息页落盘成功,才会使得所有增量改动对用户可见
-
也就是说,使用元信息页作为“全局指针”,以该指针的写入原子性来保证事务的原子性。如果宕机时,元信息页没有写入完成,所有改动便不会生效,达到了自动回滚的效果。
-
4. 隔离性
4个隔离级别:
-
未提交读:某个事务读到了另一个未提交事务修改过的记录
- 脏读:读到了一个不存在的数据,一般发生在回滚时
-
已提交读:一个事务只能读到另一个已经提交的事务修改过的数据,并且其他事务每对该数据进行一次修改并提交后,该事务都能查询得到最新值
-
可重复读:一个事务只能读到另一个已经提交的事务修改过的数据,但是第一次读过某条记录后,即使其他事务修改了该记录的值并且提交,该事务之后再读该条记录时,读到的仍是第一次读到的值,而不是每次都读到不同的数据。
-
串行化:3种隔离级别都允许对同一条记录进行
读-读、读-写、写-读的并发操作,如果我们不允许读-写、写-读的并发操作,可以使用SERIALIZABLE隔离级别boltdb 实现隔离性的方法是:
- 增量写内存。
- 穿透读磁盘。
读写事务的变动都在内存中,而只读事务通过 mmap 直接读取的磁盘上的内容,因此读写事务的改动不会为只读事务所见。多个读写事务是串行的,也不会互相影响。而每个只读事务期间所看到的状态,就是该只读事务开始执行时的状态。
参考文献
- boltdb repo:github.com/boltdb/bolt