6.824做完了,代码写的乱糟糟,想着重写一遍,整理下思路,后来发现了tinykv,相比于6.824还多了个事务,就准备把tinykv也做一下。
文档翻译
在本项目中,实现一个单机的、支持Column Family的KV存储服务。Column Family表示Key的命名空间,不同Column Family间可以有相同的Key存在。
服务提供Put/Delete/Get/Scan四种基本操作。
本项目可以拆解为两步去实现:
- 实现单机的存储引擎。
- 实现原生的服务接口。
tinykvpb.proto和kvrpcpb.proto定义了rpc接口和请求响应消息,kv/main.go中注册了RPC服务。
proto文件由protocol-buffer生成,不需要修改。
Server底层由Storage抽象类支持,需要为StandAloneStorage实现Storage的所有接口。
type Storage interface {
// Other stuffs
Write(ctx *kvrpcpb.Context, batch []Modify) error
Reader(ctx *kvrpcpb.Context) (StorageReader, error)
}
复制代码
Storage底层由badger(类似LevelDB或RocksDB的存储引擎)支持,StandAloneStorage即badger的简单封装。
目前不需要考虑kvrpcpb.Context的涵义。
一些提示。
- 使用badger.Txn来实现Reader()函数,即badger提供的事务支持。
- badger不支持Column Family,engine_util提供了一系列函数,利用前缀来实现Column Family,使用它们来实现Write()函数。
- 使用Connor1996/badger而不是dgraph-io/badger。
- 使用Discard()关闭badger.Txn,这之前需要关闭所有迭代器。
最后就是基于Storage实现RawGet/RawScan/RawPut/RawDelete,完成后通过make project1进行测试。
StandAloneStorage
engine_util封装了badger的接口,StandAloneStorage就是要在engine_util的基础上再封装一层。因此StandAloneStorage结构也很简单。
type StandAloneStorage struct {
engine *engine_util.Engines
}
复制代码
StandAloneStorage为抽象类Storage的具体实现,因此需要实现Write和Reader两个函数,函数签名如下。
func (s *StandAloneStorage) Write(ctx *kvrpcpb.Context, batch []storage.Modify) error
func (s *StandAloneStorage) Reader(ctx *kvrpcpb.Context) (storage.StorageReader, error)
复制代码
Project1中,还用不到kvrpcpb.Context,storage.Modify对应Put或Delete两个写操作,storage.StorageReader同样是一个抽象类。
Reader
type StorageReader interface {
GetCF(cf string, key []byte) ([]byte, error)
IterCF(cf string) engine_util.DBIterator
Close()
}
复制代码
可以看到StorageReader的两个函数,屏蔽了事务,简化了接口。实现StorageReader需要用到engine_util提供的GetCFFromTxn和NewCFIterator两个函数。
func GetCFFromTxn(txn *badger.Txn, cf string, key []byte) (val []byte, err error)
func NewCFIterator(cf string, txn *badger.Txn) *BadgerIterator
复制代码
获取badger.Txn的函数engine_util并未给出,需要直接调用badger.DB.NewTransaction函数。
func (db *DB) NewTransaction(update bool) *Txn
复制代码
update为真表示Put/Delete两个写操作,为假表示Get/Scan两个读操作。
Write
type Modify struct {
Data interface{}
}
type Put struct {
Key []byte
Value []byte
Cf string
}
type Delete struct {
Key []byte
Cf string
}
复制代码
Modify表示一个Put/Delete操作,Write中通过断言确定是Put还是Delete,进而调用engine_util提供的PutCF和DeleteCF两个函数。
func PutCF(engine *badger.DB, cf string, key []byte, val []byte) error
func DeleteCF(engine *badger.DB, cf string, key []byte) error
复制代码
其实这两个函数内部实现依旧是用了事务的,相比于Reader,Write对事务的屏蔽并没有让我们自己实现。
Server
StandAloneStorage是Storage的实现,Server还要在他之上,后续还会有RaftStorage,这样Server底层的存储引擎就是可以更换的,分别对应单机的和分布式的存储服务。
type Server struct {
storage storage.Storage
}
复制代码
Project1要求我们为Server实现原生的服务接口,这部分在raw_api.go中,需要实现的函数的签名如下。
func (server *Server) RawGet(_ context.Context, req *kvrpcpb.RawGetRequest) (*kvrpcpb.RawGetResponse, error)
func (server *Server) RawPut(_ context.Context, req *kvrpcpb.RawPutRequest) (*kvrpcpb.RawPutResponse, error)
func (server *Server) RawDelete(_ context.Context, req *kvrpcpb.RawDeleteRequest) (*kvrpcpb.RawDeleteResponse, error)
func (server *Server) RawScan(_ context.Context, req *kvrpcpb.RawScanRequest) (*kvrpcpb.RawScanResponse, error)
复制代码
实现这四个接口,需要调用之前实现的Write、Reader.GetCF、Reader.IterCF这三个函数,一个可能不太理解的是Scan函数,因为badgerDB内部是有序存储的,所以可以根据一个Key,向后获取N个Key,RawScanRequest.Limit就是N的涵义。
engine_util封装了DBIterator,提供了Item、Valid、Next、Seek用于实现RawScan函数。
Column Family
可能你还没理解CF是什么,其实本质就是字符串而已,用作Key的前缀,起到命名空间的作用。
const (
CfDefault string = "default"
CfWrite string = "write"
CfLock string = "lock"
)
func KeyWithCF(cf string, key []byte) []byte {
return append([]byte(cf+"_"), key...)
}
复制代码
例如默认的CF为"default",那么该CF下的名为"apple"的Key,实际存储为"default_apple"这个字符串。