对mini-bitcask
的学习,从零实现一个k-v存储引擎
原项目的github地址,感谢Rose大佬
mini-bitcask
为rosedb
的mini
版本,博主借此了解k-v存储,该项目通过对一个数据文件进行读写以进行简单示意,而非rosedb
的多个数据文件的机制,这里也提供rosedb的github地址
简而言之的话,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
接下来对主要部分进行注释和分析
db_file.go
定义了MiniBitcask
结构体,index
使用map在内存中进行key-value的存储,dbfile和dirpath
是数据文件的相关信息,mu
则用来进行并发的保护
type MiniBitcask struct {
indexes map[string]int64 // 内存中的索引信息
dbFile *DBFile // 数据文件
dirPath string // 数据目录
mu sync.RWMutex
}
db.go
主要功能为提供数据库的集成功能,创建数据库实例OPEN
实现方法写入数据PUT
、取出数据GET
、删除数据DEL
、合并数据文件merge
、关闭db实例CLOSE
,也是我们主要使用的功能
实现过程从内存中获取索引exist
、从数据文件加载索引loadIndexesFromFile
这一部分在原文和源文件中就有代码的分析,这里不再赘述
该部分有关数据文件定义
DBFile结构体包括对应数据文件的句柄,以及下次写入的偏移量、
// DBFile 数据文件定义
type DBFile struct {
File *os.File
Offset int64
HeaderBufPool *sync.Pool
}
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
}
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
}
write
方法的实现可以参考read
Entry
定义这里的一条记录,该包即为如何包装一条记录
一条entry
包括key、value的值和大小
,标志位mark
// Entry 写入文件的记录
type Entry struct {
Key []byte
Value []byte
KeySize uint32
ValueSize uint32
Mark uint16
}
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
}