为了账号安全,请及时绑定邮箱和手机立即绑定

Redis 数据结构与内存管理策略(下)

标签:
Redis

字典(dict)

dict 字典是基于 hash算法 来实现,是 Hash 数据类型的底层存储数据结构。我们来看下 redis 3.0.0 版本的 dict.h 头文件定义。

typedefstructdict{dictType *type;void*privdata;    dictht ht[2];longrehashidx;intiterators; } dict;

typedefstructdictht{dictEntry **table;unsignedlongsize;unsignedlongsizemask;unsignedlongused;} dictht;

typedefstructdictEntry{void*key;union{void*val;uint64_tu64;int64_ts64;doubled;    } v;structdictEntry*next;} dictEntry;

说到 hash table 有两个东西是我们经常会碰到的,首先就是 hash 碰撞 问题,redis dict 是采用链地址法来解决,dictEntry->next 就是指向下个冲突 key的节点。

还有一个经常碰到的就是 rehash 的问题,提到 rehash 我们还是有点担心性能的。那么redis 实现是非常巧妙的,采用 惰性渐进式 rehash 算法 。

在 dict struct 里有一个 ht[2] 组数,还有一个 rehashidx 索引。redis 进行 rehash 的大致算法是这样的,首先会开辟一个新的 dictht 空间,放在 ht[2] 索引上,此时将 rehashidx 设置为0,表示开始进入 rehash 阶段,这个阶段可能会持续很长时间,rehashidx 表示 dictEntry 个数。

每次当有对某个 ht[1] 索引中的 key 进行访问时,获取、删除、更新,redis 都会将当前 dictEntry 索引中的所有 key rehash 到 ht[2] 字典中。一旦 rehashidx=-1 表示 rehash 结束。

跳表(skip list)

skip list 是 zset 的底层数据结构,有着高性能的查找排序能力。

我们都知道一般用来实现带有排序的查找都是用 Tree 来实现,不管是各种变体的 B Tree 还是 B+ Tree,本质都是用来做顺序查找。

skip list 实现起来简单,性能也与 B Tree 相接近。

typedefstructzskiplistNode{robj *obj;doublescore;structzskiplistNode*backward;structzskiplistLevel{structzskiplistNode*forward;unsignedintspan;    } level[];} zskiplistNode;typedefstructzskiplist{structzskiplistNode*header, *tail;unsignedlonglength;intlevel;} zskiplist;

zskiplistNode->zskiplistLevel->span 这个值记录了当前节点距离下个节点的跨度。每一个节点会有最大不超过 zskiplist->level 节点个数,分别用来表示不同跨度与节点的距离。

每个节点会有多个 forward 向前指针,只有一个 backward 指针。每个节点会有对象 __*obj__ 和 score 分值,每个分值都会按照顺序排列。

整数集合(int set)

int set 整数集合是 set 数据类型的底层实现数据结构,它的特点和使用场景很明显,只要我们使用的集合都是整数且在一定的范围之内都会使用整数集合编码。

SADDset:userid100200300(integer)3

OBJECTencodingset:userid"intset"

int set 使用一块连续的内存来存储集合数据,它是数组结构不是链表结构。

typedefstructintset{uint32_tencoding;uint32_tlength;int8_tcontents[];} intset;

intset->encoding 用来确定 contents[] 是什么类型的整数编码,以下三种值之一。

#defineINTSET_ENC_INT16(sizeof(int16_t))#defineINTSET_ENC_INT32(sizeof(int32_t))#defineINTSET_ENC_INT64(sizeof(int64_t))

redis 会根据我们设置的值类型动态 sizeof 出一个对应的空间大小。如果我们集合原来是 int16 ,然后往集合里添加了 int32 整数将触发升级,一旦升级成功不会触发降级操作。

压缩表(zip list)

zip list 压缩表是 listzsethash 数据类型的底层数据结构之一。它是为了节省内存通过压缩数据存储在一块连续的内存空间中。

typedefstructzlentry{unsignedintprevrawlensize, prevrawlen;unsignedintlensize, len;unsignedintheadersize;unsignedcharencoding;unsignedchar*p;} zlentry;

它最大的优点就是压缩空间,空间利用率很高。缺点就是一旦出现更新可能就是连锁更新,因为数据在内容空间中都是连续的,最极端情况下就是可能出现顺序连锁扩张。

压缩列表会由多个 zlentry 节点组成,每一个 zlentry 记录上一个节点长度和大小,当前节点长度 lensize 和大小 len 包括编码 encoding 。

这取决于业务场景,redis 提供了一组配置,专门用来针对不同的场景进行阈值控制。

hash-max-ziplist-entries512hash-max-ziplist-value64

list-max-ziplist-entries512list-max-ziplist-value64

zset-max-ziplist-entries128zset-max-ziplist-value64

上述配置分别用来配置 ziplist 作为 hash 、listzset 数据类型的底层压缩阈值控制。

Redis Object 类型与映射

redis 内部每一种数据类型都是对象化的,也就是我们所说的5种数据类型其实内部都会对应到 redisObject 对象,然后在由 redisObject 来包装具体的存储数据结构和编码。

typedefstructredisObject {unsignedtype:4;unsignedencoding:4;unsignedlru:REDIS_LRU_BITS;intrefcount;void*ptr;} robj;

这是一个很 OO 的设计,redisObject->type 是 5 种数据类型之一,redisObject->encoding 是这个数据类型所使用的数据结构和编码。

我们看下 redis 提供的 5 种数据类型与每一种数据类型对应的存储数据结构和编码。

/* Object types */#defineREDIS_STRING 0#defineREDIS_LIST 1#defineREDIS_SET 2#defineREDIS_ZSET 3#defineREDIS_HASH 4

#defineREDIS_ENCODING_RAW 0#defineREDIS_ENCODING_INT 1#defineREDIS_ENCODING_HT 2#defineREDIS_ENCODING_ZIPMAP 3#defineREDIS_ENCODING_LINKEDLIST 4#defineREDIS_ENCODING_ZIPLIST 5#defineREDIS_ENCODING_INTSET 6#defineREDIS_ENCODING_SKIPLIST 7#defineREDIS_ENCODING_EMBSTR 8

REDIS_ENCODING_ZIPMAP 3 这个编码可以忽略了,在特定的情况下有性能问题,在 redis 2.6 版本之后已经废弃,为了兼容性保留。

webp

上图是 redis 5 种数据类型与底层数据结构和编码的对应关系,但是这种对应关系在每一个版本中都会有可能发生变化,这也是 redisObject 的灵活性所在,有着 OO 的这种多态性。

redisObject->refcount 表示当前对象的引用计数,在 redis 内部为了节省内存采用了共享对象的方法,当某个对象被引用的时候这个 refcount 会加 1,释放的时候会减 1

redisObject->lru 表示当前对象的 空转时长,也就是 idle time ,这个时间会是 redis lru 算法用来释放对象的时间依据。可以通过 OBJECT idletime 命令查看某个 key 的空转时长 lru 时间。

Redis 内存管理策略

redis 在服务端分别为不同的 db index 维护一个 dict 这个 dict 称为 key space 键空间 。每一个 redis client 只能属于一个 db index ,在 redis 服务端会维护每一个链接的 redisClient 。

typedefstructredisClient{uint64_tid;intfd;    redisDb *db;} redisClient;

在服务端每一个 redis 客户端都会有一个指向 redisDb 的指针。

typedefstructredisDb{dict *dict;    dict *expires;    dict *blocking_keys;    dict *ready_keys;    dict *watched_keys;structevictionPoolEntry*eviction_pool;intid;longlongavg_ttl;} redisDb;

key space 键空间就是这里的 redisDb->dict 。redisDb->expires 是维护所有键空间的每一个 key 的过期时间。

键 过期时间、生存时间

对于一个 key 我们可以设置它多少秒、毫秒之后过期,也可以设置它在某个具体的时间点过期,后者是一个时间戳。

EXPIRE 命令可以设置某个 key 多少秒之后过期

PEXPIRE 命令可以设置某个 key 多少毫秒之后过期

EXPIREAT 命令可以设置某个 key 在多少秒时间戳之后过期

PEXPIREAT 命令可以设置某个 key 在多少毫秒时间戳之后过期

PERSIST 命令可以移除键的过期时间

其实上述命令最终都会被转换成对 PEXPIREAT 命令。在 redisDb->expires 指向的 key 字典中维护着一个到期的毫秒时间戳。

TTL、PTTL 可以通过这两个命令查看某个 key 的过期秒、毫秒数。

redis 内部有一个 事件循环,这个事件循环会检查键的过期时间是否小于当前时间,如果小于则会删除这个键。

过期键删除策略

在使用 redis 的时候我们最关心的就是键是如何被删除的,如何高效的准时的删除某个键。其实 redis 提供了两个方案来完成这件事情。

redis 采用 惰性删除 、 定期删除 双重删除策略。

当我们访问某个 key 的时候 redis 会检查它是否过期,这是惰性删除。

robj *lookupKeyRead(redisDb *db, robj *key) {    robj *val;    expireIfNeeded(db,key);val= lookupKey(db,key);if(val==NULL)        server.stat_keyspace_misses++;elseserver.stat_keyspace_hits++;returnval;}

intexpireIfNeeded(redisDb *db, robj *key){mstime_twhen = getExpire(db,key);mstime_tnow;if(when <0)return0;/* No expire for this key */if(server.loading)return0;        now = server.lua_caller ? server.lua_time_start : mstime();if(server.masterhost !=NULL)returnnow > when;/* Return when this key has not expired */if(now <= when)return0;/* Delete the key */server.stat_expiredkeys++;    propagateExpire(db,key);    notifyKeyspaceEvent(REDIS_NOTIFY_EXPIRED,"expired",key,db->id);returndbDelete(db,key);}

redis 也会通过 事件循环 周期性的执行 key 的过期删除动作,这是定期删除。

intserverCron(structaeEventLoop *eventLoop,longlongid,void*clientData) {/* Handle background operations on Redis databases. */databasesCron();}

voiddatabasesCron(void){/* Expire keys by random sampling. Not required for slaves

     * as master will synthesize DELs for us. */if(server.active_expire_enabled && server.masterhost ==NULL)        activeExpireCycle(ACTIVE_EXPIRE_CYCLE_SLOW);}



作者:java菜
链接:https://www.jianshu.com/p/e9a32faa97a5


点击查看更多内容
TA 点赞

若觉得本文不错,就分享一下吧!

评论

作者其他优质文章

正在加载中
手记
粉丝
200
获赞与收藏
960

关注作者,订阅最新文章

阅读免费教程

  • 推荐
  • 评论
  • 收藏
  • 共同学习,写下你的评论
感谢您的支持,我会继续努力的~
扫码打赏,你说多少就多少
赞赏金额会直接到老师账户
支付方式
打开微信扫一扫,即可进行扫码打赏哦
今天注册有机会得

100积分直接送

付费专栏免费学

大额优惠券免费领

立即参与 放弃机会
意见反馈 帮助中心 APP下载
官方微信

举报

0/150
提交
取消