【TinyKV-2022】Project1:Standalone KV

文章讲述了TinyKV项目中如何实现一个单机存储引擎,使用BadgerDB作为底层存储,并介绍了BadgerDB的LSM-Tree结构和WiscKey设计。项目要求实现ColumnFamily(CF)功能,通过在Key前添加前缀模拟多列存储。文章详细说明了如何初始化存储引擎,实现Write和Reader方法,以及封装K-V服务处理器以提供读写API。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

Project1

Project1 没有设计到分布式,其要求我们实现 单机 的存储引擎,并保证其支持 CF。不同于 TiKV 使用的 RocksDB,TinyKV 采用的底层存储为 BadgerDB,虽然同样是基于 LSM 的,但后者不支持 CF,故这里需要自己实现。所谓 CF,就是把同类型的值归结在一起,这在多列存储(比如 MYSQL)中很显然,一列就是一个 CF。但是 KV 是单列存储的,因为为了实现 CF,必须通过单列来模拟多列,方法很简单,就是在 Key 前面拼上 CF 前缀。如下图:

在这里插入图片描述

也就是说,只需要给每一个 Key 加上对应的前缀,就能模拟出多列的效果,从而实现 CF。在 TinyKV 中,一共只有三个 CF,分别为 Default、Lock、Write,三个 CF 用于实现 Project4 的 2PC,这里暂时不用理解。前缀拼接无需自己实现,直接调用项目给的 KeyWithCF 即可,其余和 CF 有关的方法也都在 engine_util 中已经提供好了。

Badger

在开始编写代码之前,可以先了解一下 Badger。Badger 采用的也是 LSM-Tree 结构。但不同于传统的存储结果,它参考了 WiscKey 的设计,仅仅将 <Key - ValueAddr> 存入 LSM-Tree 中,而 Value 则无序的存放在磁盘中。由于 <Key - ValueAddr> 是很小的,所以这一部分数据完全可以放在内存中,以至于读取 Key 是根本无需访问磁盘,读取某一个 Key 的 Value 时也仅仅需要访问一次磁盘。WicsKey 的设计如下图所示:

在这里插入图片描述

Value Log 是存放在磁盘中的,且没有进行排序,即直接 append,这样的化就避免了写放大的问题,因此写的时候比传统的 LSM 结构要更快。但是这么做是有代价的,那就是 scan。因为 Value Log 是无序的,因此当 scan 某一个范围的数据时,会涉及大量的随机读,影响性能。

Project1A

Implement a standalone storage engine.

在这一节中,需要完善kv/storage/standalone_storage/standalone_storage.go 的代码,实现 storage engine 的初始化。在函数 NewStandAloneStorage 中,我们需要初始化一个包含 engine 的 storage。先生成 KV 路径和 Raft 路径,二者的基目录 dbPath 直接从 conf 中取即可。然后调用两次 CreateDB,因为其中已经封装了 MkdirAll ,故不需要另行创建目录。CreateDB 执行完后,会返回 *DB,接着用 NewEngines 生成引擎即可。

store := StandAloneStorage{
    engine: engine_util.NewEngines(kvEngine,raftEngine,kvPath,raftPath),
    config: conf,
}
return &store

Storage 是 badger K-V 存储的一层封装,其中含有读写方法,分别为 Write 和 Reader。不一样的是,Write 会直接将传入的操作写入,而 Reader 需要先返回一个 StorageReader,然后通过 StorageReader 执行读操作,而不是直接读。

type Storage interface {
    // Other stuffs
    Write(ctx *kvrpcpb.Context, batch []Modify) error
    Reader(ctx *kvrpcpb.Context) (StorageReader, error)
}

首先我们实现 Reader 方法,该方法返回一个 StoreageReader 接口,而这个接口是需要我们手动实现的。该接口有三个方法:GetCFIterCFClose。作用分别为:

  • 从 engine 中获取 CF_Key 对应的 value;
  • 获取对应 CF 的迭代器;
  • 关闭该 Reader;

为了实现StoreageReader,这里需要定义一个含有字段 txn 的结构,我将其命名为 StandAloneReader,接着让其实现上述三个方法即可。前两者在 engine_util 中已经实现,直接封装就行,Close 直接调用 txn 的 DisCard 即可。三者的代码如下:

func (s *StandAloneReader) GetCF(cf string, key []byte) ([]byte, error){
	value, err := engine_util.GetCFFromTxn(s.kvTxn,cf,key)
	if err == badger.ErrKeyNotFound {
		return nil, nil
	}
	return value,err
}

func (s *StandAloneReader) IterCF(cf string) engine_util.DBIterator{
	return engine_util.NewCFIterator(cf,s.kvTxn)
}

func (s *StandAloneReader) Close() {
	s.kvTxn.Discard()
	return
}

Reader 实现完毕后,需要实现 Write 方法。该方法不同于 Reader 返回一个接口,而是直接执行写操作。需要注意的是,写操作都是 批处理,每一个写操作都依赖于结构 Modify,类型分为 Put 和 Delete。每次执行都是批量执行,依赖于切片 batch。因此,Write 以 batch 为核心入参,遍历其中的每一个 Modify,判断其是 Put 还是 Delete,随后通过 engine_util 中的 API 进行写入即可。核心代码如下:

	for _,b := range batch {
		switch b.Data.(type) {
		case storage.Put:
			put := b.Data.(storage.Put)
			key := put.Key
			value := put.Value
			cf := put.Cf
			err := engine_util.PutCF(s.engine.Kv,cf,key,value)
			if err != nil{
				return err
			}
			break
		case storage.Delete:
			del := b.Data.(storage.Delete)
			key := del.Key
			cf := del.Cf
			err := engine_util.DeleteCF(s.engine.Kv,cf,key)
			if err != nil{
				return nil
			}
			break
		}
	}

Project1B

Implement raw key-value service handlers.

上一节实现了单机的 storage engine,在该节中我们需要对其进行一层封装,实现一个 K-V service handler,让读写操作以 API 的方式暴露出去供上层使用。这里要完善代码 kv/server/raw_api.go,其中包括 RawGetRawScanRawPutRawDelete。四者作用分别为:

  • 获取对应 CF_Key 的 Value;
  • 在指定的 CF 中,从 StartKey 开始扫描 KvPair,上限为 Limit;
  • 写入一个 KvPair;
  • 删除一个 KvPari;

首先是 RawGetRawScan,二者都是读操作,通过 storage 的 Reader 来执行读。首先,通过 server.storage.Reader() 获取到 reader。如果是 RawGet,那么通过 reader.GetCF() 直接获取到值,然后包装成 resp 返回回去。如果是 RawScan,那么通过 reader.RawScan() 先获取到迭代器,接着按照 StartKey 开始遍历,将遍历到的 K-V 整合起来即可,遍历时要注意 Limit 限制,整合完成后依然包装成 resp 返回回去。需要注意的是,如果 RawGet 没有获取到,那么返回时要标记 resp.NotFound = true

RawPutRawDelete 两者实现非常相似,都是将请求包装成 Modify 的 batch,然后通过 server.storage.Write() 执行写操作,最后把执行的结果包装成 resp 返回回去即可。二者的 Modify 有所不同,如下:

// Put
put := storage.Put{
    Key: req.Key,
    Value: req.Value,
    Cf: req.Cf,
}
modify := storage.Modify{
    Data: put,
}
// Delete
del := storage.Delete{
    Key: req.Key,
    Cf: req.Cf,
}
modify := storage.Modify{
    Data: del,
}
### 关于 Maven 版本不兼容问题 当遇到 `maven-archetype-plugin` 插件版本与当前使用的 Maven 版本不兼容的情况时,通常是因为插件本身依赖更高版本的 Maven 来运行。以下是针对此问题的具体解决方案: #### 方法一:升级 Maven 到所需版本 如果提示需要 Maven 3.6.3 或以上版本,则可以考虑将现有的 Maven 升级到满足需求的版本。可以通过以下方式获取最新版或指定版本的 Maven: 1. 访问 Apache 官方网站 [https://maven.apache.org/download.cgi](https://maven.apache.org/download.cgi),下载对应版本的安装包。 2. 下载完成后解压至目标目录,并更新环境变量中的 MAVEN_HOME 和 PATH 配置以指向新版本。 通过上述操作可确保本地环境中使用的是符合插件最低要求的 Maven 版本[^1]。 #### 方法二:修改 pom.xml 文件中的插件声明 另一种解决办法是在项目的 `pom.xml` 中显式定义所需的插件及其版本号。例如,在 `<build>` 节点下加入如下片段来锁定特定版本的插件: ```xml <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-archetype-plugin</artifactId> <version>3.1.1</version> <!-- 使用较低版本 --> </plugin> ``` 这样做的好处是可以避免因默认采用较新的插件而导致不必要的麻烦,同时也能够继续沿用现有 Maven 版本来完成开发工作流[^4]。 #### 方法三:清理并重置本地仓库缓存 有时由于历史遗留数据或其他因素造成的问题也可能引发类似的错误消息。因此建议按照以下步骤执行清理动作后再尝试重新加载项目: 1. 找到本地 Maven 缓存路径,默认情况下位于用户家目录下的 `.m2/repository/` 文件夹; 2. 导航进入 `org/apache/maven/plugins/maven-archetype-plugin/` 子目录并将其中的内容移除掉; 3. 返回 IDE 并刷新或者重建整个工程结构以便触发远程镜像同步过程从而获得最新的依赖项副本[^3]。 --- ### 注意事项 需要注意的是,虽然强制降级插件可能暂时缓解了眼前困境,但从长远来看并不推荐这样做,因为低版本可能存在安全漏洞或者其他功能性缺陷等问题。所以最好还是尽快安排时间去完成必要的基础软件栈升级计划。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值