一文讀懂redis的zset

2021-12-25 5ycode
zset的數據結構

在redis中有一個有序列表,它的底層是由壓縮列表或跳表組成。我們看下對應的數據結構

壓縮鍊表:

跳表:

下載下來4.0的源碼 https://download.redis.io/releases/redis-4.0.0.tar.gz

對應的源碼:
src/server.h
# 最大層級
#define ZSKIPLIST_MAXLEVEL 32 /* Should be enough for 2^32 elements */
# 表示上一層級是下一層級的1/4,相當於是一棵四叉樹
#define ZSKIPLIST_P 0.25 /* Skiplist P = 1/4 */

typedef struct zset {
dict *dict;
zskiplist *zsl;
} zset;
typedef struct zskiplist {
// 頭節點,尾節點
struct zskiplistNode *header, *tail;
//長度
unsigned long length;
//當前索引的層數
int level;
} zskiplist;

typedef struct zskiplistNode {
// 動態字符串,member 對象
sds ele;
// 分值表示順序
double score;
// 後退指針
struct zskiplistNode *backward;
//索引層
struct zskiplistLevel {
// 前進指針指向node
struct zskiplistNode *forward;
// 這個層跨越的節點數量
unsigned int span;
} level[];
} zskiplistNode;


#src/t_zset源碼

int zslRandomLevel(void) {
int level = 1;
while ((random()&0xFFFF) < (ZSKIPLIST_P * 0xFFFF))
level += 1;
return (level<ZSKIPLIST_MAXLEVEL) ? level : ZSKIPLIST_MAXLEVEL;
}

我們看下zadd的源碼:
/* This generic command implements both ZADD and ZINCRBY. */
void zaddGenericCommand(client *c, int flags) {
static char *nanerr = "resulting score is not a number (NaN)";
robj *key = c->argv[1];
robj *zobj;
sds ele;
double score = 0, *scores = NULL;
int j, elements;
int scoreidx = 0;
/* The following vars are used in order to track what the command actually
* did during the execution, to reply to the client and to trigger the
* notification of keyspace change. */
int added = 0; /* Number of new elements added. */
int updated = 0; /* Number of elements with updated score. */
int processed = 0; /* Number of elements processed, may remain zero with
options like XX. */

/* Parse options. At the end 'scoreidx' is set to the argument position
* of the score of the first score-element pair. */
# 解析參數
scoreidx = 2;
while(scoreidx < c->argc) {
char *opt = c->argv[scoreidx]->ptr;
if (!strcasecmp(opt,"nx")) flags |= ZADD_NX;
else if (!strcasecmp(opt,"xx")) flags |= ZADD_XX;
else if (!strcasecmp(opt,"ch")) flags |= ZADD_CH;
else if (!strcasecmp(opt,"incr")) flags |= ZADD_INCR;
else break;
scoreidx++;
}
#參數轉換
/* Turn options into simple to check vars. */
int incr = (flags & ZADD_INCR) != 0;
int nx = (flags & ZADD_NX) != 0;
int xx = (flags & ZADD_XX) != 0;
int ch = (flags & ZADD_CH) != 0;

/* After the options, we expect to have an even number of args, since
* we expect any number of score-element pairs. */
# 參數必須成對
elements = c->argc-scoreidx;
if (elements % 2 || !elements) {
addReply(c,shared.syntaxerr);
return;
}
elements /= 2; /* Now this holds the number of score-element pairs. */

/* Check for incompatible options. */
if (nx && xx) {
addReplyError(c,
"XX and NX options at the same time are not compatible");
return;
}

if (incr && elements > 1) {
addReplyError(c,
"INCR option supports a single increment-element pair");
return;
}

/* Start parsing all the scores, we need to emit any syntax error
* before executing additions to the sorted set, as the command should
* either execute fully or nothing at all. */
# 遍歷所有要添加的元素,score必須是數字
scores = zmalloc(sizeof(double)*elements);
for (j = 0; j < elements; j++) {
if (getDoubleFromObjectOrReply(c,c->argv[scoreidx+j*2],&scores[j],NULL)
!= C_OK) goto cleanup;
}

/* Lookup the key and create the sorted set if does not exist. */
# zset不存在就加鎖創建
zobj = lookupKeyWrite(c->db,key);
if (zobj == NULL) {
if (xx) goto reply_to_client; /* No key + XX option: nothing to do. */
#選擇存儲結構
if (server.zset_max_ziplist_entries == 0 ||
server.zset_max_ziplist_value < sdslen(c->argv[scoreidx+1]->ptr))
{
# 跳表
zobj = createZsetObject();
} else {
# 壓縮列表
zobj = createZsetZiplistObject();
}
# 將創建好的數據結構添加到db中
dbAdd(c->db,key,zobj);
} else {
if (zobj->type != OBJ_ZSET) {
addReply(c,shared.wrongtypeerr);
goto cleanup;
}
}

for (j = 0; j < elements; j++) {
double newscore;
score = scores[j];
int retflags = flags;
# 從參數中獲取元素member
# ZADD key score member [[score member] [score member] ...]
ele = c->argv[scoreidx+1+j*2]->ptr;
# 添加邏輯具體看zsetAdd
int retval = zsetAdd(zobj, score, ele, &retflags, &newscore);
# 添加失敗就清空
if (retval == 0) {
addReplyError(c,nanerr);
goto cleanup;
}
if (retflags & ZADD_ADDED) added++;
if (retflags & ZADD_UPDATED) updated++;
if (!(retflags & ZADD_NOP)) processed++;
score = newscore;
}
server.dirty += (added+updated);

reply_to_client:
if (incr) { /* ZINCRBY or INCR option. */
if (processed)
addReplyDouble(c,score);
else
addReply(c,shared.nullbulk);
} else { /* ZADD. */
addReplyLongLong(c,ch ? added+updated : added);
}

cleanup:
zfree(scores);
if (added || updated) {
signalModifiedKey(c->db,key);
notifyKeyspaceEvent(NOTIFY_ZSET,
incr ? "zincr" : "zadd", key, c->db->id);
}
}

void zaddCommand(client *c) {
zaddGenericCommand(c,ZADD_NONE);
}

void zincrbyCommand(client *c) {
zaddGenericCommand(c,ZADD_INCR);
}

int zsetAdd(robj *zobj, double score, sds ele, int *flags, double *newscore) {

if (zobj->encoding == OBJ_ENCODING_ZIPLIST) {

if ((eptr = zzlFind(zobj->ptr,ele,&curscore)) != NULL) {
# 存在的時候,刪除並替換
if (score != curscore) {
zobj->ptr = zzlDelete(zobj->ptr,eptr);
zobj->ptr = zzlInsert(zobj->ptr,ele,score);
*flags |= ZADD_UPDATED;
}
return 1;
} else if (!xx) {
/* Optimize: check if the element is too large or the list
* becomes too long *before* executing zzlInsert. */
zobj->ptr = zzlInsert(zobj->ptr,ele,score);
#在滿足zset_max_ziplist_entries 和zset_max_ziplist_value的時候,轉化成跳表
if (zzlLength(zobj->ptr) > server.zset_max_ziplist_entries)
zsetConvert(zobj,OBJ_ENCODING_SKIPLIST);
if (sdslen(ele) > server.zset_max_ziplist_value)
zsetConvert(zobj,OBJ_ENCODING_SKIPLIST);
if (newscore) *newscore = score;
*flags |= ZADD_ADDED;
return 1;
} else {
*flags |= ZADD_NOP;
return 1;
}
} else if (zobj->encoding == OBJ_ENCODING_SKIPLIST) {
zset *zs = zobj->ptr;
zskiplistNode *znode;
dictEntry *de;
# 存在的時候,刪除並替換
de = dictFind(zs->dict,ele);
if (de != NULL) {
if (score != curscore) {
zskiplistNode *node;
serverAssert(zslDelete(zs->zsl,curscore,ele,&node));
znode = zslInsert(zs->zsl,score,node->ele);
node->ele = NULL;
zslFreeNode(node);
/* Note that we did not removed the original element from
* the hash table representing the sorted set, so we just
* update the score. */
dictGetVal(de) = &znode->score; /* Update score ptr. */
*flags |= ZADD_UPDATED;
}
return 1;
} else if (!xx) {
# 將元素轉為sds
ele = sdsdup(ele);
znode = zslInsert(zs->zsl,score,ele);
serverAssert(dictAdd(zs->dict,ele,&znode->score) == DICT_OK);
*flags |= ZADD_ADDED;
if (newscore) *newscore = score;
return 1;
} else {
*flags |= ZADD_NOP;
return 1;
}
} else {
serverPanic("Unknown sorted set encoding");
}
return 0; /* Never reached. */
}

在redis的官方文檔中 https://raw.githubusercontent.com/redis/redis/4.0/redis.conf 有個配置參數:

# Similarly to hashes and lists, sorted sets are also specially encoded in
# order to save a lot of space. This encoding is only used when the length and
# elements of a sorted set are below the following limits:
zset-max-ziplist-entries 128
zset-max-ziplist-value 64

簡單的梳理下流程: 

通過以上資料,我們可以推斷下:

在小數據量的時候,壓縮列表的性能不會比跳表差太多;

通過數據結構的對比,壓縮列表在有序性上的性能損耗相對較大,插入數據需要將數據後移;

跳表對數據的插入相對比較友好,直接修改指針就行;

跳表需要注意多級索引的退化,需要在合適的時機修改或重構索引;

zset的應用場景滑動窗口限流

如:生產上要對外部調用系統進行限流,每分鐘只能有1000的調用量 

使用命令:

延遲隊列

使用命令:

排行榜

zadd ranking_list 1 a 初始化

ZINCRBY ranking_list 1 a 瀏覽量+1

ZREVRANGE ranking_list 0 9 按score從大到小取10條

相關焦點

  • redis zset實現排行榜
    redis zsetredis zset由於如下特性天生支持排行榜需求:利用 redis zset 實現排行榜功能現在假定有如下排行榜需求:實現一個粉絲Top10排行榜。簡單分析該需求,我們可以將用戶id作為member,用戶粉絲量作為score,並設置 redis key 為 fans_rank 形成 redis zset按照如下步驟你可以很容易地實現該功能:1.zset添加member如果用戶粉絲數目發生變動,則需要將該用戶重新添加到zset中進行排序,假定用戶id為
  • redis的zset有多牛?請把耳朵遞過來
    本篇文章很短,但信息量很大,是關於redis的zset。我來分享一點遇到過的線上數據,或許對你的決策有幫助。redis支持一個數據結構,叫做 zset,也就是有序的列表。當然redis也不能濫用,可以看我以前的規範文章:《這可能是最中肯的Redis規範了》忘了zset是個啥的同學可以看這張gif圖。通過它,可以實現遊戲排行榜一類的功能,或者實現Topx這樣的需求,也能精準的讓用戶在海量數據中找到自己的位置。
  • 【快速了解redis】五種數據類型-zset篇
    >前言五一用來休息了,現在將redis五種常見數據類型中最後的一種,zset類型的講解完。可以說,zset的數據類型是redis這幾個數據類型中,最複雜,也是最靈活的一種類型。它的中文翻譯,為有序集合,顧名思義,他的數據排列是按照一定的順序排列的,基於這個特性,我們也經常用zset類型來存儲需要排行的數據。
  • 用Redis的zset有序集合實現一個本周熱議功能
    但這裡我們使用redis來完成。之前上課時候我們說過,排行榜功能,我們可以使用redis的有序集合zset來完成。現在我們就這個數據結構來完成本周熱議的功能。在編碼之前,我們需要先來回顧一下zset的幾個基本命令。withscores代表的是否顯示順序號  start和stop代表所在的位置的索引。
  • 還不懂Redis是什麼?一文帶你深入Redis基本結構,準備向開發進軍
    #運行redis容器〉docker run - name myredis -d -p6379:6379 redis .〉yum install redis#運行客戶端> redis-cliRedis基礎數據結構Redis有5種基礎數據結構,分別為: string (字符串)、list (列表)、set (集合)、hash (哈希)和zset
  • redis基礎筆記
    ——陶淵明《飲酒》前言reference: https://www.tutorialspoint.com/redis/redis_quick_guide.htmscrapy過濾重複連結使用到了redis,所以就先熟悉了下redis的基礎。
  • Redis的set和zset
    set&zset這裡我們重點介紹集合(set)和有序集合(zset)。集合的實現本質還是使用了哈希,我們知道,哈希的key就是無序不重複的,因此,哈希的key天然就是一種set,我們用不到哈希值,將value存儲為null,就實現了集合這種數據結構。
  • 架構秘笈:移花接木,使用MySQL模擬Redis
    大家都知道redis速度快,但它的容量和內存容量有關,很容易達到瓶頸。有些網際網路公司,直接使用redis作為後端資料庫(在下佩服)。當業務量暴增,就面臨一個redis容量和價格的權衡問題。改業務代碼是來不及了,只好用一些持久化存儲 ,來模擬redis的一些數據結構。redis支持近十種數據類型,最常用的有5種。string、hash、zset、set、list等。本文將針對幾種常見的數據結構,探討一下常用操作的模擬實現。
  • Redis常用命令手冊:鍵值相關命令
    下面將Redis提供的命令做一總結。  鍵值相關命令  1、keys  返回滿足給定pattern的所有key:  redis 127.0.0.1:6379> keys *  1) "myzset2"  2) "myzset3"  3) "mylist"  4) "myset2
  • Redis(一)入門安裝篇
    Redis與其他key-value緩存產品有以下三個特點:一 Redis支持數據的持久化,可以將內存中的數據保存在磁碟中,重啟的時候可以再次加載進行使用。二 Redis不僅僅支持簡單的key-value類型的數據,同時還提供list,set,zset,hash等數據結構的存儲。三 Redis支持數據的備份,即master-slave模式的數據備份。
  • 最全總結 | 聊聊 Python 數據處理全家桶(Redis篇)
    前言前面兩篇文章聊到了 Python 處理 Mysql、Sqlite 資料庫常用方式,本篇文章繼續說另外一種比較常用的數據存儲方式:RedisRedis:Remote Dictionary Server,即:遠程字典服務,Redis 底層使用 C 語言編寫,是一款開源的、基於內存的 NoSql 資料庫由於 Redis 性能遠超其他資料庫,並且支持集群、分布式及主從同步等優勢
  • 用 Python 操作 Redis,看這一篇就夠了
    前言前面兩篇文章聊到了 Python 處理 Mysql、Sqlite 資料庫常用方式,本篇文章繼續說另外一種比較常用的數據存儲方式:RedisRedis:Remote Dictionary Server,即:遠程字典服務,Redis 底層使用 C 語言編寫,是一款開源的、基於內存的 NoSql 資料庫由於 Redis 性能遠超其他資料庫,並且支持集群、分布式及主從同步等優勢
  • 一文搞定Redis五大數據類型及使用場景
    Redis 是一種基於鍵值對的NoSQL資料庫,它的值主要由string(字符串),hash(哈希),list(列表),set(集合),z
  • 如何實現redis主從複製?
    一、多臺伺服器上配置主從複製Redis從5.0以後主從配置屬性發生了變化,在5.0之前配置的是slaveof,5.0以後變成了replicaof伺服器用途redis埠號備註centos7 192.168.1.6主機Master(寫)6379redis5.0centos7 192.168.1.4從機Slave(讀)6379redis5.0centos7 192.168.1.5
  • redis到期設置時長expire用法
    Redis不僅僅支持簡單的key-value類型的數據,同時還提供list,set,zset,hash等數據結構的存儲。Redis支持數據的備份,即master-slave模式的數據備份。auth 密碼:登錄rediskeys *:查看所有的緩存ttl keys:查看緩存時間expire key seconds:設置緩存時間1,連接redis
  • Python操作Redis大全
    (點擊上方快速關注並設置為星標,一起學Python)來源:  廖高祥        連結:https://segmentfault.com/a/1190000016696863一、從Redis 2.6 版本開始, 在命令行下Srandmember 命令接受可選返回元素數量的參數 redis>SRANDMEMBER name countSREM:移除集合中一個元素,srem(self, name, value),redis模塊任然沿用Redis 2.4 版本以前的只接受單個元素的用法。
  • Python | Python學習之Redis交互詳解
    前言最近在學習scrapy redis,順便複習了redis。本篇為redis篇,包含實例演示,主從服務配置,python交互等內容。log file /xx/xx/xx/redis-server.log 日誌文件位置slaveof ip port 主從複製的ip埠啟動redis:sudo server redis start停止redis:sudo server redis stop重啟redis:sudo server redis restart加載指定的redis
  • 二進位跳動帶你看Redis數據結構底層
    緊接著還有問題,String在redis底層是怎麼存儲的?這些數據類型在Redis中是怎麼存放的?Redis快的原因是不是只有單線程和基於內存處理?下面我們來一一梳理下這幾張數據類型的底層結構(redis3.x):Stringredis是自己定義的一種字符串格式,叫SDS中文譯名叫簡單動態字符串(Simple  Dynamic String)。
  • 超強、超詳細Redis入門教程
    官網地址: https://redis.io/命令地址:http://doc.redisfans.com/最新版本 : 3.2.9應用版本: 3.0.4Redis的五大數據類型以及應用場景 Listk-v格式中 v的數據類型是List
  • 圖解redis五種數據結構底層實現(動圖哦)
    redis有五種基本數據結構:字符串、hash、set、zset、list。但是你知道構成這五種結構的底層數據結構是怎樣的嗎? 今天我們來花費五分鐘的時間了解一下。 (目前redis版本為3.0.6)動態字符串SDSSDS是"simple dynamic string"的縮寫。