我的编程空间,编程开发者的网络收藏夹
学习永远不晚

Golang实现事务型内存数据库的方法详解

短信预约 -IT技能 免费直播动态提醒
省份

北京

  • 北京
  • 上海
  • 天津
  • 重庆
  • 河北
  • 山东
  • 辽宁
  • 黑龙江
  • 吉林
  • 甘肃
  • 青海
  • 河南
  • 江苏
  • 湖北
  • 湖南
  • 江西
  • 浙江
  • 广东
  • 云南
  • 福建
  • 海南
  • 山西
  • 四川
  • 陕西
  • 贵州
  • 安徽
  • 广西
  • 内蒙
  • 西藏
  • 新疆
  • 宁夏
  • 兵团
手机号立即预约

请填写图片验证码后获取短信验证码

看不清楚,换张图片

免费获取短信验证码

Golang实现事务型内存数据库的方法详解

内存数据库经我们经常用到,例如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目前也存在不少优秀的开源内存数据库,比如buntdb、go-memdb,在编写MossDB过程中也借鉴了一些它们的特性。

MossDB的架构如图:

自上往下分为:

  • 接口层,提供API接受用户请求
  • 核心层,实现事务、过期删除、Watch等功能
  • 存储层,提供KV的后端存储以及增删改查
  • 持久化层,使用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中一次事务的流程主要包含以下几个步骤:

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

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。

以上就是Golang实现事务型内存数据库的方法详解的详细内容,更多关于Golang事务型内存数据库的资料请关注编程网其它相关文章!

免责声明:

① 本站未注明“稿件来源”的信息均来自网络整理。其文字、图片和音视频稿件的所属权归原作者所有。本站收集整理出于非商业性的教育和科研之目的,并不意味着本站赞同其观点或证实其内容的真实性。仅作为临时的测试数据,供内部测试之用。本站并未授权任何人以任何方式主动获取本站任何信息。

② 本站未注明“稿件来源”的临时测试数据将在测试完成后最终做删除处理。有问题或投稿请发送至: 邮箱/279061341@qq.com QQ/279061341

Golang实现事务型内存数据库的方法详解

下载Word文档到电脑,方便收藏和打印~

下载Word文档

猜你喜欢

Golang实现事务型内存数据库的方法详解

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

Golang如何实现事务型内存数据库

这篇文章主要讲解了“Golang如何实现事务型内存数据库”,文中的讲解内容简单清晰,易于学习与理解,下面请大家跟着小编的思路慢慢深入,一起来研究和学习“Golang如何实现事务型内存数据库”吧!特性MossDB是一个纯Golang编写、可嵌
2023-07-05

Golang使用ttl机制保存内存数据方法详解

ttl(time-to-live)数据存活时间,我们这里指数据在内存中保存一段时间,超过期限则不能被读取到,与Redis的ttl机制类似。本文仅实现ttl部分,不考虑序列化和反序列化
2023-03-08

Qt实现编辑数据库数据的方法详解

这篇文章主要为大家详细介绍了Qt是如何实现编辑数据库数据的,文中的示例代码简洁易懂,对我们深入了解QT有一定的帮助,感兴趣的小伙伴可以了解一下
2023-02-03

Android 实现永久保存数据的方法详解

背景:在Android中按照数据保存的方式,可以分为如下几种Content Provider (用的SQLite实现),SQLite,SharedPreferences(用的XML实现),文件,网络等方式。今天在项目中,需要做一个永久保存的
2022-06-06

JavaScript数据类型检测实现方法详解

Javascript中检查数据类型一直是老生常谈的问题,类型判断在web开发中也有着非常广泛的应用,所以下面这篇文章主要给大家介绍了关于JS数据类型检测的那些事,需要的朋友可以参考下
2022-11-13

Android实现内存中数据保存到sdcard的方法

本文实例讲述了Android实现内存中数据保存到sdcard的方法。分享给大家供大家参考,具体如下:public static void writeToSdCard(String s) {try {File dst = new File("
2022-06-06

详解JAVA生成将图片存入数据库的sql语句实现方法

详解JAVA生成将图片存入数据库的sql语句实现方法实现代码:注释很清楚,不在重述~public class Image2Hex { public static void main(String[] args) { try{
2023-05-31

Golang实现CronJob(定时任务)的方法详解

这篇文章主要为大家详细介绍了Golang如何通过一个单pod去实现一个常驻服务,去跑定时任务(CronJob),文中的示例代码讲解详细,需要的可以参考下
2023-05-14

实现.Net7下数据库定时检查的方法详解

在软件开发过程中,有时候我们需要定时地检查数据库中的数据,并在发现新增数据时触发一个动作。为了实现这个需求,本文我们在 .Net 7 下进行一次简单的演示。感兴趣的可以了解一下
2022-12-20

MySQL关系型数据库事务的ACID特性与实现

这篇文章主要介绍了MySQL关系型数据库事务的ACID特性与实现,ACID 是为保证事务transaction是正确可靠的,具备原子性、一致性、隔离性、持久性等特性
2022-11-13

Golang实现IP地址转整数的方法详解

本文介绍了Go语言中将IP地址转换为整数的方法,包括使用net.ParseIP和net.IPv4函数解析IP地址,以及使用ToInt方法将其转换为整数。它强调了net.ParseIP可解析IPv4和IPv6地址,而net.IPv4专用于IPv4地址。最后,文章提醒读者注意转换后的整数表示IP地址的二进制表示,并提供了一个将"192.168.1.1"转换为整数的示例。
Golang实现IP地址转整数的方法详解
2024-04-02

vuex存储数据的几种方法实例详解

在浏览网页时我们有些时候需要记住一些用户选择的信息,比如登陆时我们如果选择了记住密码,那么我们下次进入该网页时就会有你上次的登陆信息,下面这篇文章主要给大家介绍了关于vuex存储数据的几种方法,需要的朋友可以参考下
2022-11-13

编程热搜

  • Python 学习之路 - Python
    一、安装Python34Windows在Python官网(https://www.python.org/downloads/)下载安装包并安装。Python的默认安装路径是:C:\Python34配置环境变量:【右键计算机】--》【属性】-
    Python 学习之路 - Python
  • chatgpt的中文全称是什么
    chatgpt的中文全称是生成型预训练变换模型。ChatGPT是什么ChatGPT是美国人工智能研究实验室OpenAI开发的一种全新聊天机器人模型,它能够通过学习和理解人类的语言来进行对话,还能根据聊天的上下文进行互动,并协助人类完成一系列
    chatgpt的中文全称是什么
  • C/C++中extern函数使用详解
  • C/C++可变参数的使用
    可变参数的使用方法远远不止以下几种,不过在C,C++中使用可变参数时要小心,在使用printf()等函数时传入的参数个数一定不能比前面的格式化字符串中的’%’符号个数少,否则会产生访问越界,运气不好的话还会导致程序崩溃
    C/C++可变参数的使用
  • css样式文件该放在哪里
  • php中数组下标必须是连续的吗
  • Python 3 教程
    Python 3 教程 Python 的 3.0 版本,常被称为 Python 3000,或简称 Py3k。相对于 Python 的早期版本,这是一个较大的升级。为了不带入过多的累赘,Python 3.0 在设计的时候没有考虑向下兼容。 Python
    Python 3 教程
  • Python pip包管理
    一、前言    在Python中, 安装第三方模块是通过 setuptools 这个工具完成的。 Python有两个封装了 setuptools的包管理工具: easy_install  和  pip , 目前官方推荐使用 pip。    
    Python pip包管理
  • ubuntu如何重新编译内核
  • 改善Java代码之慎用java动态编译

目录