• [Golang]K-V存储引擎的学习 从零实现 (RoseDB mini版本)


    项目的简单介绍

    mini-bitcask的学习,从零实现一个k-v存储引擎
    原项目的github地址,感谢Rose大佬
    mini-bitcaskrosedbmini版本,博主借此了解k-v存储,该项目通过对一个数据文件进行读写以进行简单示意,而非rosedb多个数据文件的机制,这里也提供rosedb的github地址

    详情

    此处为rose大佬提供的项目的相关知识的介绍,可供查阅

    简而言之的话,minidb设计了数据在内存、磁盘二者中的存放,使用类似LSM的存储结构。
    实现了数据PUT、GET、DELETE的功能
    核心思想为利用顺序IO来提升性能。

    代码分析

    这里博主为了学习,由于原main.go固定流程,将其改为可自定义的流程,这里附学习项目的github地址,只对main.go进行了修改,并将数据文件的地址改到项目根目录中。

    项目结构

    .
    ├── db_file.go
    ├── db.go
    ├── db_test.go
    ├── entry.go
    ├── errors.go
    ├── example
    │   └── main.go
    ├── go.mod
    ├── minidb.iml
    └── README.md
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    接下来对主要部分进行注释和分析
    db_file.go

    db.go

    定义了MiniBitcask结构体,index使用map在内存中进行key-value的存储,dbfile和dirpath数据文件的相关信息,mu则用来进行并发的保护

    type MiniBitcask struct {
    	indexes map[string]int64 // 内存中的索引信息
    	dbFile  *DBFile          // 数据文件
    	dirPath string           // 数据目录
    	mu      sync.RWMutex
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    db.go主要功能为提供数据库的集成功能创建数据库实例OPEN

    实现方法写入数据PUT、取出数据GET、删除数据DEL、合并数据文件merge、关闭db实例CLOSE,也是我们主要使用的功能

    实现过程从内存中获取索引exist从数据文件加载索引loadIndexesFromFile

    这一部分在原文和源文件中就有代码的分析,这里不再赘述

    db_file.go

    该部分有关数据文件定义
    DBFile结构体包括对应数据文件的句柄,以及下次写入的偏移量、

    // DBFile 数据文件定义
    type DBFile struct {
    	File          *os.File
    	Offset        int64
    	HeaderBufPool *sync.Pool
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    newInternal函数创建一个DBFile实例NewDBFile创建一个新的数据文件NewMergeDBFile新建一个合并时的数据文件

    func newInternal(fileName string) (*DBFile, error) {
        // 打开文件,如果不存在则创建
        file, err := os.OpenFile(fileName, os.O_CREATE|os.O_RDWR, 0644)
        if err != nil {
            return nil, err
        }
    
        // 获取文件的状态信息,这里目的是为了获取文件的大小,以设置偏移量offset
        stat, err := os.Stat(fileName)
        if err != nil {
            return nil, err
        }
    
        // 创建一个对象池,用于存储 Entry 头部数据的缓冲区
        pool := &sync.Pool{New: func() interface{} {
            return make([]byte, entryHeaderSize)
        }}
    
        // 返回一个新的 DBFile 对象,其中包括文件的偏移量、打开的文件句柄和对象池
        return &DBFile{Offset: stat.Size(), File: file, HeaderBufPool: pool}, nil
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22

    DBFile实现以下方法,Read读取数据Write写入Entry

    // Read 从 offset 处开始读取,在引擎初始化时,这里被循环调用
    func (df *DBFile) Read(offset int64) (e *Entry, err error) {
        // 从对象池中获取一个用于存储数据的缓冲区
        buf := df.HeaderBufPool.Get().([]byte)
        defer df.HeaderBufPool.Put(buf) // 确保在函数返回时将缓冲区归还给对象池
    
        // 从文件中读取数据到缓冲区
        if _, err = df.File.ReadAt(buf, offset); err != nil {
            return
        }
    
        // 解码缓冲区中的数据为 Entry 对象
        if e, err = Decode(buf); err != nil {
            return
        }
    
        // 计算键和值在文件中的偏移量,并读取键和值的数据
        offset += entryHeaderSize // 将偏移量移到键值对数据开始的位置
    
        // 如果键的大小大于 0,则读取键的数据
        if e.KeySize > 0 {
            key := make([]byte, e.KeySize) // 创建一个存储键数据的切片
            if _, err = df.File.ReadAt(key, offset); err != nil {
                return
            }
            e.Key = key // 将读取的键数据赋值给 Entry 对象
        }
    
        // 更新偏移量,准备读取值的数据
        offset += int64(e.KeySize) // 将偏移量移到值数据开始的位置
    
        // 如果值的大小大于 0,则读取值的数据
        if e.ValueSize > 0 {
            value := make([]byte, e.ValueSize) // 创建一个存储值数据的切片
            if _, err = df.File.ReadAt(value, offset); err != nil {
                return
            }
            e.Value = value // 将读取的值数据赋值给 Entry 对象
        }
    
        // 返回读取的 Entry 对象及可能的错误
        return
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44

    write方法的实现可以参考read

    entry.go

    Entry定义这里的一条记录,该包即为如何包装一条记录
    一条entry包括key、value的值和大小标志位mark

    // Entry 写入文件的记录
    type Entry struct {
    	Key       []byte
    	Value     []byte
    	KeySize   uint32
    	ValueSize uint32
    	Mark      uint16
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    NewEntry根据传入的参数返回一条Entry实例
    并实现了Entry的编码Encode解码Decode 方法
    encode定义了entry编码的顺序,decode可以参考这部分

    // Encode 编码 Entry,返回字节数组
    func (e *Entry) Encode() ([]byte, error) {
    	buf := make([]byte, e.GetSize())
    	binary.BigEndian.PutUint32(buf[0:4], e.KeySize)
    	binary.BigEndian.PutUint32(buf[4:8], e.ValueSize)
    	binary.BigEndian.PutUint16(buf[8:10], e.Mark)
    	copy(buf[entryHeaderSize:entryHeaderSize+e.KeySize], e.Key)
    	copy(buf[entryHeaderSize+e.KeySize:], e.Value)
    	return buf, nil
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
  • 相关阅读:
    Windows Server 2019 搭建WEB环境(IIS+CA)
    6 个超级良心的开源教程!
    一文搞懂nginx的反向代理 负载均衡
    还不懂JVM的设计原理与实现?这份《jvm虚拟机pdf》给你整的明明白白的
    【docker desktop】创建node:18 server
    分布式数据库OceanBase的安装
    GPIO子系统编写LED灯的驱动、linux内核定时器
    PHP文件锁同步实例
    Docker
    MySQL性能优化 - 别再只会说加索引了
  • 原文地址:https://blog.csdn.net/kingsill/article/details/136714893