Golang实现一个事务型内存数据库

2023-11-09

内存数据库经我们经常用到,例如Redis,那么如何从零实现一个内存数据库呢,本文旨在介绍如何使用Golang编写一个KV内存数据库MossDB

特性

MossDB是一个纯Golang编写、可嵌入的、键值型内存数据库,包含以下特性

  • 可持久化,类似Redis AOF(Append only Log)
  • 支持事务
  • 支持近实时的TTL(Time to Live), 可以实现毫秒级的过期删除
  • 前缀搜索
  • Watch接口,可以监听某个键值的内容变化,类似etcd的Watch
  • 多后端存储,目前支持HashMap和RadixTree

命名由来

Moss有苔、苔花的含义,MossDB的名字来源于清代袁牧的一句诗:

苔花如米小,也学牡丹开

MossDB虽小,但五脏俱全,也支持了很多重要功能。另外,巧合的是《流浪地球2》中的超级计算机550W名字就是Moss。

架构

内存数据库虽然使用简单,实现起来却有很多细节,Golang目前也存在不少优秀的开源内存数据库,比如buntdbgo-memdb,在编写MossDB过程中也借鉴了一些它们的特性。

MossDB的架构如图:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-dkMrcn4p-1677806852723)(null)]

自上往下分为:

  1. 接口层,提供API接受用户请求
  2. 核心层,实现事务、过期删除、Watch等功能
  3. 存储层,提供KV的后端存储以及增删改查
  4. 持久化层,使用AOL持久化即每次修改操作都会持久化到磁盘Log中

快速开始

MossDB可嵌入到Go程序中,可以通过go get获取

go get github.com/qingwave/mossdb

MossDB提供了易用的API,可以方便地进行数据处理,下面的示例代码展示了如何使用MossDB:

package main

import (
	"log"

	"github.com/qingwave/mossdb"
)

func main() {
    // create db instance
	db, err := mossdb.New(&mossdb.Config{})
	if err != nil {
		log.Fatal(err)
	}

    // set, get data
	db.Set("key1", []byte("val1"))
    log.Printf("get key1: %s", db.Get("key1"))

    // transaction
    db.Tx(func(tx *mossdb.Tx) error {
        val1 := tx.Get("key1")

        tx.Set("key2", val1)

        return nil
    })
}

更多示例见源码

具体实现

从下往上分别介绍下MossDB如何设计与实现,以及相关的细节。

AOF持久化

AOF源于Redis提供两种持久化技术,另外一种是RDB,AOF是指在每次写操作后,将该操作序列化后追加到文件中,重启时重放文件中的对应操作,从而达到持久化的目的。其实现简单,用在MossDB是一个不错的选择,但需要注意的是AOF缺点同样明显,如果文件较大,每次重启会花费较多时间。

Redis的AOF是一种后写式日志,先写内存直接返回给用户,再写磁盘文件持久化,可以保证其高性能,但如果中途宕机会丢失数据。MossDB中的AOF采用了WAL(预写式日志)实现,先写Log再写内存,用来保证数据不会丢失,从而可以进一步实现事务。

那么采用WAL会不会影响其性能?每次必须等到落盘后才进行其他操作,WAL的每次写入会先写到内核缓冲区,这个调用很快就返回了,内核再将数据落盘。我们也可以使用fsync调用强制内核执行直接将数据写入磁盘。在MossDB中普通写操作之会不会直接调用fsync,事务写中强制开启fsync,从而平衡数据一致性与性能。

WAL的实现引用了tiwall/wal,其中封装了对Log文件的操作,可以支持批量写入。由于WAL是二进制的,必须将数据进行编码,通过varint编码实现,将数据长度插入到数据本体之前,读取时可以读取定长的数据长度,然后按长度读取数据本体。MossDB中数据格式如下:

type Record struct {
	Op        uint16
	KeySize   uint32
	ValSize   uint32
	Timestamp uint64
	TTL       uint64
	Key       []byte
	Val       []byte
}

对应编码后的二进制格式为:

|------------------------------------------------------------|
| Op | KeySize | ValSize | Timestamp | TTL | Key    | Val    |
|------------------------------------------------------------|
| 2  | 4       | 4       | 8         | 8   | []byte | []byte |
|------------------------------------------------------------|

使用binary.BigEndian.PutUint16进行编码,解码时通过binary.BigEndian.Uint16,从而依次取得生成完整的数据。

存储引擎

MossDB提供了存储接口,只要实现了此接口都可以作为其后端存储

type Store interface {
	Get(key string) ([]byte, bool)
	Set(key string, val []byte)
	Delete(key string)
	Prefix(key string) map[string][]byte
	Dump() map[string][]byte

	Len() int
}

内置提供了HashMap与RadixTree两种方式,HashMap实现简单通过简单封装map可以快速进行查询与插入,但范围搜索性能差。RadixTree即前缀树,查询插入的时间复杂度只与Key的长度相关,而且支持范围搜索,MossDB采用Adaptive Radix Tree可以避免原生的前准树空间浪费。

由于RadixTree的特性,MossDB可以方便的进行前缀搜索,目前支持ListWatch操作。

事务实现

要实现事务必须要保证其ACID特性,MossDB的事务定义如下:

type Tx struct {
	db      *DB // DB实例
	commits []*Record // 对于写操作生成 Record, 用来做持久化
	undos   []*Record // 对于写操作生成 Undo Record,用于回滚
}

MossDB中一次事务的流程主要包含以下几个步骤:

  1. 首先加锁,保证其数据一致性
  2. 对于写操作,生成Commits和Undo Records,然后写入内存;读操作则直接执行
  3. 提交阶段,将Commits持久化到WAL中;若写入失败,则删除已写入数据;成功则设置数据的其他属性(TTL, Watch等)
  4. 若中间发生错误,执行回滚操作,将Undo Records的记录执行
  5. 事务完成,释放锁

Watch

由于工作中经常使用Kubernetes,对于其Watch接口印象深刻,通过Watch来充当其事件总线,保证其声明式对象的管理。Kubernetes的Watch底层由etcd实现,MossDB也实现了类似的功能。

Watch的定义如下:

type Watcher struct {
	mu       sync.RWMutex // 锁
	watchers map[string]*SubWatcher // watchId与具体Watcher直接的映射
	keys     map[string]containerx.Set[string] // Watch单个key
	ranges   *art.Tree // 前缀Watch
	queue    workqueue.WorkQueue // 工作队列,存放所有事件
	stop     chan struct{} // 是否中止
}

通过工作队列模式,任何写操作都会同步追加到队列中,如果存在单个key的监听者,则通过watchers map获取到对应列表,依次发送事件。对于前缀Watch,我们不可能记录此前缀的所有Key,这里借鉴了etcd,通过RadixTree保存前缀Key,当有新事件时,匹配Key所在的路径,如果有监听者,则进行事件通知。

调用Watch会返回一个Channel,用户只需要监听Channel即可

func Watch(db *mossdb.DB) {
	key := "watch-key"

	ctx, cancel := context.WithTimeout(context.Background(), 1000*time.Millisecond)
	defer cancel()

	// start watch key
	ch := db.Watch(ctx, key)
	log.Printf("start watch key %s", key)

	go func() { // 模拟发送event
		db.Set(key, mossdb.Val("val1"))
		db.Set(key, mossdb.Val("val2"))
		db.Delete(key)

		time.Sleep(100 * time.Second)

		db.Set(key, mossdb.Val("val3"))
	}()

	for {
		select {
		case <-ctx.Done():
			log.Printf("context done")
			return
		case resp, ok := <-ch:
			if !ok {
				log.Printf("watch done")
				return
			}
			log.Printf("receive event: %s, key: %s, new val: %s", resp.Event.Op, resp.Event.Key, resp.Event.Val)
		}
	}

    // Output
    // 2023/02/23 09:48:50 start watch key watch-key
	// 2023/02/23 09:34:19 receive event: MODIFY, key: watch-key, new val: val1
	// 2023/02/23 09:34:19 receive event: MODIFY, key: watch-key, new val: val2
	// 2023/02/23 09:34:19 receive event: DELETE, key: watch-key, new val:
	// 2023/02/23 09:34:19 context done
}

TTL

过期删除再很多场景很有用,比如验证码过期、订单未支付关闭等。MossDB采用时间堆来实现精确的Key过期策略,具体原理可以参考之前的文章Golang分布式应用之定时任务,在查询操作时也会检查Key是否过期,如果过期则直接返回空数据。配合Watch操作可以精确管理数据的生命周期。

总结

至此,MossDB的实现细节已经分析完成,支持了事务、持久化、Watch与过期删除等特性,后续可能会支持HTTP API、存储快照等功能。

所有代码见https://github.com/qingwave/mossdb,欢迎批评指正以及Star。

Explore more in https://qingwave.github.io

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

Golang实现一个事务型内存数据库 的相关文章

随机推荐

  • MCU,MPU,MMU,CACHE的含义

    1 mcu和mpu CPU Central Processing Unit 中央处理器 发展出来三个分枝 一个是DSP Digital Signal Processing Processor 数字信号处理 另外两个是MCU Micro Co
  • HTML <colgroup> 标签

    实例 两个 colgroup 元素为表格中的三列规定了不同的对齐方式和样式 注意第一个 colgroup 元素横跨两列 table width 100 border 1 table
  • [138]小米笔记本怎么关闭secure boot

    关闭Secure Boot的步骤 一 关闭 快速启动 功能 1 右键 开始菜单 电源选项 进入后 点击 选择电源按钮的功能 2 进入电源选项设置后 点击 更改当前不可用的设置 再把 启用快速启动 推荐 前边的勾去掉 若没有该选择则不需要操作
  • MDK与芯片的联系

    程序执行的时候FLASH空间 code RO data 程序执行时SRAM空间 RW data ZI data 程序存储时占用空间 code RO data RW data 在目录下打开命令行窗口 按shift 鼠标右键 gt 可以将信息输
  • 区块链:Solidity值类型(Solidity 枚举Enums & 结构体Structs)

    枚举Enums 案例 pragma solidity 0 4 4 contract test enum ActionChoices GoLeft GoRight GoStraight SitStill ActionChoices choic
  • 华为OD机试 C++ 叠积木

    题目 你手里有一堆砖头 它们都有一样的宽和高 但长度不同 你想用这些砖头堆砌一堵墙 每一层墙可以只用一个砖头 也可以用两个拼接起来 但这两种情况下 每层的长度必须都是一样的 如果你想使用所有的砖头 并堆砌出尽可能多的层数 那么最多可以搭建多
  • C#(winform)调用pytorch模型

    winform调用pytorch上训练好的unet模型 项目是写一个辅助诊断系统软件 用winform写软件 调用pytorch和matlab的模型 这篇博客只包含调用pytorch模型的部分 1 c libtorch 调用模型 2 c 生
  • java使用aspose.pdf或者spire.pdf 将pdf文件转word,实测

    1 aspose pdf aspose pdf不是破解版的只能转3页 所以我弄了个破解版 aspose pdf破解版在网上都有破解方法也比较简单 这里就不说了 直接引入破解版的jar包 在这里我用的是aspose pdf 21 11 jar
  • Qt第四十五章:QComboBox 禁止滚轮

    很简单 直接反射将QComboBox的wheelEvent方法重置掉即可 self combo box QComboBox self setattr self combo box wheelEvent lambda a None
  • 车联网Apollo(阿波罗),研究carlife车机端集成及开发,(WeLink,carplay/carlife)

    Apollo 阿波罗 是携程框架部门研发的分布式配置中心 能够集中化管理应用不同环境 不同集群的配置 配置修改后能够实时推送到应用端 并且具备规范的权限 流程治理等特性 适用于微服务配置管理场景 https github com ctrip
  • C语言提取一列数据并保存

    c语言求教 txt文档只有一列数据但是有很多 需要把它提取出来 每1024个数保存在一个文件中 求大神指教 c语言
  • 什么时候需要使用引用?使用引用的好处是什么?

    作者 谢之易 链接 https www zhihu com question 34267829 answer 58414818 来源 知乎 著作权归作者所有 商业转载请联系作者获得授权 非商业转载请注明出处 记忆里 C 的设计与演化 一书提
  • 【华为机试真题 Python实现】仿 LISP 运算【2022 Q1 Q2

    题目描述 LISP 语言唯一的语法就是括号要配对 形如 OP P1 P2 括号内元素由单个空格分割 其中第一个元素 OP 为操作符 后续元素均为其参数 参数个数取决于操作符类型 注意 参数 P1 P2 也有可能是另外一个嵌套的 OP P1
  • 语音识别-初识

    ASRT https blog ailemon net 2018 08 29 asrt a chinese speech recognition system ASR Automatic Speech Recognition Paddle
  • 计算机加入域的一种方法

    重装系统后 想把机子加入域 却总是不成功 隐约记得以前老大们讲过 厚着脸皮问了 o 之后 决定记下来 省的我以后又忘了 计算机加入域 一 在网络中加入DNS地址 二 step 1 更改计算机名字 右键点击 我的电脑 打开 属性 页面 找到
  • redis-benchmark测试Redis集群性能

    基础环境配置 Redis5 三主三从cluster 1 100个并发连接 100000个请求 检测host为172 16 254 124端口为7004的redis服务器性能 redis benchmark h 172 16 254 124
  • C# 单例模式详解

    定义 单例模式是比较常见的一种设计模式 目的是保证一个类只能有一个实例 而且自行实例化并向整个系统提供这个实例 避免频繁创建对象 节约内存 单例模式的应用场景很多 比如我们电脑的操作系统的回收站就是一个很好的单例模式应用 电脑上的文件 视频
  • 2023年大唐杯仿真部分-5G信令流程仿真实验

    参考视频连接 第十届大唐杯信令流程仿真讲解 哔哩哔哩 bilibili 1 5G系统消息的获取 根据题目要求 UE开机需要获取消息 消息分别是MIB SIB1 SIB2 SIB3 上面为什么选的是SIB1 Systeminformation
  • 如何判断一个指定的经纬度点是否落在一个多边形内

    1 理论支持 如果从需要判断的点出发的一条射线与该多边形的焦点个数为奇数 则该点在此多边形内 否则该点在此多边形外 射线不能与多边形顶点相交 2 编程思路 该程序的思路是从A点出发向左做一条水平射线 平行于x轴 向X轴的反方向 判断与各边是
  • Golang实现一个事务型内存数据库

    内存数据库经我们经常用到 例如Redis 那么如何从零实现一个内存数据库呢 本文旨在介绍如何使用Golang编写一个KV内存数据库MossDB 特性 MossDB是一个纯Golang编写 可嵌入的 键值型内存数据库 包含以下特性 可持久化