zset
zset是一种很有用的数据结构,你可能听说过set,但什么是zset呢?zset就是sorted_set(排序集),STL里有set,底层是用红黑树实现的,并没有提供sorted_set的实现,要实现一个zset并不难,比如SSDB的作者就用map和set实现了一个简单的zset。代码在ssdb/src/util/sorted_set.h sorted_set.cpp中。
sorted_set.h
class SortedSet
{
public:
bool empty() const{
return size() == 0;
}
int size() const;
int add(const std::string &key, int64_t score);
// 0: not found, 1: found and deleted
int del(const std::string &key);
// the first item is copied into key if SortedSet not empty
int front(std::string *key, int64_t *score=NULL) const;
int back(std::string *key, int64_t *score=NULL) const;
int64_t max_score() const;
int pop_front();
int pop_back();
};
sorted_set.cpp
#include "sorted_set.h"
int SortedSet::size() const{
return (int)sorted_set.size();
}
int SortedSet::add(const std::string &key, int64_t score){
int ret;
std::map<std::string, std::set<Item>::iterator>::iterator it;
it = existed.find(key);
if(it == existed.end()){
// new item
ret = 1;
}else{
ret = 0;
std::set<Item>::iterator it2 = it->second;
const Item &item = *it2;
if(item.score == score){
// not updated
return 0;
}
// remove existing item
sorted_set.erase(it2);
}
Item item;
item.key = key;
item.score = score;
std::pair<std::set<Item>::iterator, bool> p = sorted_set.insert(item);
existed[key] = p.first;
return ret;
}
int SortedSet::del(const std::string &key){
int ret;
std::map<std::string, std::set<Item>::iterator>::iterator it;
it = existed.find(key);
if(it == existed.end()){
// new item
ret = 0;
}else{
ret = 1;
sorted_set.erase(it->second);
existed.erase(it);
}
return ret;
}
int SortedSet::front(std::string *key, int64_t *score) const{
std::set<Item>::iterator it2 = sorted_set.begin();
if(it2 == sorted_set.end()){
return 0;
}
const Item &item = *it2;
*key = item.key;
if(score){
*score = item.score;
}
return 1;
}
int SortedSet::back(std::string *key, int64_t *score) const{
std::set<Item>::reverse_iterator it2 = sorted_set.rbegin();
if(it2 == sorted_set.rend()){
return 0;
}
const Item &item = *it2;
*key = item.key;
if(score){
*score = item.score;
}
return 1;
}
int64_t SortedSet::max_score() const{
int64_t score = 0;
std::string key;
this->back(&key, &score);
return score;
}
int SortedSet::pop_front(){
if(sorted_set.empty()){
return 0;
}
std::set<Item>::iterator it = sorted_set.begin();
const Item &item = *it;
existed.erase(item.key);
sorted_set.erase(it);
return 1;
}
int SortedSet::pop_back(){
if(sorted_set.empty()){
return 0;
}
std::set<Item>::iterator it = sorted_set.end();
it --;
const Item &item = *it;
existed.erase(item.key);
sorted_set.erase(it);
return 1;
}
很简单是不是,sorted_set 根据score排序而不是key,但今天要说的zset实现不是上面的代码,上面的简单实现只是用来处理ttl的。
下面来说说基于leveldb的zset实现。
zset实现分析
zset的实现在 src/ssdb/t_zset.h t_zset.cpp 下。
zset的接口如下:
/* zset */
virtual int zset(const Bytes &name, const Bytes &key, const Bytes &score, char log_type=BinlogType::SYNC);
virtual int zdel(const Bytes &name, const Bytes &key, char log_type=BinlogType::SYNC);
// -1: error, 1: ok, 0: value is not an integer or out of range
virtual int zincr(const Bytes &name, const Bytes &key, int64_t by, int64_t *new_val, char log_type=BinlogType::SYNC);
//int multi_zset(const Bytes &name, const std::vector<Bytes> &kvs, int offset=0, char log_type=BinlogType::SYNC);
//int multi_zdel(const Bytes &name, const std::vector<Bytes> &keys, int offset=0, char log_type=BinlogType::SYNC);
virtual int64_t zsize(const Bytes &name);
/**
* @return -1: error; 0: not found; 1: found
*/
virtual int zget(const Bytes &name, const Bytes &key, std::string *score);
virtual int64_t zrank(const Bytes &name, const Bytes &key);
virtual int64_t zrrank(const Bytes &name, const Bytes &key);
virtual ZIterator* zrange(const Bytes &name, uint64_t offset, uint64_t limit);
virtual ZIterator* zrrange(const Bytes &name, uint64_t offset, uint64_t limit);
/**
* scan by score, but won't return @key if key.score=score_start.
* return (score_start, score_end]
*/
virtual ZIterator* zscan(const Bytes &name, const Bytes &key,
const Bytes &score_start, const Bytes &score_end, uint64_t limit);
virtual ZIterator* zrscan(const Bytes &name, const Bytes &key,
const Bytes &score_start, const Bytes &score_end, uint64_t limit);
virtual int zlist(const Bytes &name_s, const Bytes &name_e, uint64_t limit,
std::vector<std::string> *list);
virtual int zrlist(const Bytes &name_s, const Bytes &name_e, uint64_t limit,
std::vector<std::string> *list);
实现如下:
只看插入 即zset:
int SSDBImpl::zset(const Bytes &name, const Bytes &key, const Bytes &score, char log_type){
Transaction trans(binlogs);
int ret = zset_one(this, name, key, score, log_type);
if(ret >= 0){
if(ret > 0){
if(incr_zsize(this, name, ret) == -1){
return -1;
}
}
leveldb::Status s = binlogs->commit();
if(!s.ok()){
log_error("zset error: %s", s.ToString().c_str());
return -1;
}
}
return ret;
}
主要调用了 zset_one:
// returns the number of newly added items
static int zset_one(SSDBImpl *ssdb, const Bytes &name, const Bytes &key, const Bytes &score, char log_type){
if(name.empty() || key.empty()){
log_error("empty name or key!");
return 0;
//return -1;
}
if(name.size() > SSDB_KEY_LEN_MAX ){
log_error("name too long!");
return -1;
}
if(key.size() > SSDB_KEY_LEN_MAX){
log_error("key too long!");
return -1;
}
std::string new_score = filter_score(score);
std::string old_score;
int found = ssdb->zget(name, key, &old_score);//先查询是否有旧的值,有的话记录old值
if(found == 0 || old_score != new_score){ //需要更新或插入新值
std::string k0, k1, k2;
if(found){
// delete zscore key
k1 = encode_zscore_key(name, key, old_score);//将三个值编码成底层leveldb 操作的key。也就是旧的score
ssdb->binlogs->Delete(k1);//删掉
}
// add zscore key
k2 = encode_zscore_key(name, key, new_score);
ssdb->binlogs->Put(k2, "");//插入新的score
// update zset
k0 = encode_zset_key(name, key);
ssdb->binlogs->Put(k0, new_score); //提交leveldb
ssdb->binlogs->add_log(log_type, BinlogCommand::ZSET, k0); //写log
return found? 0 : 1;
}
return 0;
}
问题来了,新的记录插入,如何排序呢?
靠的是 encode_zscore_key函数,这个函数要确保 两个score 编码前后 大小关系一致,也就是说 如果编码前score1> score2,则编码后 e_score1 > e_score2 依然成立。
static inline
std::string encode_zscore_key(const Bytes &key, const Bytes &val, const Bytes &score){
//zset_one 调用此函数时 传入的参数分别是:name、key、score
//例如 client 输入 命令 zset 'z' 1 100
//再输入 第二条 zset 'z' 2 99
// 'z' 对应 name 2 对应key 99 对应score
// 传入到encode 函数后 'z' 对应 key, 2 对应 value , 99对应 score。
//如何保证编码以score决定大小,而排除key的影响呢?
std::string buf;
buf.append(1, DataType::ZSCORE); //这个append 都一样
buf.append(1, (uint8_t)key.size()); //这个 append 也都一样 都是 'z' 的size
buf.append(key.data(), key.size());//都是 'z'
int64_t s = score.Int64();
if(s < 0){
buf.append(1, '-');
}else{
buf.append(1, '=');
}
s = encode_score(s);
buf.append((char *)&s, sizeof(int64_t));//接着就append 了score 99 到这里已经比出了大小 string 只比较第一个不一样的 字符。
buf.append(1, '=');
buf.append(val.data(), val.size()); //最后才append 2
return buf;
}
为什么传入leveldb的key 可比较大小,就实现了soreted_set 呢?
这是因为 leveldb的内存实现是skiplist,它是排序的。磁盘文件sst也是排序的.
zget 很简单就不讲了。