从零实现一个数据库(DataBase) Go语言实现版 6.持久化到磁盘

news2024/10/6 16:19:20

英文源地址

持久化至磁盘

前一章中的b树数据结构可以很容易地转存到磁盘上.让我们在它之上建立一个简单地kv存储. 由于我们的b树实现是不可变的, 我们将以仅追加的方式分配磁盘空间, 重用磁盘空间将推迟到下一章.

持久化数据的方式

正如前面章节所提到的, 将数据持久化到磁盘不仅仅是将数据转存到文件中. 这里有几个要考虑的因素:

  1. 崩溃恢复: 这包括数据库进程崩溃, 操作系统崩溃和电源故障.重启后数据库必须处于可用状态.
  2. 持久性: 在数据库成功响应后, 所涉及的数据可以保证持久化, 即使在崩溃之后也是如此. 换句话说, 持久化在响应客户端之前发生.

有许多描述数据库的专业术语ACID(原子性, 一致性, 隔离性, 持久性), 但这些概念不是正交的, 很难解释, 所以让我们转而关注我们的实际示例.

  1. b树的不可变方面: 更新b树不会触及b树的前一个版本, 这使得崩溃恢复变得容易–如果更新出错, 我们可以简单地恢复到前一个版本.
  2. 持久性是通过fsync linux系统调用实现的.通过write或mmap的普通文件IO首先进入页缓存(page cache), 系统稍后必须将页缓存(page cache)刷新(flush)到磁盘上.fsync系统调用阻塞, 知道所有脏页都被刷新.

如果更新出错, 我们将如何恢复到以前的版本? 我们可以将更新分为两个阶段:

  1. 更新操作创建新的节点, 将它们写入磁盘.
  2. 每次更新都会创建一个新的根节点, 我们需要将指向根节点的指针存储在某个地方.

第一个阶段可能涉及将多个页面写入磁盘, 这通常不是原子操作.但是第二阶段只涉及单个指针, 并且可以在原子的单页写入操作中完成. 这使得整个操作原子化–如果数据库崩溃, 更新将不会发生.
第一个阶段必须在第二个阶段前持久化, 否则, 根节点指针可能会在崩溃后指向一个损坏的(部份持久化的)树版本.这两个阶段之间应该有一个fsync(作为一个屏障)
第二阶段也应该在响应客户端之前进行fsync.

基于mmap的IO

可以使用mmap系统调用从虚拟地址映射磁盘文件的内容. 从这个地址读取将启动透明磁盘IO, 这与通过read系统调用读取文件相同, 但不需要用户空间缓冲区(buffer)和系统调用的开销. 映射地址是页缓存(page cache)的代理, 通过它修改数据与write系统调用相同.
mmap很方便, 我们将在KV存储中使用它. 然而, 使用mmap并不是必需的.

func mmapInit(fp *os.File) (int, []byte, error) {
	fi, err := fp.Stat()
	if err != nil {
		return 0, nil, fmt.Errorf("stat: %w", err)
	}
	if fi.Size()%BTREE_PAGE_SIZE == 0 {
		return 0, nil, errors.New("File size is not a multiple of page size.")
	}
	mmapSize := 64 << 20
	if mmapSize%BTREE_PAGE_SIZE != 0 {
		panic("wrong mmapsize")
	}
	for mmapSize < int(fi.Size()) {
		mmapSize *= 2
	}
	chunk, err := syscall.Mmap(int(fp.Fd()), 0, mmapSize, syscall.PROT_READ|syscall.PROT_WRITE, syscall.MAP_SHARED)
	if err != nil {
		return 0, nil, fmt.Errorf("mmap: %w", err)
	}
	return int(fi.Size()), chunk, nil
}

上面的函数创建了至少与文件大小相等的初始映射. 映射的大小可以大于文件大小, 并且超过文件末尾的范围是不可访问的(SIGBUS), 但是可以稍后拓展文件.
随着文件的增长, 我们可能需要扩展映射的范围. 扩展mmap范围的系统调用是mremap.不幸的是, 当通过重新映射扩展范围时. 我们可能无法保留起始地址. 我们扩展映射的方法是使用多个映射–为溢出文件范围创建一个新的映射.

type KV struct {
	Path string
	fp *os.File
	tree BTree
	mmap struct{
		file int
		total int
		chunks [][]byte
	}
	page struct{
		flushed uint64
		temp [][]byte
	}
}

func extendMmap(db *KV, npages int) error {
	if db.mmap.total >= npages*BTREE_PAGE_SIZE {
		return nil
	}
	chunk, err := syscall.Mmap(
		int(db.fp.Fd()), int64(db.mmap.total), db.mmap.total,
		syscall.PROT_READ|syscall.PROT_WRITE, syscall.MAP_SHARED)
	if err != nil {
		return fmt.Errorf("mmap: %w", err)
	}
	db.mmap.total += db.mmap.total
	db.mmap.chunks = append(db.mmap.chunks, chunk)
	return nil
}

新映射的大小呈指数增长, 因此我们不必频繁调用mmap.
下面是我们如何从映射地址访问一个页.

func (db *KV) pageGet(ptr uint64) BNode {
	start := uint64(0)
	for _, chunk := range db.mmap.chunks {
		end := start + uint64(len(chunk))/BTREE_PAGE_SIZE
		if ptr < end {
			offset := BTREE_PAGE_SIZE * (ptr - start)
			return BNode{chunk[offset: offset+BTREE_PAGE_SIZE]}
		}
		start = end
	}
	panic("bad ptr")
}

主页 MasterPage

文件的第一页用于存储指向根节点的指针, 我们称之为’master page’.分配新节点所需的总页数, 因此页存储在那里.在这里插入图片描述
下面的函数在初始化数据库时读取master page.

const DB_SIG = "BuildYourOwnDB05"

func masterLoad(db *KV) error {
	if db.mmap.file == 0 {
		db.page.flushed = 1
		return nil
	}
	data := db.mmap.chunks[0]
	root := binary.LittleEndian.Uint64(data[16:])
	used := binary.LittleEndian.Uint64(data[24:])

	if !bytes.Equal([]byte(DB_SIG), data[:16]) {
		return errors.New("Bad signature")
	}
	bad := !(1 <= used && used <= uint64(db.mmap.file/BTREE_PAGE_SIZE))
	bad = bad || !(0 <= root && root < used)
	if bad {
		return errors.New("Bad master page.")
	}
	db.tree.root = root
	db.page.flushed = used

	return nil
}

下面是更新master page的功能. 与用于读取的代码不同, 它不使用映射地址进行写入.这是因为通过mmap修改页不是原子性的. 内核可能会在中途刷新页并损坏磁盘文件, 而不跨越页边界的小的write操作则保证是原子性的.

分配自盘页

我们将简单地将新的页添加到数据库的末尾, 直到在下一章中添加空闲列表.
新的页暂时保存在内存中, 直到稍后(在可能拓展文件之后)复制到文件中.

func (db *KV) pageNew(node BNode) uint64 {
	if len(node.data) > BTREE_PAGE_SIZE {
		panic("error size")
	}
	ptr := db.page.flushed + uint64(len(db.page.temp))
	db.page.temp = append(db.page.temp, node.data)
	return ptr
}

func (db *KV) pageDel(uint64)  {
	
}

在写入挂起的页面之前, 我们可能首先扩展文件. 对应的系统调用是fallocate

func extendFile(db *KV, npages int) error {
	filePages := db.mmap.file / BTREE_PAGE_SIZE
	if filePages >= npages {
		return nil
	}

	for filePages < npages {
		inc := filePages / 8
		if inc < 1 {
			inc = 1
		}
		filePages += inc
	}
	
	fileSize := filePages * BTREE_PAGE_SIZE
	err := syscall.Fallocate(int(db.fp.Fd()), 0, 0, int64(fileSize))
	if err != nil {
		return fmt.Errorf("fallocate: %w", err)
	}
	
	db.mmap.file = fileSize
	return nil
}

初始化数据库

将我们已经完成的放在一起

func (db *KV) Open() error {
	fp, err := os.OpenFile(db.Path, os.O_RDWR|os.O_CREATE, 0664)
	if err != nil {
		return fmt.Errorf("OpenFile: %w", err)
	}
	db.fp = fp
	
	sz, chunk, err := mmapInit(db.fp)
	if err != nil {
		goto fail
	}
	db.mmap.file = sz
	db.mmap.total = len(chunk)
	db.mmap.chunks = [][]byte{chunk}
	
	db.tree.get = db.pageGet
	db.tree.new = db.pageNew
	db.tree.del = db.pageDel
	
	err = masterLoad(db)
	if err != nil {
		goto fail
	}
	
	return nil
	
	fail:
		db.Close()
		return fmt.Errorf("KV.Open: %w", err)
}

func (db *KV) Close()  {
	for _, chunk := range db.mmap.chunks {
		err := syscall.Munmap(chunk)
		if err != nil {
			panic("error")
		}
	}
	_ = db.fp.Close()
}

更新操作

与查询不同, 更新操作在返回之前必须持久化数据

func (db *KV) Get(key []byte) ([]byte, bool)  {
	return db.tree.Get(key)
}

func (db *KV) Set(key []byte, val []byte) error {
	db.tree.Insert(key, val)
	return flushPages(db)
}

func (db *KV) Del(key []byte) (bool, error) {
	deleted := db.tree.Delete(key)
	return deleted, flushPages(db)
}

flushPages是持久化新页的函数

func flushPages(db *KV) error {
	if err := writePages(db); err != nil {
		return err
	}

	return syncPages(db)
}

如前所述, 它分为两个阶段

func writePages(db *KV) error {
	npages := int(db.page.flushed) + len(db.page.temp)
	if err := extendFile(db, npages); err!= nil {
		return err
	}
	if err := extendMmap(db, npages); err != nil {
		return err
	}

	for i, page := range db.page.temp {
		ptr := db.page.flushed + uint64(i)
		copy(db.pageGet(ptr).data, page)
	}
	
	return nil
}

fsync在它们之间和之后.

func syncPages(db *KV) error {
	if err := db.fp.Sync(); err != nil {
		return fmt.Errorf("fsync: %w", err)
	}
	db.page.flushed += uint64(len(db.page.temp))
	db.page.temp = db.page.temp[:0]

	if err := masterLoad(db); err != nil {
		return err
	}
	if err := db.fp.Sync(); err != nil {
		return fmt.Errorf("fsync: %w", err)
	}
	return nil
}

我们的KV存储是功能性的, 但是当我们更新数据库时, 文件不可能永远增长, 我们将在下一章通过重用磁盘页来完成kv存储.

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/559756.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

黑马程序员的基础班都是一些什么内容?

黑马推出了基础班的课程&#xff0c;各学科点击申请基础班试学 Java学科基础班 JavaSE基础基础班阶段一 课时:9天 技术点:60项 测验:1次 学习方式:线下面授 学习目标 1.掌握Java开发环境基本配置 2.掌握运算符、表达式、流程控制语句、数组等的使用 3.熟练使用IDEA开发工具…

SQL优化的思路和步骤

数据库优化 创建索引: 创建合适的索引提高查询速度 分库分表:当一张表的数据比较多或者一张表的某些字段的值比较多并且使用时改用水平分表和垂直分表来优化 读写分离(集群): 当一台服务不能满足需要时&#xff0c;采用读写分离的方式进行集群 缓存: 使用redis来进行缓存 …

ServerBoss:国产免费的Linux连接工具,服务器管理工具

在这个数字化时代&#xff0c;Linux正在成为越来越多企业的首选操作系统。但是&#xff0c;由于它复杂的命令行界面和复杂的文件系统&#xff0c;许多用户可能会认为Linux不太友好和难以驾驭。同时目前大部分Linux连接工具都是国外产品&#xff0c;且需要商业授权。在此背景下&…

案例14:Java酒店管理系统设计与实现开题报告

博主介绍&#xff1a;✌全网粉丝30W,csdn特邀作者、博客专家、CSDN新星计划导师、java领域优质创作者,博客之星、掘金/华为云/阿里云/InfoQ等平台优质作者、专注于Java技术领域和毕业项目实战✌ &#x1f345;文末获取源码联系&#x1f345; &#x1f447;&#x1f3fb; 精彩专…

产研团队任务管理工具:盘点国内外9款知名任务管理系统软件

以下是10款国内外最知名的任务管理系统&#xff1a;1.研发项目任务管理-PingCode&#xff1b;2.通用项目任务管理-Worktile&#xff1b;3.免费开源研发任务工具-Redmine&#xff1b;4.海外著名项目任务管理工具-Asana&#xff1b;5.免费任务管理软件-Trello&#xff1b;6.个人任…

springboot防止反编译proguard+xjar

一、背景 项目组核心代码模块部署于用户服务器上&#xff0c;直接甩jar包到服务器的方式&#xff0c;极有可能导致数据泄露和代码泄露&#xff0c;为了防止有技术能力的用户反编译我们的程序&#xff0c;采用了proguard和xjar两种方式来混淆和加密jar包&#xff0c;注&#xf…

shell和ansible自动化运维实例

目录 1、找到java进程并kill 2、nohup启动jar包&#xff0c;并按日期写入log日志 3、vscode上传服务器 4、ansible-playbook的copy模块 5、ansible-playbook的cron模块 6、ansible将文件批量推送至其他服务器的指定目录 将N台电脑上的java程序定时重启&#xff0c;不用每隔…

霍尔电流传感器的注意事项及其在直流列头柜中的应用

安科瑞虞佳豪 霍尔电流传感器​注意事项 &#xff08;1&#xff09;电流传感器必须根据被测电流的额定有效值适当选用不同的规格的产品。被测电流长时间超额&#xff0c;会损坏末极功放管&#xff08;指磁补偿式&#xff09;&#xff0c;一般情况下&#xff0c;2倍的过载电流…

清华开源图文对话大模型!表情包解读有一手,奇怪的benchmark增加了

丰色 萧箫 发自 凹非寺 量子位 | 公众号 QbitAI 什么&#xff0c;最懂表情包的中文开源大模型出现了&#xff1f;&#xff1f;&#xff01; 就在最近&#xff0c;来自清华的一个叫VisualGLM-6B的大模型在网上传开了来&#xff0c;起因是网友们发现&#xff0c;它连表情包似乎…

物联协议整理——蓝牙BLE

最近公司很多物联设备都使用BLE蓝牙和ZigBee通信&#xff0c;中间对设备功耗要求很高&#xff0c;补充下相关知识。 蓝牙协议栈 PHY层&#xff08;Physical layer物理层&#xff09;。PHY层用来指定BLE所用的无线频段&#xff0c;调制解调方式和方法等。PHY层做得好不好&#…

编译原理之词法分析实验(附完整C/C++代码与总结)

一、实验内容 通过完成词法分析程序&#xff0c;了解词法分析的过程。编制一个读单词程序&#xff0c;对PL/0语言进行词法分析&#xff0c;把输入的字符串形式的源程序分割成一个个单词符号&#xff0c;即基本保留字、标识符、常数、运算符、分界符五大类。 对PL/0语言进行词法…

关于VSCODE的插件 一

官方API文档 1. 要学好TypeScript。 官方教程 1.1TypeScript是一门弱类型语言。 强类型和弱类型主要是站在变量类型处理的角度进行分类的。这些概念未经过严格定义&#xff0c;它们并不是属于语言本身固有的属性&#xff0c;而是编译器或解释器的行为。主要用以描述编程语言…

IT知识百科:三大云计算模型IAAS、PAAS、SAAS

引言 云计算已经成为现代IT架构的核心组成部分&#xff0c;而云服务模型是构建和交付云计算服务的关键概念。在云服务模型中&#xff0c;IAAS、PAAS和SAAS是最常见的三种模型。 本文将深入介绍这三种模型&#xff0c;探讨它们的特点、优势以及在不同场景下的适用性。 IAAS&am…

MySQL学习教程

目录 一、数据库操作 1.查看数据库版本号 2.创建数据库 3.查看指定的数据库 4.查看所有的数据库 5.删除指定的数据库 6.使用指定的数据库 7.数据库存储引擎介绍 二、数据库表说明 1.数据库表常见的列类型 2.数据库表的字段属性 三、数据库表操作 1.创建数据库表 2…

APlayer MetingJS 音乐播放器使用指南

文章目录 1.引用2.安装3.APlayer 原生用法4.MetingJS 的用法 1.引用 APlayer 是一个简洁漂亮、功能强大的 Html5 音乐播放器&#xff0c;GitHub地址&#xff1a;https://github.com/DIYgod/APlayer MetingJS 是为 APlayer 添加网易云、QQ音乐等支持的插件&#xff0c;GitHub地…

Servlet的使用与部署

目录 Servlet概念 创建一个Servlet程序 1、创建项目 2、导入依赖 3、创建目录 4、编写代码 5、打包程序 6、部署程序 7、验证程序 Servlet概念 Servlet 是一种实现动态页面的技术 . 是一组 Tomcat 提供给程序猿的 API, 帮助程序猿简单高效的开发一个 web app. S…

喜讯!热烈祝贺安科瑞DJSF1352-RN/D直流电能表取得UL证书

安科瑞虞佳豪 UL认证是由美国安全实验室&#xff08;Underwriters Laboratories&#xff09;提供的安全性认证服务。UL认证虽然不是强制的&#xff0c;但它是北美市场的保证&#xff0c;有UL标志的产品具有很高的市场认可度。 2安科瑞导轨式直流电能表 安科瑞导轨式直流电能…

visualgo学习与使用

前言&#xff1a;在反反复复学习数据结构和算法的过程中“邂逅”了visualgo----这款超级棒的学习网站。喜悦之情不亚于我以前玩前端时发现codepen时的快乐。 地址&#xff1a;https://visualgo.net/en visualgo是新加坡国立大学计算机学院一位很棒的博士老师Dr. Steven Halim …

基于M1芯片的Mac的k8s搭建

基础环境 centos8 macbook pro M1 vm vm安装centos8参考:MacBook M1芯片 安装Centos8 教程&#xff08;无界面安装&#xff09;_m1安装centos 8.4_Mr_温少的博客-CSDN博客 步骤 参考: MacOS M1芯片CentOS8部署搭建k8s集群_Liu_Shihao的博客-CSDN博客 所有机器前置配置 …

SSH登录和SSH免密登录

在了解ssh的时候产生了概念混淆&#xff0c;发现ssh登录和ssh免密登录是两码事。 可以从目的和过程对比这两个概念&#xff1a; 1.目的 1.1 SSH登录 简单来说就是&#xff1a;建立客户端和服务器之间安全的远程连接&#xff0c;登录远程服务器&#xff0c;以访问文件系统 。…