rosedb01--Bitcask 简单实现

前言

在KV存储引擎的世界中,Bitcask与B+树、LSM树等概念相似,都是用于高效管理键值对的数据结构。Bitcask通过在内存中维护一个索引表,记录每个键对应的值在磁盘文件中的位置,实现了快速的数据检索。

相比于另外两种模型,Bitcask是一个更加简单,易于理解的模型。它是 LSM树的一种变形,通过顺序写日志,合并清理冗余日志提升写性能。

架构设计 & 简单代码实现

Bitcask

一个 Bitcask 数据库对应一个目录,包含一个活动文件(active file)和一个或多个旧文件(old files)。每个文件由多个条目(entry)组成,每个条目存储一个键值对。

entry

// Entry 磁盘文件是多个 Entry的集合
type Entry struct {
    Key       []byte
    Value     []byte
    KeySize   uint32 //单位是字节
    ValueSize uint32
    Mark      uint16 // 标记是删除/新增操作
}

file

type DBFile struct {
    // 实际的文件
    File *os.File
    // 偏移量 即文件现在写到那里
    Offset int64
    // 对应正在操作的 Entry
    HeaderBufPool *sync.Pool
}

Bitcask

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

内存哈希表(indexes)

bitcask的特点在于 在内存中维护了一个 哈希表,这个哈希表存储了 每个key 在哪个文件,位置及最近一次更新得到大小。对应 上述 Bitcaskindexes

图片来源:数据库存储系列(3)解析 Bitcask 存储引擎原理高性能的 Key Value 存储在今天的互联网技术圈里几乎是一个 - 掘金 (juejin.cn)

读请求(Read)

当接收到读请求时,首先在内存中的 indexes 中查找键的偏移量,如果找到,则读取磁盘文件中的数据。

// Get 取出数据
func (db *MiniBitcask) Get(key []byte) (val []byte, err error) {
    if len(key) == 0 {
        return
    }
​
    db.mu.RLock()
    defer db.mu.RUnlock()
​
    offset, err := db.exist(key)
    if errors.Is(ErrKeyNotFound, err) {
        return
    }
​
    e, err := db.dbFile.Read(offset)
    if err != nil && err != io.EOF {
        return
    }
    if e != nil {
        val = e.Value
    }
    return
}

写请求(Put/Delete)

写操作仅能写入到活动文件内,写入后更新内存中的索引。

// Put 写入数据
func (db *MiniBitcask) Put(key []byte, value []byte) (err error) {
    if len(key) == 0 {
        return
    }
    //加锁
    db.mu.Lock()
    defer db.mu.Unlock()
​
    offset := db.dbFile.Offset
    // 封装成 Entry
    entry := NewEntry(key, value, PUT)
    // 追加到数据文件当中
    err = db.dbFile.Write(entry)
    //写到内存
    db.indexes[string(key)] = offset
    return
}

删除操作也写入到活动文件内,Mark 字段标记操作类型。

​
// Del 删除数据
func (db *MiniBitcask) Del(key []byte) (err error) {
    if len(key) == 0 {
        return
    }
​
    db.mu.Lock()
    defer db.mu.Unlock()
​
    _, err = db.exist(key)
    if err == ErrKeyNotFound {
        err = nil
        return
    }
​
    // 封装成 Entry 并写入
    e := NewEntry(key, nil, DEL)
    err = db.dbFile.Write(e)
    if err != nil {
        return
    }
​
    // 删除内存中的 key
    delete(db.indexes, string(key))
    return
}

清理旧数据(Merge)

合并所有的旧文件,压缩空间。

从文件开头开始读 entry,如果该entry的key在内存中,说明这个entry需要保留。如果不在,说明已经被删了

func (db *MiniBitcask) Merge() error {
    if db.dbFile.Offset == 0 {
        return nil
    }
    var (
        validEntries []*Entry
        offset       int64
    )
    // 读取数据文件的Entry
    for {
        e, err := db.dbFile.Read(offset)
        if err != nil {
            if err == io.EOF {
                break
            }
            return err
        }
        //内存的索引状态是最新的,直接对比过滤出有效的 Entry
        off, ok := db.indexes[string(e.Key)]
        if ok && off == offset {
            validEntries = append(validEntries, e)
        }
        offset += e.GetSize()
    }
    mergeFile, err := newMergeFile(db.dirPath)
    if err != nil {
        return err
    }
    // 执行完删除合并文件
    defer func() {
        _ = os.Remove(mergeFile.File.Name())
    }()
​
    db.mu.Lock()
    defer db.mu.Unlock()
​
    //写入有效的Entry
    for _, entry := range validEntries {
        off := mergeFile.Offset
​
        // offset在Write里被更新
        err = mergeFile.Write(entry)
        if err != nil {
            return err
        }
        //更新索引
        db.indexes[string(entry.Key)] = off
    }
​
    dbFileName := db.dbFile.File.Name()
    // 关闭文件
    _ = db.dbFile.File.Close()
    _ = os.Remove(dbFileName)
​
    _ = mergeFile.File.Close()
    mergeFileName := mergeFile.File.Name()
    // 临时文件更名为数据文件
    _ = os.Rename(mergeFileName, filepath.Join(db.dirPath, FileName))
​
    dbFile, err := newDBFile(db.dirPath)
    if err != nil {
        return err
    }
    db.dbFile = dbFile
    return nil
}

启动

// 从文件当中加载索引
func (db *MiniBitcask) loadIndexesFromFile() {
    if db.dbFile == nil {
        return
    }
​
    var offset int64
    for {
        e, err := db.dbFile.Read(offset)
        if err != nil {
            // 读取完毕
            if err == io.EOF {
                break
            }
            return
        }
        // 设置索引状态
        db.indexes[string(e.Key)] = offset
​
        if e.Mark == DEL {
            //删除内容中的key
            delete(db.indexes, string(e.Key))
        }
        offset += e.GetSize()
    }
}

总结

Bitcask 是一种简单而高效的键值存储机制,主要通过顺序写入和合并操作来优化性能。虽然 Bitcask 易于理解并实现,但在实际应用中仍需解决以下问题:

  • 存储索引:如何高效存储索引信息,减少内存和磁盘空间的使用。

  • 高效构建内存哈希表:如何利用提示文件(hint file)等方式加速内存哈希表的构建。

存在的问题

  • 无法支持range scan

  • merge操作和写入同时进行的时候,会变成随机io,性能下降

代码地址

mini-bitcask: 学习数据库bitcask存储模型 (gitee.com)

  • 26
    点赞
  • 25
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值