tinykv Project1 实现思路

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。这里的itemget_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()产生的TxnBadgerIterator全部关闭。代码实现如下:

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 完成。readerGetCF可以将键对应的值全部返回出来。当没有错误发生,而返回的值又为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 的含义有点意思。我们发现RawScanRequestStartKeyLimit两个字段。这表示我们需要再数据库中,读取从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 也就完成了。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值