tinykv 项目地址:https://github.com/talent-plan/tinykv
本博客提供一个参考思路,未必是正确答案(但能够通过测试集),请注意甄别。欢迎在评论区讨论。
文章目录
Project1 的目标是建立一个单独的结点,称为 StandAloneStorage;并且在这个结点上实现 gRPC 服务,即提供数据库操作的 API 以供调用。据此也可以看出,任务分为两层。
需要修改的文件:
kv/storage/standalone_storage/standalone_storage.go
kv/server/raw_api.go
建立 StandAloneStorage
实现 StandAloneStorage 的数据和方法
我对于StandAloneStorage
的结构体的实现如下:
// StandAloneStorage is an implementation of `Storage` for a single-node TinyKV instance. It does not
// communicate with other nodes and all data is stored locally.
type StandAloneStorage struct {
// Your Data Here (1).
running bool // if this storage is running
db *badger.DB
readers []*StandAloneReader
}
其中running
标识这个结点是否已启动。db
是这个结点所基于的badger.DB
数据库对象。后面基于这个数据库,可能会产生很多StandAloneReader
对象,所以用一个readers
存起来,以便后面把这些都关掉。
创建一个StandAloneStorage
很简单,实际上只需要为新的StandAloneStorage
对象分配一个数据库即可。engine_util.CreateDB
接收所创建数据库的路径,以及这个结点是否是Raft
(集群中的结点,Project1 是独孤结点)。
func NewStandAloneStorage(conf *config.Config) *StandAloneStorage {
// Your Code Here (1).
return &StandAloneStorage{
running: false,
db: engine_util.CreateDB("StandAloneKv", false),
}
}
开启一个StandAloneStorage
的函数如下。
func (s *StandAloneStorage) Start() error {
// Your Code Here (1).
if s.running {
return errors.New("cannot start")
}
s.running = true
return nil
}
关闭一个StandAloneStorage
的函数如下。可以看到,在数据库被关闭之前,它所有的StandAloneReader
对象也都被关闭。最后这个os.RemoveAll
比较重要,正常来讲我们关闭一个数据库,不应该删除其存储的所有内容。但是鉴于这个StandAloneStorage
只是用于测试,没有具体指定数据库要创建在哪个路径上("StandAloneKv"
的名字是我自定义的),而且只会经历一次开启-关闭过程,随即抛弃。如果不把这个路径下数据库的内容删除,那么测试用例之间会互相干扰。
func (s *StandAloneStorage) Stop() error {
// Your Code Here (1).
if !s.running {
return errors.New("cannot stop")
}
s.running = false
// Close all readers
for _, reader := range s.readers {
reader.Close()
}
s.db.Close()
// Destroy the StandAloneKv database
os.RemoveAll("StandAloneKv")
return nil
}
然后完成一下对于结点的写操作。代码如下。函数参数batch
是我们需要对数据库进行的更改,这种更改包括两种:
storage.Put
:往数据库中增加键值对。Storage.Delete
:删除数据库中已有的键值对。
所有的修改操作都被包装在一个匿名函数里面,然后将这个匿名函数传给Update
函数,造成数据库的更改。实际上Update
的实现逻辑应该是,创建了一个新的事务(即badger.Txn
),然后使用匿名函数载入这个事务的内容,随后更新数据库。
func (s *StandAloneStorage) Write(ctx *kvrpcpb.Context, batch []storage.Modify) error {
// Your Code Here (1).
if !s.running {
return errors.New("cannot write on storage that is not running")
}
var err error
for _, abatch := range batch {
switch abatch.Data.(type) {
case storage.Put:
err = s.db.Update(func(txn *badger.Txn) error {
return txn.Set(engine_util.KeyWithCF(abatch.Cf(), abatch.Key()), abatch.Value())
})
case storage.Delete:
err = s.db.Update(func(txn *badger.Txn) error {
return txn.Delete(engine_util.KeyWithCF(abatch.Cf(), abatch.Key()))
})
}
if err != nil {
return err
}
}
return nil
}
最后是关于读操作,完成如下,我们需要返回一个StorageReader
对象。
func (s *StandAloneStorage) Reader(ctx *kvrpcpb.Context) (storage.StorageReader, error) {
// Your Code Here (1).
if !s.running {
return nil, errors.New("cannot read on storage that is not running")
}
return NewStandALoneReader(s), nil
}
实现 StandAloneReader 的数据和方法
StandAloneReader
是我自定义的一个结构体,它实现了storage.StorageReader
接口,从而能被StandAloneStorage.Reader
函数返回。该接口的定义是:
type StorageReader interface {
// When the key doesn't exist, return nil for the value
GetCF(cf string, key []byte) ([]byte, error)
IterCF(cf string) engine_util.DBIterator
Close()
}
可见我们需要为StandAloneReader
实现这些方法。我对于StandAloneReader
结构体的实现如下:
type StandAloneReader struct {
db *badger.DB
txns []*badger.Txn
iters []*engine_util.BadgerIterator
}
首先我们应该实现创建一个StandAloneReader
的方法如下。我们把StandAloneStorage
的数据库指针给到了它派生出来的StandAloneReader
。然后,这个派生出来的 Reader 被加到StandAloneStorage
的列表中存起来。
func NewStandALoneReader(storage *StandAloneStorage) *StandAloneReader {
reader := &StandAloneReader{
db: storage.db,
}
storage.readers = append(storage.readers, reader)
return reader
}
接下来是实现接口中的三个函数。首先实现GetCF
。这里的item
和get_err
通过匿名函数中的txn.Get
方法得到。这里的item
对象是badger.Item
类型,通过item.Key()
可以获取键,通过item.Value()
可以获得值。我们首先要判断一下Get
函数有没有出错,如果没错说明找到了对应的 key,直接调用item.Value()
即可。如果出错,需要注意一种特殊情形,就是错误为ErrKeyNotFound
的情况——因为项目要求我们在找不到键的情况下返回nil
作为值。
func (reader *StandAloneReader) GetCF(cf string, key []byte) ([]byte, error) {
value := []byte{}
var item *badger.Item
var get_err error
view_err := reader.db.View(func(txn *badger.Txn) error {
item, get_err = txn.Get(engine_util.KeyWithCF(cf, key))
return nil
})
if view_err != nil {
return value, view_err
}
if get_err == nil {
return item.Value()
} else if get_err == badger.ErrKeyNotFound {
return nil, nil
} else {
return nil, get_err
}
}
然后实现IterCF
函数。这个函数要返回一个engine_util.DBIterator
对象(这是一个接口),我们发现给出的代码中,只有engine_util.BadgerIterator
对象实现了这个接口。所以获取某个数据库db
的迭代器,首先通过这个数据库db
产生一个新的事务对象txn
,然后调用NewCFIterator
函数生成一个迭代器iter
。这个iter
需要被记录下来,以便用完的时候关闭。
func (reader *StandAloneReader) IterCF(cf string) engine_util.DBIterator {
txn := reader.db.NewTransaction(false)
iter := engine_util.NewCFIterator(cf, txn)
reader.iters = append(reader.iters, iter)
reader.txns = append(reader.txns, txn)
return iter
}
最后实现Close
函数。这个函数由StandAloneStorage
调用,关闭它范围内的所有 reader。我们注意到IterCF()
函数中产生了一个新的Txn
和一个新的BadgerIterator
,它们在使用完之后都需要关闭。所以一个 reader 一旦收到关闭的命令,要把它先前通过IterCF()
产生的Txn
和BadgerIterator
全部关闭。代码实现如下:
func (reader *StandAloneReader) Close() {
for _, iter := range reader.iters {
iter.Close()
}
for _, txn := range reader.txns {
txn.Discard()
}
}
提供 raw_api 服务
一个StandAloneStorage
上层还有一个server
,而真正暴露给用户的是server
。所以server
需要提供一系列的 API(增删改查之类的)以供用户使用。这里我们将完成这些 API。
RawGet API
RawGet
需要server
读取它底下的 storage,可以产生一个数据库的 reader 完成。reader
的GetCF
可以将键对应的值全部返回出来。当没有错误发生,而返回的值又为nil
时,说明键找不到,返回的RawGetResponce
中应有NotFound = true
。注意,reader
的使命完成后需要关闭。
// RawGet return the corresponding Get response based on RawGetRequest's CF and Key fields
func (server *Server) RawGet(_ context.Context, req *kvrpcpb.RawGetRequest) (*kvrpcpb.RawGetResponse, error) {
// Your Code Here (1).
reader, err := server.storage.Reader(req.Context)
defer reader.Close()
if err != nil {
return nil, err
} else {
value, err := reader.GetCF(req.Cf, req.Key)
if err != nil {
return nil, err
} else {
responce := &kvrpcpb.RawGetResponse{
Value: value,
}
if value == nil {
responce.NotFound = true
}
return responce, nil
}
}
}
RawPut API
可以直接使用 storage 的Write()
方法。
// RawPut puts the target data into storage and returns the corresponding response
func (server *Server) RawPut(_ context.Context, req *kvrpcpb.RawPutRequest) (*kvrpcpb.RawPutResponse, error) {
// Your Code Here (1).
// Hint: Consider using Storage.Modify to store data to be modified
err := server.storage.Write(req.Context, []storage.Modify{{
Data: storage.Put{
Key: req.Key,
Value: req.Value,
Cf: req.Cf,
}},
})
return &kvrpcpb.RawPutResponse{}, err
}
RawDelete API
和RawPut
类似,也可以直接使用 storage 的Write()
方法。
// RawDelete delete the target data from storage and returns the corresponding response
func (server *Server) RawDelete(_ context.Context, req *kvrpcpb.RawDeleteRequest) (*kvrpcpb.RawDeleteResponse, error) {
// Your Code Here (1).
// Hint: Consider using Storage.Modify to store data to be deleted
err := server.storage.Write(req.Context, []storage.Modify{{
Data: storage.Delete{
Key: req.Key,
Cf: req.Cf,
},
}})
return &kvrpcpb.RawDeleteResponse{}, err
}
RawScan API
这个 API 的含义有点意思。我们发现RawScanRequest
有StartKey
和Limit
两个字段。这表示我们需要再数据库中,读取从StartKey
开始的键值对,而键值对的个数最多是Limit
个。我们首先需要产生一个 reader,然后在这个 reader 的基础上产生一个迭代器——这两者用完之后都应该关闭。然后最坑的一点是,这个迭代器iter
要么先Rewind()
,要么先Seek()
,不然用不了。所谓Rewind()
就是移到零点,从一开始的元素开始迭代,而Seek()
可以从给定的元素开始迭代。很显然我们这里需要从StartKey
开始迭代。据此,不难写出代码如下:
// RawScan scan the data starting from the start key up to limit. and return the corresponding result
func (server *Server) RawScan(_ context.Context, req *kvrpcpb.RawScanRequest) (*kvrpcpb.RawScanResponse, error) {
// Your Code Here (1).
// Hint: Consider using reader.IterCF
reader, err := server.storage.Reader(req.Context)
if err != nil {
return &kvrpcpb.RawScanResponse{}, err
}
defer reader.Close()
iter := reader.IterCF(req.Cf)
defer iter.Close()
iter.Seek(req.StartKey)
kvs := []*kvrpcpb.KvPair{}
for iter.Valid() && len(kvs) < int(req.Limit) {
item := iter.Item()
key := item.Key()
value, err := item.Value()
if err != nil {
return &kvrpcpb.RawScanResponse{}, err
}
if value != nil {
kvs = append(kvs, &kvrpcpb.KvPair{Key: key, Value: value})
}
iter.Next()
}
return &kvrpcpb.RawScanResponse{Kvs: kvs}, nil
}
至此,Project1 也就完成了。