1. 简介
Redis 中的每个 Key-Value 在内存中都会被划分成 DictEntry,RedisObject 以及具体对象, 其中 DictEntry 又分别包含指向 Key 和 Value 的指针 (以 RedisObject 的形式) 以及指向下一个 DictEntry 的指针.
Key 固定是字符串, 因此使用字符串对象来进行表示, Value 可以是字符串, 列表, 哈希, 集合, 有序集合对象中的任意一种.
Redis 提供了五种对象, 每种对象都需要使用 RedisObject 进行表示.
Redis 使用 redisObject 结构来表示对象(存储对象的相关信息)
- typedef struct redisObject {
- unsigned type;
- unsigned encoding;
- unsigned lru;
- int refcount;
- void *ptr;
- }robj;
type 属性: 存储对象的类型(String,List,Hash,Set,ZSet 中的一种)
encoding 属性: 存储对象使用的编码方式, 不同的编码方式使用不同的数据结构进行存储.
lru 属性: 存储对象最后一次被访问的时间.
refcount 属性: 存储对象被引用的次数.
*ptr 指针: 指向对象的地址.
使用 type 命令可以查看对象的类型.
使用 object encoding 命令可以查看对象使用的编码方式.
使用 object idletime 命令可以查看对象的空转时间(即多久没有被访问, 并不会刷新当前 RedisObject 的 lru 属性)
使用 object refcount 命令可以查看对象被引用的次数.
* 这些命令都是通过 Key 找到对应的 Value 再从 Value 对应的 RedisObject 中进行获取.
2. 字符串
Redis 没有直接使用 C 语言的字符串, 而是自定义了一种字符串类型, 以对象的形式存在(C 语言的字符串只是单纯的字面量, 不能够进行修改)
Redis 使用 sdshdr 结构来表示字符串对象(SDS)
- struct sdshdr {
- int len;
- int free;
- char buf[];
- };
len 属性: 字符串的长度.
free 属性: 未使用的字节数量.
buf 数组: 字符串的底层实现用于存储字符.
*buf 数组中会有 \ 0 空字符, 该空字符不会记录在 len 属性中.
SDS 相比 C 语言的字符串
C 语言中存储字符串的字节数组其长度总是 N+1(最后一个是结束符), 因此一旦对字符串进行追加则需要重新分配内存.
为了避免 C 字符串的这种缺陷, SDS 通过未使用的空间解除了字符串长度和底层数组长度之间的关系, 在 SDS 中 buf 数组的长度不一定就是字符串长度 + 1, 数组里面还可以包含未使用的字节.
通过未使用的空间, SDS 实现了空间预分配和惰性空间释放两种策略, 从而减少由于字符串的修改导致内存重分配的次数.
空间预分配: 用于优化 SDS 保存的字符串的增长操作, 当需要对 SDS 保存的字符串进行增长操作时, 程序除了会为 SDS 分配所必须的空间以外, 还会为 SDS 分配额外的未使用空间.
惰性空间释放: 用于优化 SDS 保存的字符串的缩短操作, 当需要对 SDS 保存的字符串进行缩短操作时, 程序并不会立即使用内存重分配来回收缩短后多出来的字节, 而是使用 free 属性将这些多出来的字节的数量记录出来, 等待将来使用.
3. 字典
Redis 的字典使用散列表作为底层实现, 同时字典也是 Redis 数据库和 HashTable 编码方式的底层实现.
Redis 使用 dictht 结构来表示散列表
- typedef struct dictht {
- dictEntry **table;
- unsigned long size;
- unsigned long sizemask;
- unsigned long used;
- }dictht;
table 属性: 散列表.
size 属性: 散列表的大小.
sizemask 属性: 用于计算索引值.
used 属性: 散列表中节点的数量.
*Redis 的散列表使用链地址法的方式解决散列冲突, 最终就是指针数组的形式, 数组中的每个元素都是一个指向 DictEntry 的指针.
Redis 使用 dictEntry 结构来表示散列表中的节点
- typedef struct dictEntry {
- void *key;
- union{
- void *val;
- uint_tu64;
- int64_ts64;
- }v
- struct dictEntry next*;
- }dictEntry;
key 属性: 指向 Key 的指针(即 RedisObject)
value 属性: 可以是一个指向 Value 的指针(即 RedisObject),uint64_t 整数, int64_t 整数
next 属性: 指向下一个 DictEntry 的指针.
Redis 使用 dict 结构来表示字典, 每个字典包含两个 dictht.
- typedef struct dict{
- dictType *type;
- void *privatedata;
- dictht ht[2];
- int rehashidx;
- }dict;
type 属性: 指向 DictType 的指针, 每个 DictType 结构保存了一系列函数.
privatadata 属性: 传给特定函数的可选参数.
ht 属性: 长度为 2 的 dictht 数组, 一般情况下只使用 ht[0]散列表, 而 ht[1]散列表只会在对 ht[0]散列表进行 rehash 时使用
rehashidx 属性: 记录了 rehash 目前的进度, 如果目前没有进行 rehash 那么值为 - 1
DictType 的定义
- typedef struct dictType{
- // 哈希函数
- unsigned int (*hashFunction)(const void *key);
- // 复制 Key 的函数
- void *(*keyDup)(void *privatedata, const void *key);
- // 复制 Value 的函数
- void *(*valDup)(void *privatedata, const void *obj);
- // 对比 Key 的函数
- int (*keyCompare)(void *privatdata, const void *key1 , const void *key2);
- // 销毁 Key 的函数
- void (*keyDestructor)(void *privatedata, void *key);
- // 销毁 Value 的函数
- void (*valDestructor)(void *privatedata, void *obj);
- }dictType;
3.1 在字典中进行查找, 添加, 更新, 删除操作
在字典中进行查找
以客户端传递的 Key 作为关键字 K, 通过 dict 中的 dictType 的 H(K)散列函数计算散列值, 使用 dictht[0]的 sizemask 属性和散列值计算索引, 遍历索引对应的链表, 判断是否存在 Key 相同的 DictEntry, 若存在则返回该 DictEntry, 否则返回 NULL.
在字典中进行添加和更新操作
以客户端传递的 Key 作为关键字 K, 通过 dict 中的 dictType 的 H(K)散列函数计算散列值, 使用 dictht[0]的 sizemask 属性和散列值计算索引, 遍历索引对应的链表, 判断是否存在 Key 相同的 DictEntry, 若不存在 Key 相同的 DictEntry, 则创建代表 Key 的 SDS 对象和 RedisObject 以及代表 Value 的对象和 RedisObject, 然后创建一个 DictEntry 并分别指向 Key 和 Value 对应的 RedisObject, 最终将该 DictEntry 追加到链表的最后一个节点中, 若存在 Key 相同的 DictEntry, 则判断当前的命令是否满足 Value 对应的类型, 若满足则进行更新, 否则报错.
* 创建和更新操作是相对的, 当不存在则创建否则进行更新.
在字典中进行删除操作
以客户端传递的 Key 作为关键字 K, 通过 dict 中的 dictType 的 H(K)散列函数计算散列值, 使用 dictht[0]的 sizemask 属性和散列值计算索引, 遍历索引对应的链表, 找到 Key 相同的 DictEntry 进行删除.
3.2 散列表的扩容和缩容
由于散列表的负载因子需要维持在一个合理的范围内, 因此当散列表中的元素过多时会进行扩容, 过少时会进行缩容.
一旦散列表的长度发生改变, 那么就要进行 rehash, 即对原先散列表中的元素在新的散列表中重新进行 hash.
Redis 中的 rehash 是渐进式的, 并不是一次性完成, 因为要考虑性能问题, 如果散列表中包含上百万个节点, 那么庞大的计算量可能会导致 Redis 在一段时间内无法对外提供服务.
在 rehash 进行期间, 每次对字典执行查找, 添加, 更新, 删除操作时, 除了会执行指定的操作以外, 还会顺带将 ht[0]散列表在 rehashidx 索引上的所有节点 rehash 到 ht[1]上, 然后将 rehashidx 属性的值加 1.
渐进式 Rehash 的步骤
1. 为字典的 ht[1]散列表分配空间.
* 若执行的是扩容操作, 那么 ht[1]的长度为第一个大于等于 ht[0].used*2 的 2ⁿ.
* 若执行的是缩容操作, 那么 ht[1]的长度为第一个大于等于 ht[0].used 的 2ⁿ.
2.rehashidx 属性设置为 0, 表示开始进行 rehash.
3. 在 rehash 进行期间, 每次对字典执行查找, 添加, 更新, 删除操作时, 除了会执行指定的操作以外, 还会顺带将 ht[0]散列表在 rehashidx 索引上的所有节点 rehash 到 ht[1]上, 然后将 rehashidx 属性的值加 1.
4. 随着对字典不断的操作, 最终在某个时间点上, ht[0]散列表中的所有 dictEntry 都会被 rehash 到 ht[1]上, 当 rehash 结束之后将 rehashidx 属性的值设为 - 1, 表示 rehash 操作已完成.
* 在进行渐进式 rehash 的过程中, 字典会同时使用 ht[0]和 ht[1]两个散列表, 因此字典的查找, 更新, 删除操作会在两个散列表中进行, 如果在 ht[0]计算得到的索引指向 NULL 则从 ht[1]中进行匹配.
4.Redis 提供的编码方式
Redis 提供了八种编码方式, 每种编码方式都有其特定的数据存储结构.
4.1 INT 编码方式
INT 编码方式会将 RedisObject 中的 * ptr 指针直接改写成 long prt,prt 属性直接存储整数值.
4.2 EMBSTR 编码方式
4.3 ROW 编码方式
*EMBSTR 和 ROW 编码方式在内存中都会创建一个 RedisObject 和 SDS, 区别在于 EMBSTR 编码方式中 RedisObject 和 SDS 共同使用同一块内存单元, Redis 内存分配器只需要分配一次内存, 而 ROW 编码方式中需要分别为 RedisObject 和 SDS 分配内存单元.
4.4 ZIPLIST 编码方式
压缩列表是 Redis 为了节约内存而开发的, 它是一块顺序表(顺序存储结构, 内存空间连续), 一个压缩列表中可以包含多个 entry 节点, 每个 entry 节点可以保存一个字节数组或者一个整数值.
zlbytes: 记录了压缩列表的大小, 占 4 个字节.
zltail: 记录了压缩列表表尾节点距离起始位置的大小, 占 4 个字节.
zllen: 记录了压缩列表中节点的个数, 占 2 个字节.
entry: 压缩列表中的节点, 大小由节点中保存的内容决定.
zlend: 压缩列表的结束标志, 占 1 个字节.
如果存在一个指针 P 指向压缩列表的起始位置, 就可以根据 P+zltail 得到最后一个节点的地址.
4.5 LINKEDLIST 编码方式
Redis 使用 listNode 结构来表示链表中的节点.
- typedef struct listNode {
- struct listNode *prev;
- struct listNode *next;
- void *value;
- }listNode;
每个 listNode 节点分别包含指向前驱和后继节点的指针以及指向元素的指针.
Redis 使用 list 结构来持有 listNode
- typedef struct list {
- listNode *head;
- listNode *tail;
- unsigned long len;
- void dup(void *ptr); // 节点复制函数
- void free(void *ptr); // 节点值释放函数
- int match(void *ptr , void *key); // 节点值比对函数
- }list;
head 属性: 指向表头节点的指针.
tail 属性: 指向表尾节点的指针.
len 属性: 存储链表中节点的个数.
4.6 INTSET 编码方式
Redis 使用 intset 结构来表示整数集合.
- typedef struct inset {
- uint32_t encoding;
- uint32_t length;
- int8_t contents[];
- }intset;
encoding 属性: contents 数组的类型, 支持 INTESET_ENC_INT16,INTESET_ENC_INT32,INTESET_ENC_INT64.
length 属性: 存储整数集合中元素的个数.
contents 数组: 整数集合的底层实现, 集合中的每个元素在数组中都会按照值从小到大进行排序同时保证元素不会重复.
Contents 升级
当往数组中添加一个比当前数组类型还要大的元素时, 将要进行升级.
1. 根据新元素的类型对数组进行扩容( (length + 1) * 新类型大小)
2. 将数组中现有的元素都转换成与新元素相同的类型, 并将转换后的元素移动到正确的位置上.
3. 将新元素添加到数组中.
4. 修改 intset 中的 encoding 属性为新的类型.
Contents 降级
contents 数组不支持降级, 一旦为 contents 数组进行了升级那么就要一直保持升级后的状态.
4.7 HT 编码方式
4.8 SKIPLIST 编码方式
通过在每个节点中维持多个指向其他节点的指针, 从而达到快速访问节点的目的.
Redis 使用 zskiplistNode 结构来表示跳跃表中的节点.
- typedef struct zskiplistNode {
- struct zskiplistLevel {
- struct zskiplistNode *forward;
- unsigned int span;
- }level[];
- struct zskiplistNode *backward;
- double score;
- robj *obj;
- }zskiplistNode
level[]数组: 用于存储 zskiplistLevel, 每个 zskiplistLevel 都包含 forward 和 span 属性.
forward 属性为指向表尾方向的其他节点, span 属性则记录了 forward 指针所指向的节点距离当前节点的跨度(forward 指针遵循同层连接的原则)
backward 属性: 指向上一个节点的指针.
score 属性: 存储元素的分数.
obj 属性: 指向元素的指针(redisObject->sds)
每次创建一个新的跳跃表节点时, 会随机生成一个介于 1 到 32 之间的值作为 level 数组的大小.
Redis 使用 zskiplist 结构来持有 zskiplistNode
- typedef struct zskiplist {
- struct zskiplistNode *header,*tail;
- unsigned long length;
- int level;
- }zskiplist;
header 属性: 指向表头节点的指针.
tail 属性: 指向表尾节点的指针.
length 属性: 存储跳跃表中节点的个数, 不包括表头节点.
level 属性: 跳跃表中节点 level 的最大值, 不包括表头节点.
* 跳跃表中存在表头节点, 表头节点一共有 32 个 level, 即数组的大小为 32.
遍历 zskiplist 的流程
1. 通过 zskiplist 访问跳跃表中的头节点.
2. 从下一个节点最高的 level 开始往下遍历, 若下一个节点的最高 level 超过当前节点的最高 level, 则从当前节点最高的 level 开始往下遍历.
3. 当不存在下一个节点时, 遍历结束.
5.Redis 对象
Redis 各个对象支持的编码方式
5.1 字符串对象
字符串对象支持 INT,EMBSTR,ROW 三种编码方式
INT 编码方式
如果字符串的值是整数, 并且可以使用 long 来进行表示, 那么 Redis 将会使用 INT 编码方式.
INT 编码方式会将 RedisObject 中的 * ptr 指针直接改写成 long prt,prt 属性直接存储整数值.
EMBSTR 编码方式
如果字符串的值是字符, 并且其长度小于 32 个字节, 那么 Redis 将会使用 EMBSTR 编码方式.
ROW 编码方式
如果字符串的值是字符, 并且其长度大于 32 个字节, 那么 Redis 将会使用 ROW 编码方式.
*EMBSTR 和 ROW 编码方式在内存中都会创建一个 RedisObject 和 SDS, 区别在于 EMBSTR 编码方式中 RedisObject 和 SDS 共同使用同一块内存单元, Redis 内存分配器只需要分配一次内存, 而 ROW 编码方式中需要分别为 RedisObject 和 SDS 分配内存单元.
编码转换
如果字符串的值不再是整数或者用 long 无法进行表示, 那么 INT 编码方式将会转换成 ROW 编码方式.
如果字符串的值其长度大于 32 个字节, 那么 EMBSTR 编码方式将会转换成 ROW 编码方式.
*INT 编码方式和 EMBSTR 编码方式在满足条件的情况下, 将会转换成 ROW 编码方式.
*INT 编码方式不能转换成 embstr 编码方式.
字符串共享对象
Redis 在启动时会初始化值为 0~9999 的 SDS 作为共享对象, 当 set 一个 Key 其 Value 是在 0~9999 范围时, 会直接使用该共享对象, DictEntry 中的 Value 指针直接指向该共享 SDS 对应的 RedisObject.
在集群模式中, Redis 的每个节点启动时都会初始化值为 0~9999 的 SDS 作为共享对象.
在 RedisV4.0 以上, 使用 Object refcount 命令不再返回共享对象实际被引用的次数, 而是直接返回 Integer.MAX_VALUE.
5.2 列表对象
列表对象支持 ZIPLIST,LINKEDLIST 两种编码方式
ZIPLIST 编码方式
如果列表对象保存的所有元素的长度都小于 64 个字节同时元素的数量小于 512 个, 那么 Redis 将会使用 ZIPLIST 编码方式.
LINKEDLIST 编码方式
如果列表对象保存的元素的长度大于 64 个字节或元素的数量大于 512 个, 那么 Redis 将会使用 LINKEDLIST 编码方式.
编码转换
如果列表对象保存的元素的长度大于 64 个字节或元素的数量大于 512 个, 那么 Redis 将会使用 LINKEDLIST 编码方式.
可以通过 list-max-ziplist-value 和 list-max-ziplist-entries 参数调整列表对象 ZIPLIST 编码方式所允许保存的元素的最大值以及最多可以保存元素的数量.
5.3 哈希对象
哈希对象支持 ZIPLIST 和 HT 两种编码方式.
ZIPLIST 编码方式
如果哈希对象保存的所有键值对的键和值的字符串长度都小于 64 个字节同时键值对的数量小于 512 个, 那么 Redis 将会使用 ZIPLIST 编码方式.
HT 编码方式
如果哈希对象保存的键值对的键或值的字符串长度大于 64 个字节或键值对的数量大于 512 个, 那么 Redis 将会使用 HASHTABLE 编码方式.
编码转换
如果哈希对象保存的键值对的键或值的字符串长度大于 64 个字节或键值对的数量大于 512 个, 那么 Redis 将会使用 HASHTABLE 编码方式.
可以通过 hash-max-ziplist-value 和 hash-max-ziplist-entries 参数调整哈希对象 ZIPLIST 编码方式所允许保存的元素的最大值以及最多可以保存元素的数量.
5.4 集合对象
集合对象支持 INTSET 和 HT 两种编码方式
INTSET 编码方式
如果集合对象保存的所有元素都是整数同时元素的数量不超过 512 个, 那么 Redis 将会使用 INTSET 编码方式.
HT 编码方式
如果集合对象保存的元素并不是整数或元素的数量超过 512 个, 那么 Redis 将会使用 HASHTABLE 编码方式.
编码转换
如果集合对象保存的元素并不是整数或元素的数量超过 512 个, 那么 Redis 将会使用 HASHTABLE 编码方式.
可以通过 set-max-intset-entries 参数调整集合对象 INTSET 编码方式最多可以保存元素的数量.
5.5 有序集合对象
有序集合对象支持 ZIPLIST 和 SKIPLIST 两种编码方式.
ZIPLIST 编码方式
如果有序集合对象保存的所有元素的字符串长度都小于 64 个字节同时元素的数量不超过 128 个, 那么 Redis 将会使用 ZIPLIST 编码方式.
SKIPLIST 编码方式
如果有序集合对象保存的元素的字符串长度大于 64 个字节或元素的数量超过 128 个, 那么 Redis 将会使用 SKIPLIST 编码方式.
编码转换
如果有序集合对象保存的元素的字符串长度大于 64 个字节或元素的数量超过 128 个, 那么 Redis 将会使用 SKIPLIST 编码方式.
可以通过 zset-max-ziplist-value 和 zset-max-ziplist-entries 参数调整有序集合对象 ZIPLIST 编码方式所允许保存的元素的最大值以及最多可以保存元素的数量.
6.Redis 内存分配器
Redis 提供了 jemalloc,libc,tcmalloc 内存分配器, 默认使用 jemalloc, 需要在编译时指定.
Jemalloc 内存分配器
jemalloc 内存分配器将内存划分为小, 大, 巨大三个范围, 每个范围又包含多个大小不同的内存单元.
DictEntry,RedisObject 以及对象在初始化时, Redis 内存分配器都分配一个合适的内存大小.
如果频繁修改 Value, 且 Value 的值相差很大, 那么 Redis 内存分配器需要重新为对象分配内存, 然后释放掉对象之前所占用的内存(编码转换或者数组越界)
7.Redis 内存监控
可以使用 info memory 命令查看 Redis 内存的使用情况
used_memory:Redis 有效数据占用的内存大小(包括使用的虚拟内存)
uesd_memory_rss:Redis 有效数据占用的内存大小(不包括使用的虚拟内存),Redis 进程所占用的内存大小, 内存碎片(与 TOP 命令查看的内存一直)
mem_fragmentation_ratio(内存碎片率) = used_memory_rss / used_memory
mem_allocator:Redis 内存分配器, 可选 jemalloc(默认),libc,tcmalloc
*max_memory 配置的是 Redis 有效数据最大可使用的内存大小, 不包括内存碎片, 因此 Redis 实际占用的内存大小最终一定会比 max_memory 要大.
内存碎片率
1. 当内存碎片率 <1 时, 表示 Redis 正在使用虚拟内存.
2. 当内存碎片率严重> 1, 表示 Redis 存在大量的内存碎片.
* 内存碎片率在 1~1.1 之间是比较健康的状态.
有可能产生内存碎片的操作: 频繁更新 Value 且 Value 的值相差很大(重新为对象分配内存, 释放之前的内存),Redis 的内存淘汰机制.
产生内存碎片的根本原因: Redis 释放的内存无法被操作系统所回收.
解决内存碎片的方法
1. 重启 Redis 服务, 会重新读取 RDB 文件进行数据的恢复, 重新为对象分配内存.
2.Redis4.0 提供了清除内存碎片的功能
- # 运行期自动清除
- activedefrag yes
- # 手动执行命令清除
- memory purge
8.Redis 监视器
客户端向服务器发送命令请求时, 服务器除了会执行相应的命令以外, 还会将关于这条命令请求的信息转发给所有的监视器.
通过执行 monitor 命令, 客户端可以将自己变成一个监视器, 实时接收服务器当前正在执行的命令请求的相关信息.
来源: https://www.cnblogs.com/funyoung/p/11378837.html