[toc]
rosedb 是一个简单、内嵌的 k-v 数据库,使用 Golang
实现,支持多种数据结构,包含 String
、List
、Hash
、Set
、Sorted Set
,接口名称风格和 Redis 类似,如果你对 redis 比较熟悉,那么使用起来会毫无违和感。
大概半年前(2020 年中),我刚开始学习 Go 语言,由于之前有 Java 语言的经验,加上 Go 的基本语法较简单,上手还是很快,但是学完基础的语法知识之后,就不知道下一步应该做什么了。
一个偶然的机会,我在网上看到了一篇介绍数据库模型的文章,文章很简单,理解起来也很容易,加上我对于数据库还是比较感兴趣的,因此想着可以自己实现一个,造个轮子来玩玩,借此巩固自己的一些基础知识。
因此这个项目也是学习并巩固 Go 相关知识的不错的素材,通过实践这个项目,你至少可以学到:
- Golang 大多数基础语法,以及一些高级特性
- 数据结构及算法相关知识,链表,哈希表,跳表等
- 操作系统的一些知识,特别是对文件系统,内存映射相关的
由于个人能力有限,因此欢迎大家提 Issue 和 Pr,一起完善这个项目。
一个 rosedb 实例,其实就是系统上的一个文件夹,在这个文件夹中,除了一些配置外,最主要的便是数据文件。一个实例中,只会存在一个活跃的数据文件进行写操作,如果这个文件的大小达到了设置的上限,那么这个文件会被关闭,然后创建一个新的活跃文件。
其余的文件,我称之为已归档文件,这些文件都是已经被关闭,不能在上面进行写操作,但是可以进行读操作。
所以整个数据库实例就是当前活跃文件、已归档文件、其他配置的一个集合:
在每一个文件中,写数据的操作只会追加到文件的末尾,这保证了写操作不会进行额外的磁盘寻址。写入的数据是以一个个被称为 Entry 的结构组织起来的,Entry 的主要数据结构如下:
因此一个数据文件可以看做是多个 Entry 的集合:
当写入数据时,如果是 String 类型,为了支持 string 类型的 key 前缀扫描和范围扫描,我将 key 存放到了跳表中,如果是其他类型的数据,则直接存放至对应的数据结构中。然后将 key、value 等信息,封装成 Entry 持久化到数据文件中。
如果是删除操作,那么也会被封装成一个 Entry,标记其是一个删除操作,然后持久化到数据文件中,这样的话就会带来一个问题,数据文件中可能会存在大量的冗余数据,造成不必要的磁盘空间浪费。为了解决这个问题,我写了一个 reclaim 方法,你可以将其理解为对数据文件进行重新整理,使其变得更加的紧凑。
reclaim 方法的执行流程也比较的简单,首先建立一个临时的文件夹,用于存放临时数据文件。然后遍历整个数据库实例中的所有已归档文件,依次遍历数据文件中的每个 Entry,将有效的 Entry 写到新的临时数据文件中,最后将临时文件拷贝为新的数据文件,原数据文件则删除。
这样便使得数据文件的内容更加紧凑,并且去除了无用的 Entry,避免占据额外的磁盘空间。
项目基于 Go 1.14.4 开发,首先需要确保安装了 Golang 环境,安装请参考 Golang 官网。
使用 go get github.com/roseduan/rosedb
安装,然后在你的项目中 import 即可:
import (
github.com/roseduan/rosedb
)
初始化默认配置数据库:
package main
import (
"github.com/roseduan/rosedb"
"log"
)
func main() {
config := rosedb.DefaultConfig()
db, err := rosedb.Open(config)
if err != nil {
log.Fatal(err)
}
defer db.Close()
//...
}
可配置的选项如下:
type Config struct {
DirPath string `json:"dir_path"`//数据库数据存储目录
BlockSize int64 `json:"block_size"`//每个数据块文件的大小
RwMethod storage.FileRWMethod `json:"rw_method"`//数据读写模式
IdxMode DataIndexMode `json:"idx_mode"`//数据索引模式
MaxKeySize uint32 `json:"max_key_size"`//key的最大size
MaxValueSize uint32 `json:"max_value_size"`//value的最大size
Sync bool `json:"sync"`//每次写数据是否持久化
ReclaimThreshold int `json:"reclaim_threshold"`//回收磁盘空间的阈值
}
默认配置如下:
func DefaultConfig() Config {
return Config{
DirPath: os.TempDir(),//操作系统临时目录,这个配置最好自定义
BlockSize: DefaultBlockSize,//16MB
RwMethod: storage.FileIO,
IdxMode: KeyValueRamMode,
MaxKeySize: DefaultMaxKeySize,//128字节
MaxValueSize: DefaultMaxValueSize,//1MB
Sync: false,
ReclaimThreshold: DefaultReclaimThreshold,//4 当已封存文件个数到达 4 时,可进行回收
}
}
设置普通的键值对数据,如果 key 已经存在,则覆盖原来的 value。
err := db.Set([]byte("test_key"), []byte("I am roseduan"))
if err != nil {
log.Fatal("write data error ", err)
}
如果对应的 key 不存在才添加,否则不做任何操作。
获取 key 对应的 value
_ = db.Set([]byte("test_key"), []byte("test_value"))
_ =db.SetNx([]byte("test_key"), []byte("value_001"))
_ =db.SetNx([]byte("test_key_new"), []byte("value_002"))
val1, _ := db.Get([]byte("test_key"))
val2, _ := db.Get([]byte("test_key_new"))
将键 key 的值设为 value , 并返回键 key 在被设置之前的旧值。
val, err := db.GetSet([]byte("test_key001"), []byte("test_new_val_001"))
if err != nil {
log.Fatal(err)
}
t.Log("original val : ", string(val))
val, _ = db.Get([]byte("test_key001"))
t.Log("new val : ", string(val))
如果 key 存在,则将 value 追加至原来的 value 末尾,否则相当于 Set 方法。
_ = db.Set([]byte("my_name"), []byte("roseduan"))
_ = db.Append([]byte("my_name"), []byte(" append some val"))
val, _ := db.Get([]byte("my_name"))
返回 key 存储的字符串值的长度。
判断 key 是否存在。
删除 key 及其数据。
_ = db.Set([]byte("my_name"), []byte("roseduan"))
_ = db.StrRem([]byte("my_name"))
val, _ := db.Get([]byte("my_name")) //val == nil
根据前缀查找所有匹配的 key 对应的 value。
db.Set([]byte("ac"), []byte("3"))
db.Set([]byte("aa"), []byte("1"))
db.Set([]byte("ae"), []byte("4"))
db.Set([]byte("ar"), []byte("6"))
db.Set([]byte("ba"), []byte("7"))
db.Set([]byte("ab"), []byte("2"))
db.Set([]byte("af"), []byte("5"))
findPrefix := func(limit, offset int) {
values, err := db.PrefixScan("a", limit, offset)
if err != nil {
log.Fatal(err)
}
if len(values) > 0 {
for _, v := range values {
t.Log(string(v))
}
}
}
//findPrefix(-1, 0)
//findPrefix(2, 0)
//findPrefix(2, 2)
//findPrefix(1, 3)
findPrefix(1, 20)
范围扫描,查找 key 是从 start 到 end 之间的数据。
_ = db.Set([]byte("100054"), []byte("ddfd"))
_ = db.Set([]byte("100009"), []byte("dfad"))
_ = db.Set([]byte("100007"), []byte("rrwe"))
_ = db.Set([]byte("100011"), []byte("eeda"))
_ = db.Set([]byte("100023"), []byte("ghtr"))
_ = db.Set([]byte("100056"), []byte("yhtb"))
val, err := db.RangeScan([]byte("100007"), []byte("100030"))
if err != nil {
log.Fatal(err)
}
if len(val) > 0 {
for _, v := range val {
t.Log(string(v))
}
}
在列表的头部添加元素,返回添加后的列表长度。
在列表的尾部添加元素,返回添加后的列表长度。
取出列表头部的元素。
取出列表尾部的元素。
key := []byte("my_list")
db.LPush(key, []byte("list_data_001"), []byte("list_data_002"))
db.RPush(key, []byte("list_data_003"), []byte("list_data_004"))
val1, _ := db.LPop(key)
t.Log(string(val1))
val2, _ := db.RPop(key)
t.Log(string(val2))
将值 val 插入到列表 key 当中,位于值 pivot 之前或之后。
key := []byte("my_list")
db.LInsert(string(key), 0, []byte("new val"), []byte("list_data_003"))
将列表 key 下标为 index 的元素的值设置为 val。
key := []byte("my_list")
ok, err := db.LSet(key, 0, []byte("new val"))
移除列表中与参数 value 相等的元素。
key := []byte("my_list")
printAll := func() {
vals, _ := db.LRange(key, 0, -1)
for _, v := range vals {
t.Logf("%s ", string(v))
}
t.Log()
}
db.LPush(key, []byte("11"), []byte("12"), []byte("23"), []byte("11"))
printAll()
db.LRem(key, []byte("11"), 0)
printAll()
对一个列表进行修剪(trim),让列表只保留指定区间内的元素,不在指定区间之内的元素都将被删除。
db.LTrim(key, 3, 5)
返回列表 key 中指定区间内的元素,区间以偏移量 start 和 end 指定。
将哈希表 hash 中域 field 的值设置为 value。
当且仅当域 field 尚未存在于哈希表的情况下, 将它的值设置为 value。
返回哈希表中给定域的值。
返回哈希表 key 中,所有的域和值。
返回哈希表 key 中域的数量。
key := []byte("my_set")
db.HSet(key, []byte("name"), []byte("roseduan"))
db.HSet(key, []byte("age"), []byte("24"))
db.HSet(key, []byte("hobbies"), []byte("coding writing football"))
db.HSet(key, []byte("dream"), []byte("be better"))
db.HSetNx(key, []byte("dream"), []byte("dream at day"))
db.HSetNx(key, []byte("height"), []byte("1.75"))
dream := db.HGet(key, []byte("dream"))
t.Log("my dream is ", string(dream))
l := db.HLen(key)
t.Log(l)
all := db.HGetAll(key)
for _, v := range all {
t.Log(string(v))
}
检查给定域 field 是否存在于 key 对应的哈希表中。
返回哈希表 key 中的所有域。
返回哈希表 key 中的所有域对应的值。
key := []byte("my_set")
keys := db.HKeys(key)
values := db.HValues(key)
添加元素,返回添加后的集合中的元素个数。
随机移除并返回集合中的 count 个元素。
判断 member 元素是不是集合 key 的成员。
key := []byte("my_set")
db.SAdd(key, []byte("set_data_001"), []byte("set_data_002"), []byte("set_data_003"))
values, _ := db.SPop(key, 2)
t.Log(len(values))
for _, v := range values {
t.Log(string(v))
}
ok := db.SIsMember(key, []byte("set_data_001"))
ok = db.SIsMember(key, []byte("set_data_003"))
t.Log(ok)
从集合中返回随机元素。
key := []byte("my_set")
members := db.SRandMember(key, 5)
for _, m := range members {
t.Log(string(m))
}
移除集合 key 中的一个或多个 member 元素,不存在的 member 元素会被忽略。
- 支持 TTL
- 支持事务,ACID 特性
- 文件数据压缩存储
- String 类型 key 加入前缀扫描
- 缓存淘汰策略
- 写一个简单的客户端,支持命令行操作
- 完善相关文档
rosedb 根据 MIT License 许可证授权,有关完整许可证文本,请参阅 LICENSE。