当前位置: 首页 > news >正文

MiniOB 并发B+树实现解析

MiniOB 是 OceanBase 联合华中科技大学推出的一款用于教学的小型数据库系统,希望能够帮助数据库爱好者系统性的学习数据库原理与实战。

B+ 树介绍

B+ 树是传统数据库中常见的索引数据结构,比如MySQL、PostgreSQL都实现了B+树索引。B+ 树是一个平衡多叉树,层级少(通常只有3层)、数据块(内部节点/叶子节点)大小固定,是一个非常优秀的磁盘数据结构。关于B+ 树的原理和实现,网上有非常多的介绍,就不在此聒噪。这里将介绍如何实现支持并发操作的B+树以及MiniOB中的实现。

B+树的并发操作

在多线程并发操作时,通常使用的手段是加锁,这里的实现方法也是这样。不过在学习并发B+树实现原理之前,需要对B+树的实现比较熟悉,有兴趣的同学可以网上搜索一下。

Crabing Protocol

在操作B+树时加对应的读写锁是一种最简单粗暴但是有效的方法,只是这样实现效率不高。于是就有一些研究创建了更高效的并发协议,并且会在协议设计上防止死锁的发生。
B+树示例

B+树是一个树状的结构,并且所有的数据都是在叶子节点上,每次操作,几乎都是从根节点开始向下遍历,直到找到对应的叶子节点。然后在叶子节点执行相关操作,如果对上层节点会产生影响,必须需要重新平衡,那就反方向回溯调整节点。
Crabing协议是从根节点开始加锁,找到对应的子节点,就加上子节点的锁。一直循环到叶子节点。在拿到某个子节点锁时,如果当前节点是“安全的”,那就可以释放上级节点的锁。

什么是“安全的”
如果在操作某个节点时,可以确定这个节点上的动作,不会影响到它的父节点,那就说是“安全的”。
B+树上节点的操作有三个:插入、删除和查询。

  • 插入:一次仅插入一个数据。如果插入一个数据后,这个节点不需要分裂,就是当前节点元素个数再增加一个,也不会达到一个节点允许容纳的最大个数,那就是安全的。不会分裂就不会影响到父节点。
  • 删除:一次仅删除一个数据。如果删除一个数据后,这个节点不需要与其它节点合并,就是当前节点元素个数删除一个后,也不会达到节点允许容纳的最小值,那就是安全的。不需要合并就不会影响到父节点。
  • 查询:读取数据对节点来说永远是安全的。

B+树的操作除了上述的插入、删除和查询,还有一个扫描操作。比如遍历所有的数据,通常是从根节点,找到最左边的叶子节点,然后从向右依次访问各个叶子节点。此时与加锁的顺序,与之前描述的几种方式是不同的,那为了防止死锁,就需要对遍历做特殊处理。一种简单的方法是,在访问某个叶子节点时,尝试对叶子节点加锁,如果判断需要等待,那就退出本次遍历扫描操作,重新来一遍。当然这种方法很低效,有兴趣的同学可以参考[2],了解更高效的扫描加锁方案。

问题:哪种场景下,扫描加锁可能会与更新操作的加锁引起死锁?
问题:请参考[2],给出一个遍历时不需要重试的加锁方案。

MiniOB实现

MiniOB的B+树并发实现方案与上个章节描述的方法是一致的。这里介绍一些实现细节。

在这里假设同学们对B+树的实现已经有了一定的了解。

B+树与Buffer Pool

B+树的数据是放在磁盘上的,但是直接读写磁盘是很慢的一个操作,因此这里增加一个内存缓冲层,叫做Buffer Pool。了解数据库实现的同学对这个名词不会陌生。在MiniOB中,Buffer Pool的实现是 class DiskBufferPool。对Buffer Pool实现不太了解也没关系,这里接单介绍一下。

DiskBufferPool 将一个磁盘文件按照页来划分(假设一页是8K,但是不一定),每次从磁盘中读取文件或者将数据写入到文件,都是以页为单位的。在将文件某个页面加载到内存中时,需要申请一块内存。内存通常会比磁盘要小很多,就需要引入内存管理。在这里引入Frame(页帧)的概念(参考 class Frame),每个Frame关联一个页面。FrameManager负责分配、释放Frame,并且在没有足够Frame的情况下,淘汰掉一些Frame,然后将这些Frame关联到新的磁盘页面。

那如何知道某个Frame关联的页面是否可以释放,然后可以与其它页面关联?
如果这个Frame没有任何人使用,就可以重新关联到其它页面。这里使用的方法是引用计数,称为 pin_count。每次获取某个Frame时,pin_count就加1,操作完释放时,pin_count减1。如果pin_count是0,就可以将页面数据刷新到磁盘(如果需要的话),然后将Frame与磁盘文件的其它数据块关联起来。

为了支持并发操作,Frame引入了读写锁。操作B+树时,就需要加对应的读写锁。

B+ 树的数据保存在磁盘,其树节点,包括内部节点和叶子节点,都对应一个页面。当对某个节点操作时,需要申请相应的Frame,pin_count加1,然后加读锁/写锁。由于访问子节点时,父节点的锁可能可以释放,也可能不能释放,那么需要记录下某个某个操作在整个过程中,加了哪些锁,对哪些frame 做了pin操作,以便在合适的时机,能够释放掉所有相关的资源,防止资源泄露。这里引入class LatchMemo 记录当前访问过的页面,加过的锁。

问题:为什么一定要先执行解锁,再执行unpin(frame引用计数减1)?

处理流程

B+树相关的操作一共有4个:插入、删除、查找和遍历/扫描。这里对每个操作的流程都做一个汇总说明,希望能帮助大家了解大致的流程。

插入操作
除了查询和扫描操作需要加读锁,其它操作都是写锁。

- leaf_node = find_leaf // 查找叶子节点是所有操作的基本动作memo.init // memo <=> LatchMemo,记录加过的锁、访问过的页面lock root page- node = crabing_protocal_fetch_page(root_page)loop: while node is not leaf // 循环查找,直到找到叶子节点child_page = get_child(node)- node = crabing_protocal_fetch_page(child_page)frame = get_page(child_page, memo)lock_write(memo, frame)node = get_node(frame)// 如果当前节点是安全的,就释放掉所有父节点和祖先节点的锁、pin_countrelease_parent(memo) if is_safe(node)- insert_entry_into_leaf(leaf_node)- split if node.size == node.max_size- loop: insert_entry_into_parent // 如果执行过分裂,那么父节点也会受到影响- memo.release_all // LatchMemo 帮我们做资源释放

删除操作
与插入一样,需要对操作的节点加写锁。

- leaf_node = find_leaf // 查找的逻辑与插入中的相同
- leaf_node.remove_entry
- node = leaf_node
- loop: coalesce_or_redistribute(node) if node.size < node.min_size and node is not rootneighbor_node = get_neighbor(node)// 两个节点间的数据重新分配一下redistribute(node, neighbor_node) if node.size + neighbor_node.size > node.max_size// 合并两个节点coalesce(node, neighbor_node) if node.size + neighbor_node.size <= node.max_sizememo.release_all

查找操作
查找是只读的,所以只加读锁

- leaf_node = find_leaf // 与插入的查找叶子节点逻辑相同。不过对所有节点的操作都是安全的
- return leaf_node.find(entry)
- memo.release_all

扫描/遍历操作

- leaf_node = find_left_nodeloop: node != nullptrscan nodenode_right = node->right // 遍历直接从最左边的叶子节点,一直遍历到最右边return LOCK_WAIT if node_right.try_read_lock // 不直接加锁,而是尝试加锁,一旦失败就返回node = node_rightmemo.release_last // 释放当前节点之前加到的锁

根节点处理

前面描述的几个操作,没有特殊考虑根节点。根节点与其它节点相比有一些特殊的地方:

  • B+树有一个单独的数据记录根节点的页面ID,如果根节点发生变更,这个数据也要随着变更。这个数据不是被Frame的锁保护的;
  • 根节点具有一定的特殊性,它是否“安全”,就是根节点是否需要变更,与普通节点的判断有些不同。

按照上面的描述,我们在更新(插入/删除)执行时,除了对节点加锁,还需要对记录根节点的数据加锁,并且使用独特的判断是否“安全的”方法。

在MiniOB中,可以参考LatchMemo,是直接使用xlatch/slatch对Mutex来记录加过的锁,这里可以直接把根节点数据保护锁,告诉LatchMemo,让它来负责相关处理工作。
判断根节点是否安全,可以参考IndexNodeHandler::is_safeis_root_node相关的判断。

如何测试

想要保证并发实现没有问题是在太困难了,虽然有一些工具来证明自己的逻辑模型没有问题,但是这些工具使用起来也很困难。这里使用了一个比较简单的方法,基于google benchmark框架,编写了一个多线程请求客户端。如果多个客户端在一段时间内,一直能够比较平稳的发起请求与收到应答,就认为B+树的并发没有问题。测试代码在bplus_tree_concurrency_test.cpp文件中,这里包含了多线程插入、删除、查询、扫描以及混合场景测试。

其它

有条件的开启并发

MiniOB是一个用来学习的小型数据库,为了简化上手难度,只有使用-DCONCURRENCY=ON时,并发才能生效,可以参考 mutex.h中class Mutexclass SharedMutex的实现。当CONCURRENCY=OFF时,所有的加锁和解锁函数相当于什么都没做。

并发中的调试

死锁是让人非常头疼的事情,我们给Frame增加了调试日志,并且配合pin_count的动作,每次加锁、解锁以及pin/unpin都会打印相关日志,并在出现非预期的情况下,直接ABORT,以尽早的发现问题。这个调试能力需要在编译时使用条件 -DDEBUG=ON 才会生效。
以写锁为例:

void Frame::write_latch(intptr_t xid)
{{std::scoped_lock debug_lock(debug_lock_);  // 如果非DEBUG模式编译,什么都不会做ASSERT(pin_count_.load() > 0,   // 加锁时,pin_count必须大于0,可以想想为什么?"frame lock. write lock failed while pin count is invalid. ""this=%p, pin=%d, pageNum=%d, fd=%d, xid=%lx, lbt=%s", // 这里会打印各种相关的数据,帮助调试this, pin_count_.load(), page_.page_num, file_desc_, xid, lbt()); // lbt会打印出调用栈信息ASSERT(write_locker_ != xid, "frame lock write twice." ...);ASSERT(read_lockers_.find(xid) == read_lockers_.end(),"frame lock write while holding the read lock." ...);}lock_.lock();write_locker_ = xid;LOG_DEBUG("frame write lock success." ...); // 加锁成功也打印一个日志。注意日志级别是DEBUG
}

欢迎提交issue和pull request

MiniOB 当前还是一个非常不完善的项目,包括覆盖的知识面、代码质量、注释、测试、开发环境等,都需要完善,欢迎开源爱好者一起共同建设。

参考

[1] 15445 indexconcurrency
[2] Concurrency of Operations on B-Trees
[3] MySQL/MariaDB mini trans相关代码

相关文章:

MiniOB 并发B+树实现解析

MiniOB 是 OceanBase 联合华中科技大学推出的一款用于教学的小型数据库系统&#xff0c;希望能够帮助数据库爱好者系统性的学习数据库原理与实战。 B 树介绍 B 树是传统数据库中常见的索引数据结构&#xff0c;比如MySQL、PostgreSQL都实现了B树索引。B 树是一个平衡多叉树&am…...

SpringCloud负载均衡服务调用——Ribbon

Ribbon 本专栏学习内容来自尚硅谷周阳老师的视频 有兴趣的小伙伴可以点击视频地址观看 简介 Spring Cloud Ribbon是基于Netflix Ribbon实现的一套客户端负载均衡的工具。 简单的说&#xff0c;Ribbon是Netflix发布的开源项目&#xff0c;主要功能是提供客户端的软件负载均衡算…...

各种邮箱服务软件对比

1.宝塔邮局管理器 特点:简单易用,可视化操作,小白也能搞,还有备份功能,一般足够用了 缺点:稳定性真是差,隔三差五的不能收发.没有接口,不能任意修改邮箱密码,只能管理员修改 注意要点:一定要开启ssl,否则有些邮箱给你发邮件你收不到 建议:不要入坑.坏了之后没法修复,哭都没地方…...

相机单独标定的实现过程[autoware标定]、tmp文件的查看方式

安装了autoware1.13和calibration标定包&#xff0c;发现实现相机单独标定的过程较为坎坷&#xff0c;参考了一些博主的方法&#xff0c;发现下面的过程更加适合自己&#xff0c;做个笔记。 1安装标定箱&#xff08;与calibration标定包的安装并不冲突&#xff09; 标定工具箱…...

4.10.1、IP 多播技术的相关基本概念

多播&#xff08;Multicast&#xff0c;也称为组播&#xff09;是一种实现 “一对多” 通信的技术&#xff0c;与传统单播“一对一”通信相比&#xff0c;多播可以极大地节省网络资源。 在因特网上进行的多播&#xff0c;称为 IP 多播。 1、单播 & 多播 如下所示&#xf…...

PIGOSS BSM监控国产数据库Oscar

前言神通数据库(原OSCAR数据库&#xff09;是天津神舟通用数据技术有限公司&#xff08;简称“神舟通用公司”&#xff09;拥有自主知识产权的企业级、大型通用关系型数据库管理系统。PIGOSS BSM作为网利友联科技完全自主研发的纯国产基础 IT 架构运行状态监测平台软件&#xf…...

Spring Boot中文件上传

Spring Boot中文件上传 前言 本篇主要参考Spring官方文档&#xff0c;整理了Spring Boot中文件上传如何实现&#xff0c;以及在代码中使用RestTemplate和HttpClient两种方式实现文件上传。 创建Spring Boot项目 首先创建一个Spring Boot Web项目&#xff0c;使用的Spring B…...

Github上传大文件(>25MB)教程

Github上传大文件&#xff08;>25MB&#xff09;教程Github上传大文件&#xff08;>25MB&#xff09;教程安装git安装Git Large File Storage实例踩坑点1&#xff1a;failed to push some refs to踩坑点2&#xff1a;main与master踩坑点3&#xff1a;Failed to connect t…...

面试官:mysql索引会缓存内存吗?

文章目录 InnoDB缓冲池如何设置方法一:使用 `innodb_buffer_pool_size` 变量方法二:修改my.ini配置文件InnoDB缓冲池 InnoDB存储引擎是基于磁盘存储表文件和索引的,并将数据按页的方式管理,由于访问磁盘的速度较慢,多次访问磁盘会造成数据库性能的下降,为此,InnoDB在内…...

bs4解析数据和csv文件

\b 检测所在的位置是否是单词边界&#xff08;任何可以将不同的单词进行区分的符号&#xff1a;空白符号&#xff0c;标点符号&#xff0c;字符串开头&#xff0c;字符串结尾&#xff09; ^ 检测是否是字符串开头 $ 检测是否是字符串结尾 csv保存数据 什么是csv文件 读操作…...

Linux中Buffer和Cache的区别

Linux中Buffer和Cache的区别 free命令中会有一项buff/cache, 通过man free可以看到这里的关于buff/cache的介绍 buff/cache包含两部分 buffers:内核缓存区用到的内存&#xff0c;对应/proc/meminfo中Buffers的值 cache:内核页缓存和Slab用到的内存&#xff0c;对应/proc/mem…...

Docker 镜像使用

目录 1、列出镜像列表 2、获取一个新的镜像 3、查找镜像 4、拖取镜像 5、删除镜像 6、创建镜像 a.更新镜像 b.构建镜像 设置镜像标签 当运行容器时&#xff0c;使用的镜像如果在本地中不存在&#xff0c;docker 就会自动从 docker 镜像仓库中下载&#xff0c;默认是从 …...

Java阶段一Day10

Java阶段一Day10 文章目录Java阶段一Day10抽象类和抽象方法接口案例小练习引用类型数组教师总结回顾&#xff1a;精华笔记&#xff1a;笔记&#xff1a;补充&#xff1a;抽象类和抽象方法 关键字&#xff1a;abstract 只有方法的定义&#xff0c;没有具体的实现&#xff08;连…...

触摸屏与PLC之间如何快速实现无线PPI通信?

PPI协议是西门子为S7-200专门开发的通信协议&#xff0c;是不开放的协议&#xff0c;CPU自带的两个通信口&#xff08;Port0&#xff0c;Port1&#xff09;均支持该协议&#xff0c;S7-200的一些通信模块也支持PPI协议。编程软件Micro/WIN与CPU进行编程通信也使用PPI协议&#…...

【华为OD机试 2023最新 】 羊、狼、农夫过河(C++ 100%)

题目描述 羊、狼、农夫都在岸边,当羊的数量小于狼的数量时,狼会攻击羊,农夫则会损失羊。农夫有一艘容量固定的船,能够承载固定数量的动物。 要求求出不损失羊情况下将全部羊和狼运到对岸需要的最小次数。 只计算农夫去对岸的次数,回程时农夫不会运送羊和狼。 备注:农…...

Java中关于try、catch、finally中的细节分析

本文讲解的是关于Java中关于try、catch、finally中一些问题 下面看一个例子&#xff08;例1&#xff09;&#xff0c;来讲解java里面中try、catch、finally的处理流程 public class TryCatchFinally {SuppressWarnings("finally")public static final String test(…...

Zookeeper原理

一、概念 Zookeeper是一个开源的、分布式的&#xff0c;为分布式应用提供协调服务的Apache项目。封装好复杂易出错的关键服务&#xff0c;将简单易用的接口和性能高效、功能稳定的系统提供给用户。 二、选举机制 首先是几个概念&#xff1a; myid&#xff1a;节点的唯一标识&…...

关于FPGA如何快速生成模块的例化模板(实用)

关于FPGA如何快速生成模块的例化模板&#xff08;实用&#xff09; 语言 &#xff1a;Verilg HDL 、VHDL EDA工具&#xff1a;ISE、Vivado、Quartus II 关于FPGA如何快速生成模块的例化模板&#xff08;实用&#xff09;一、引言二、快速生成例化模块的几种方法1. IP核的例化模…...

在 Python 中将字符串转换为集合

使用 set() 类将字符串转换为集合&#xff0c;例如 my_set set(my_str)。 set() 类将通过拆分其字符将字符串转换为集合。 my_str one# ✅ 通过拆分字符将字符串转换为集合 my_set set(my_str) print(my_set) # &#x1f449;️ {n, o, e}# -----------------------------…...

大数据Flink进阶(十三):Flink 任务提交模式

文章目录 Flink 任务提交模式 一、会话模式&#xff08;Session Mode&#xff09; 二、单作业模式&#xff08;Per-Job Mode&#xff09; 三、应用模式&#xff08;Application Mode&#xff09; Flink 任务提交模式 Flink分布式计算框架可以基于多种模式部署&#xff0c;…...

day11—编程题

文章目录1.第一题1.1题目1.2涉及的相关知识1.3思路1.4解题2.第二题2.1题目2.2思路2.3解题1.第一题 1.1题目 描述&#xff1a; 将一棵无穷大满二叉树的结点按根结点一层一层地从左往右编号&#xff0c;根结点编号为1。现给定a&#xff0c;b为两个结点。设计一个算法&#xff0…...

CentOS下安装crontab及cron表达式解析

目录安装依赖服务启停任务操作参数简要说明1、参数说明2、cron表达式解析(1)定义(2)结构(3)字段含义(4)注意事项(5)常用表达式例子crontab示例结尾安装依赖 # vixie-cron软件包是crontab的主程序 # crontabs软件包是用来安装、卸装、或列举用来驱动crontab守护进程的表格的程序…...

python 绘制训练曲线--基于Numpy.convolve曲线平均滤波

文章目录1 训练曲线--震荡的非常厉害2 基于Numpy.convolve曲线平均滤波3 python 绘制训练曲线 平滑处理--Savitzky-Golay 滤波器曲线平滑4 python 绘制训练曲线--插值法 曲线平滑处理1 训练曲线–震荡的非常厉害 上一篇文章用python自己绘制训练曲线震荡的非常厉害&#xff08…...

状态管理插件vuex

概念: 专门在Vue中实现集中式状态(数据&#xff09;管理的一个Vue插件&#xff0c;对vue应用中多个组件的共享状态进行集中式的管理(读/写)&#xff0c;也是一种组件间通信的方式&#xff0c;且适用于任意组件间通信。 作用&#xff1a; 如果我们使用全局总线要让所有的组件…...

arthas—阿里开源的Java诊断工具

一、arthas简述Arthas 是阿里开源的Java诊断工具。安装在系统所在服务器&#xff0c;有着强大的能力&#xff0c;是一个开发运维神器。主要功能在线热替换代码/代码增强全局视角的性能分析查看方法执行情况&#xff0c;帮助跟踪偶现的bug支持JDK6二、官方资料官方文档的介绍非常…...

Java学习记录

阅读前请看一下&#xff1a;我是一个热衷于记录的人&#xff0c;每次写博客会反复研读&#xff0c;尽量不断提升博客质量。文章设置为仅粉丝可见&#xff0c;是因为写博客确实花了不少精力。希望互相进步谢谢&#xff01;&#xff01; 文章目录阅读前请看一下&#xff1a;我是一…...

OpenGL API 之 glVertexAttribPointer

glVertexAttribPointer 定义通用顶点属性数据的数组 C Specification format void glVertexAttribPointer(GLuint index,GLint size,GLenum type,GLboolean normalized,GLsizei stride,const void * pointer); Parameters nametypedescriptionindexGLuint Specifies the inde…...

蓝桥杯真题4

[蓝桥杯 2017 省 AB] 分巧克力 题目描述 儿童节那天有 KKK 位小朋友到小明家做客。小明拿出了珍藏的巧克力招待小朋友们。 小明一共有 NNN 块巧克力&#xff0c;其中第 iii 块是 HiWiH_i \times W_iHi​Wi​ 的方格组成的长方形。 为了公平起见&#xff0c;小明需要从这 NN…...

day02_基本语法

今日内容 上课同步视频:CuteN饕餮的个人空间_哔哩哔哩_bilibili 同步笔记Java2307_沐沐霸的博客-CSDN博客 零、复习昨日 一、程序&Java介绍 二、安装JDK&配置环境变量 三、DOS命令 四、第一个程序[重点] 五、Java语言规范[重点] 六、运行机制 七、Typora工具使用 附录:…...

多线程之单例模式

前言 本篇介绍的是wait与notify方法&#xff0c;通过wait来顺序控制执行一些代码&#xff0c;了解单例模式&#xff0c;进行单例模式的简单实现&#xff0c;介绍饿汉模式下出现线程不安全的问题与解决&#xff1b;如有错误&#xff0c;请在评论区指正&#xff0c;让我们一起交…...