Go 存储系列:Hash存储引擎 Bitcask

news2024/11/28 0:34:26

Hash 存储引擎

在现代软件系统中,存储和检索数据是一个非常重要的任务。随着数据量的不断增长,如何高效地存储和检索数据变得越来越重要。Hash 存储引擎是一种常见的存储引擎,它可以快速地存储和检索数据。

在本文中,我们将介绍 Hash 存储引擎的工作原理,并分析一个 Go 语言实现一个简单的 Hash 存储引擎:tiny-bitcask。tiny-bitcask 是参考论文:https://riak.com/assets/bitcask-intro.pdf 开发出的一个 简单版本的demo。

通过分析 tiny-bitcask,有助于我们理解一个存储一系统是如何构造的,包括内存结构,磁盘文件结构,如何从内存结构找到对应的磁盘文件内容。

Bitcask 介绍

Bitcask 是一种底层格式为日志模样的 kv 存储,就是只追加,保证文件是一直顺序写入的,写入性能非常好

Bitcask模型的整体结构

在这里插入图片描述

  • Bitcask 只有一个可写的文件。 可写的文件叫做 active data file,只读的叫做 older data file。
  • 写数据只写 active data file,而且是顺序写入,读数据从 active data file 和 older data
    file 中读 hint file:
  • 在合并 data file 产生的文件,用于快速重新建立内存索引,可以理解成是在磁盘中的索引文件

Bitcask 它的文件格式

在这里插入图片描述
crc:校验码,tstamp:时间戳,ksz:key 大小,value_sz 值大小

Bitcask 它内存的索引结构

在这里插入图片描述
file_id: 所在的物理文件,value_sz: 值大小,value_pos: value 在文件中的位置,tstamp:时间戳

数据操作

  • 写入数据:Bitcask 先写文件,持久化落盘之后更新内存 hash 表。
  • 读取数据:从内存索引查询key,从这里知道了value所在的file_id,位置,大小,然后只要调用系统的读取接口就行了

在这里插入图片描述

  • 删除数据 :不直接删除记录,而是新增一条相同key的记录,把value设置一个删除的标记 原有记录依然存在于数据文件中,只是更新索引哈希表
  • 修改数据 :Bitcask不支持随机写入,修改数据时不会找到目标记录进行修改 还是新增一条相同key的记录,把value设置为新值
  • 旧数据处理:Bitcask会定期进行Marge操作,扫描所有旧数据文件中的数据,生成新的数据文件 扫描时,把已经被置为删除状态的记录直接过滤掉,修改过的数据,只保留时间最近的一条
  • Marge 操作:会遍历所有不可变的旧数据文件,将所有有效的数据重新写到新的数据文件中,并且将旧的数据文件删除掉。
    在这里插入图片描述

tiny-bitcask 实现

在上面介绍了 bitcask 的基本概念后,我们知道了 bitcask 大概有哪些部分

  • Entry:代表DB中一条数据的信息。
  • DataFiles:磁盘中的文件
  • Storage:与文件系统打交道的对象,包括了写入,读取数据。将数据写入到 DataFiles 中
  • Index,索引,记录一条数据的具体信息,主要是数据在磁盘中的位置。
  • DB,DB的实体。包含了DB的各种操作,包括读取,写入数据。

数据结构

DB 结构

type DB struct {
    rw      sync.RWMutex // 读写锁
    kd      *index.KeyDir // 内存index
    storage *storage.DataFiles // 文件
    opt     *Options // 参数
}

Entry 结构

Entry 的结构 就是对应着 Bitcask 的文件格式,其中 Flag 是用来标识是否是删除的记录

type Entry struct {
    Key   []byte
    Value []byte
    Meta  *Meta
}
 
type Meta struct {
    Crc       uint32
    position  uint64
    TimeStamp uint64
    KeySize   uint32
    ValueSize uint32
    Flag      uint8
}

一条数据是以怎么样的形式存进磁盘的。磁盘是不知道你存进来的数据是什么的,他只知道是一些二进制,至于那些二进制是什么,由放进来的应用程序来定义。所以,我们要对数据写入到磁盘的时候继续编码,也就是编码成二进制,其中我们的 Meta 这个结构体就是记录了我们的数据的一些圆信息,我们要利用这些信息进行编码。

func (e *Entry) Encode() []byte {
    size := e.Size() // int64(MetaSize + e.Meta.KeySize + e.Meta.ValueSize)
    buf := make([]byte, size)
    binary.LittleEndian.PutUint64(buf[4:12], e.Meta.position) // 这里并没有实际作用
    binary.LittleEndian.PutUint64(buf[12:20], e.Meta.TimeStamp) // 时间戳
    binary.LittleEndian.PutUint32(buf[20:24], e.Meta.KeySize) // key 大小
    binary.LittleEndian.PutUint32(buf[24:28], e.Meta.ValueSize) // value 大小
    buf[28] = e.Meta.Flag
    if e.Meta.Flag != DeleteFlag {
        copy(buf[MetaSize:MetaSize+len(e.Key)], e.Key)
        copy(buf[MetaSize+len(e.Key):MetaSize+len(e.Key)+len(e.Value)], e.Value)
    }
    c32 := crc32.ChecksumIEEE(buf[4:])
    binary.LittleEndian.PutUint32(buf[0:4], c32) // 写入CRC
    return buf
}

DataFiles 磁盘文件

type DataFiles struct {
    dir         string // 数据目录
    oIds        []int // old data files 的文件列表
    segmentSize int64
    active      *ActiveFile // active data file 文件
    olds        map[int]*OldFile //  old data files 的文件列表 
}

写入过程

Set: db 暴露出来的接口,写入一个key value 值

func (db *DB) Set(key []byte, value []byte) error {
    db.rw.Lock()
    defer db.rw.Unlock()
    entry := entity.NewEntryWithData(key, value) // Entry
    h, err := db.storage.WriterEntity(entry) // 写入到文件
    if err != nil {
        return err
    }
    db.kd.AddIndexByData(h, entry) // 更新内存 index
    return nil
}

WriterEntity: 写入 Entity 到文件

func (af *ActiveFile) WriterEntity(e entity.Entity) (h *entity.Hint, err error) {
    buf := e.Encode() // 编码
    n, err := af.fd.WriteAt(buf, af.off) // 写入到 文件 offset
    if n < len(buf) {
        return nil, WriteMissDataErr
    }
    if err != nil {
        return nil, err
    }
    h = &entity.Hint{Fid: af.fid, Off: af.off} 
    af.off += e.Size() // 更新文件下一个offset
    return h, nil
}

AddIndexByData: 更新内存 index

// DataPosition means a certain position of an entity.Entry which stores in disk.
type DataPosition struct {
    Fid       int // 文件 fd
    Off       int64 // 所在文件的偏移量
    Timestamp uint64
    KeySize   int
    ValueSize int
}
 
 
func (kd *KeyDir) AddIndexByRawInfo(fid int, off int64, key, value []byte) {
    index := newDataPosition(fid, off, key, value)
    kd.Add(string(key), index) // 记录到内存
}
 
 
func newDataPosition(fid int, off int64, key, value []byte) *DataPosition {
    dp := &DataPosition{}
    dp.Fid = fid
    dp.Off = off 
    dp.KeySize = len(key)
    dp.ValueSize = len(value)
    return dp
}

读取流程

Get:db 暴露出来的接口,读取一个key的值

// Get gets value by using key
func (db *DB) Get(key []byte) (value []byte, err error) {
    db.rw.RLock()
    defer db.rw.RUnlock()
    i := db.kd.Find(string(key))
    if i == nil {
        return nil, KeyNotFoundErr
    }
    entry, err := db.storage.ReadEntry(i)
    if err != nil {
        return nil, err
    }
    return entry.Value, nil
}

ReadEntry:从文件中读取一个 Entry

func (dfs *DataFiles) ReadEntry(index *index.DataPosition) (e *entity.Entry, err error) {
    dataSize := entity.MetaSize + index.KeySize + index.ValueSize
    if index.Fid == dfs.active.fid {
        return dfs.active.ReadEntity(index.Off, dataSize) // 从acive 文件中读取
    }
    of, exist := dfs.olds[index.Fid]// 从 old data file 中读取
    if !exist {
        return nil, MissOldFileErr
    }
    return of.ReadEntity(index.Off, dataSize)
}
func readEntry(fd *os.File, off int64, length int) (e *entity.Entry, err error) {
    buf := make([]byte, length)
    n, err := fd.ReadAt(buf, off) // 根据 key 在内存中读取问津啊对应的 offset 记录
    if n < length {
        return nil, ReadMissDataErr
    }
    if err != nil {
        return nil, err
    }
    e = entity.NewEntry()
    e.DecodeMeta(buf[:entity.MetaSize])
    e.DecodePayload(buf[entity.MetaSize:]) // 解码,从磁盘数据还原成写入时的数据
    return e, nil
}

Marge 操作

Marge 操作的流程就是

  • 获取到old data file
  • 看下当前内存 index中有没有数据,这条数据可能被删除了,如果在内存index 没有找到,就是被删除了,不需要处理
  • 比较一些当前读到的这个数据是不是最新的数据,如果是,则需要写入到 active data file 中
  • 遍历完成一个之后,就删除老的文件,继续遍历下一个
// Merge clean the useless data
func (db *DB) Merge() error {
    db.rw.Lock()
    defer db.rw.Unlock()
    fids := db.storage.GetOldFiles()
    if len(fids) < 2 {
        return NoNeedToMergeErr
    }
    sort.Ints(fids)
    for _, fid := range fids[:len(fids)-1] {
        var off int64 = 0
        reader := db.storage.GetOldFile(fid) // 获取到old data file
        for {
            entry, err := reader.ReadEntityWithOutLength(off) // 读取一条数据
            if err == nil {
                key := string(entry.Key)
                off += entry.Size()
                oldIndex := db.kd.Find(key) // 看下当前内存 index中有没有数据,这条数据可能被删除了
                if oldIndex == nil {
                    continue
                }
                // oldIndex 是这个 key 最新的文件
                if oldIndex.IsEqualPos(fid, off) { // 比较一些当前读到的这个数据是不是最新的数据,如果是,则需要写入到 active data file 中
                    h, err := db.storage.WriterEntity(entry)
                    if err != nil {
                        return err
                    }
                    db.kd.AddIndexByData(h, entry) // 更新内存index
                }
            } else {
                if err == io.EOF {
                    break
                }
                return err
            }
        }
        err := db.storage.RemoveFile(fid) // 删除老的文件
        if err != nil {
            return err
        }
    }
    return nil
}
 

总结

  • 大部分接触的KV存储引擎是可能都是Redis。Redis的所有数据都是装在内存的,也可以根据配置持久化在磁盘里面,但是读都是从内存里面读的,这意味着redis的读写速度都非常快。
  • 但是这有一个限制,那就是单机Redis存储的数据不能大于内存本身。而Bitcask的最大限制是内存必须装得下所有的key,因为Bitcask的value是存在磁盘上的。所以相比Redis,Bitcask的存在意义不是和redis比速度,而是当你的数据用Redis存不下的时候,可以考虑稍微损失读取效率,试试Bitcask。

参考

  • Go存储引擎资料汇总
  • https://riak.com/assets/bitcask-intro.pdf

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

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

相关文章

深入篇【C++】类与对象:友元函数与友元类

深入篇【C】类与对象&#xff1a;友元函数与友元类 ①.提出问题&#xff1a;重载operator<<②.解决问题&#xff1a;友元Ⅰ.友元函数【特点】 Ⅱ.友元类【特点】 ③.总结问题 ①.提出问题&#xff1a;重载operator<< 如果我们尝试去重载运算符operator<<,你…

【JavaSE】Java基础语法(十八):接口

文章目录 1. 接口的概述2. 接口的特点3. 接口的成员特点4. 类和接口的关系5. 抽象类和接口的关系 1. 接口的概述 接口就是一种公共的规范标准&#xff0c;只要符合规范标准&#xff0c;大家都可以通用。Java中接口存在的两个意义 用来定义规范用来做功能的拓展 2. 接口的特点…

SpringBoot3.0升级遇到关于Invalid bound statement (not found)处理方案

前言 今天升级SpringBoot3时尝试兼容Mybatis和MybatisPlus出现多个异常。顺带写写排查方案&#xff0c;springboot2其实也一样用 排查方向&#xff1a;mapper接口中的方法名和mapper.xml中的id标签不一致 仔细核对抛出异常的接口和xml文件名。方法名排查方向&#xff1a;.map…

基于docker容器化的jenkins2.406升级迁移(jdk8升级jdk11)

查看基础配置 查看jenkins的home路径: 查看磁盘占比: 发现占比比较大的是: 主要子目录&#xff1a; jobs&#xff1a;包含Jenkins管理的构建作业的配置细节、构建产物和数据&#xff1b;logs&#xff1a;Jenkins的日志目录&#xff1b;plugins&#xff1a;包含所有已经安装了…

低代码开发平台助力门店管理创新,提升用户体验

随着信息技术的飞速发展&#xff0c;低代码开发成为了近年来热门的开发方式。同时&#xff0c;在零售业中&#xff0c;门店管理也成为了一个重要的议题。本文将结合低代码开发和门店管理两个主题&#xff0c;探讨如何应用低代码技术优化门店管理。 一、门店管理的挑战 门店管…

Ansible从入门到精通【二】

大家好&#xff0c;我是早九晚十二&#xff0c;目前是做运维相关的工作。写博客是为了积累&#xff0c;希望大家一起进步&#xff01; 我的主页&#xff1a;早九晚十二 专栏名称&#xff1a;Ansible从入门到精通 立志成为ansible大佬 文章目录 ansible常用命令ansibleansible-d…

Revit幕墙:用幕墙巧做屋面瓦及如何快速幕墙?

一、Revit中用幕墙巧做屋面瓦 屋面瓦重复性很高&#xff0c;我们如何快速的创建呢?下面我们来学会快速用幕墙来创建屋面瓦的技巧。 1.新建“公制轮廓-竖挺”族&#xff0c;以此来创建瓦的族(以便于载入项目中使用) 2.在轮廓族中绘制瓦的轮廓(轮廓需要闭合)&#xff0c;将族名称…

淘宝天猫618预售活动时间是从几号什么时候开始2023天猫淘宝618预售定金能退吗?

2023年淘宝天猫618预售什么时候开始&#xff1f; 2023年5月26日20:00淘宝天猫618预售活动开始截至到5月31日18:00结束&#xff1b; 2023年淘宝天猫618预售定金支付后可退吗&#xff1f; 淘宝天猫618预售定金支付后如不想要该预售商品了&#xff0c;可以在5月31日20:00后完成尾…

如何获得高清、4K无水印视频素材?教你轻松拥有高清视频

随着短视频越来越火爆&#xff0c;大家也都加入到了视频创作的行业中&#xff0c;平时也会喜欢剪辑一些视频发布到平台上&#xff0c;那高清的短视频肯定是最受欢迎的&#xff0c;我们自己又如何获得高清的视频呢&#xff1f; 一、为什么需要高清的视频素材&#xff1f; 1. 视…

基于非靶向和靶向代谢组学分析婴幼儿血管瘤的氨基酸代谢

文章标题&#xff1a;Integrated nontargeted and targeted metabolomics analyses amino acids metabolism in infantile hemangioma 发表期刊&#xff1a;Frontiers in Oncology 影响因子&#xff1a;5.738 作者单位&#xff1a;四川大学华西医院 百趣提供服务&#xf…

prometheus 部署安装

prometheus 部署安装 监控系统硬件&#xff08;node-exporter&#xff09;监控mysql &#xff08;mysql_exporter&#xff09;监控redis&#xff08;redis_exporter&#xff09;监控docker &#xff08;cadvisor&#xff09;监控可视化展示 (Grafana)监控报警 &#xff08;Ale…

Adams几何元素

简单学了一下Adams的几何元素&#xff0c;记录一下面板。 1.几何点 Point Add to Ground 添加到大地 Add to Part 添加到现有构件 Attach Near 关联构件 Don’t Attach 不关联构件 创建添加行&#xff0c;输入点坐标。创建好的点可以在导航栏右键删除和修改。 2. 坐标系Mar…

ROS学习——利用电脑相机标定

一、 安装usb-cam包和标定数据包 sudo apt-get install ros-kinetic-usb-cam sudo apt-get install ros-kinetic-camera-calibration 要把kinetic改成你自己的ros版本 。 二、启动相机 roslaunch usb_cam usb_cam-test.launch 就会出现一个界面 可以通过下面命令查看相机…

leetcode 1140. Stone Game II(石头游戏II)

涉及game的问题&#xff0c;2个player, 现有几堆石头&#xff0c;每堆石头个数为piles[i], 刚开始M1, player1先拿石头&#xff0c;可以拿走前 x 堆&#xff08;假设从第 i 堆开始拿&#xff0c;可以拿 i ~ ix-1 堆&#xff09;&#xff0c;1 < x < 2M, 拿完之后&#xf…

2023.5.19Hadoop具体操作(四种)

大作业 1、ens33没有地址 查看虚拟机的NAT8网段 使用ip a显示ens33的ip ip a设置静态ip 编辑网络接口配置文件&#xff1a;输入以下命令来编辑网络接口的配置文件&#xff1a; sudo vi /etc/network/interfaces在打开的文件中&#xff0c;找到要设置为静态IP的网络接口&am…

来领走你的AI老师

现在很多大学生不上课&#xff0c;在b站学习。 有没有想过有一天&#xff0c;你的大多数时间都在跟AI学习&#xff1f; 未来已来&#xff0c;这里有一份万能提示词&#xff0c;让你立马拥有一位AI导师。 这位导师可了不得&#xff0c;除了啥都知道之外&#xff0c;还能&…

C# 队列(Queue)

目录 一、概述 二、基本的用法 1.添加元素 2.取出元素 1&#xff09;Dequeue 方法 2&#xff09;Peek 方法 3.判断元素是否存在 4.获取队列的长度 5.遍历队列 6.清空容器 7.Queue 泛型类 三、结束 一、概述 表示对象的先进先出集合。 队列和其他的数据结构一样&a…

Ros2中MoveItConfigsBuilder的功能作用说明

文章目录 前言MoveItConfigsBuilder的功能作用机器人resource文件样例总结 前言 在学习moveit2的样例时发现加载机器人配置参数多使用MoveItConfigsBuilder&#xff0c;它具体的功能和使用方法是什么呢。 这篇博文用来记录说明该函数的使用方法、作用和调用逻辑。 MoveItConfi…

Tomcat的讲解与安装

文章目录 前言一.Tomcat是什么二.Tomcat的原理三.Tomcat的安装和说明**1.下载****2.解压安装**bin目录conf目录lib目录log目录temp目录webapps目录work目录 3.配置环境变量 四.验证安装 前言 Tomcat 是一个 HTTP 服务器. 前面我们已经学习了 HTTP 协议, 知道了 HTTP 协议就是 …

共享电单车RFID停车技术分析

近段时间&#xff0c;某地主城区运营商信号基站受严重干扰&#xff0c;造成300多个基站&#xff0c;超过5万的用户受到影响。据无线电监测站的调查确认干扰源来自共享电单车&#xff0c;是共享电单车加装的RFID停车标签惹的祸&#xff0c;而该地区RFID终端选用的是超高频&#…