1、Redis 对象
字列集哈有
1、字符串 String
1.1、使用
String 就是字符串,它是 Redis 中最基本的数据对象,最大为 512 MB,我们可以通过配置项 proto-max-buk-len 来修改它,一般来说是不用主动修改的
SET key value
SETNX -- Not Exist
EX -- Expiration
PX -- Millisecond
NX -- Not Exist
XX -- Exist
GET key
MGET key1 key2
DEL key
1.2、编码方式
三种编码方式:INT、EMBSTR、RAW
INT 编码:就是存一个整型,可以用 long 表示的整数就以这种编码存储
EMBSTR 编码:如果字符串小于等于阈值字节(39 / 44 字节),使用 EMBSTR 编码
RAW 编码:字符串大于阈值字节,则用 RAW 编码
EMBSTR 和 RAW 都是由 redisobject 和 SDS 两个结构组成
它们的差异在于:EMBSTR 下 redisObject 和 SDS 是连续的内存,RAW 编码下 redisObject 和 SDS 的内存是分开的
EMBSTR 优点是 redisObject 和 SDS 两个结构可以一次性分配空间,缺点在于如果重新分配空间,整体都需要再分配
所以 EMBSTR 设计为只读,任何写操作之后 EMBSTR 都会变成 RAW,理念是发生过修改的字符串通常会认为是易变的
随着我们的操作,编码可能会转换
- INT -> RAW:当存的内容不再是整数,或者大小超过了 long 的时候
- EMBSTR -> RAW:任何写操作之后 EMBSTR 都会变成 RAW,原因前面有解释
1.3、SDS
SDS:它是 Simple Dynamic String 的缩写,即简单动态字符串
在 C 语言中,字符串用一个 '\0' 结尾的 char 数组表示,比如 "hello" 即 "hello\0"
- 每次计算字符串长度的复杂度为 O(N)
- 对字符串进行追加,需要重新分配内存
- 非二进制安全
在 Redis 内部,字符串的追加和长度计算很常见,这两个简单的操作不应该成为性能的瓶颈
于是 Redis 封装了一个叫 SDS 的字符串结构,用来解决上述问题,下面我们来看看它是怎样的结构
Redis 中 SDS 分为 sdshdr8、sdshdr16、sdshdr32、sdshdre64,它们的字段属性都是一样,区别在于应对不同大小的字符串,我们以 sdshdr8 为例
其中有两个关键字段,一个是 len(表示使用了多少),一个是 alloc(表示一共分配了多少内存)
这两个字段的差值(alloc - len)就是预留空间的大小,flags 则是标记是哪个分类,比如 sdshdr8 的分类就是 #define SDS_TYPE_8 1
// from Redis 7.0.8
struct __attribute__((__packed__)) sdshdr8 {
uint8_t len;
uint8_t alloc;
unsigned char flags;
char buf[];
};
- 增加长度字段 len,快速返回长度
- 增加空余空间(alloc - len),为后续追加数据留余地,预留空间大小为 min(len, 1M)
- 不再以 '\0' 作为判断标准,二进制安全
2、列表 List
2.1、使用
List 是一组连接起来的字符串集合,最大元素个数是 2 ^ 32 - 1(4,294,967,295),新版本已经是 2 ^ 64 - 1 了
LPUSH key value
RPUSH key value
LPOP key
RPOP key
LREM key count value
count = 0:移除所有
count > 0:从左往右
count < 0:从右往左
LLEN key
LRANGE key start stop
起始索引为 0,负数表示倒数,最后一个元素为 -1
DEL key
UNLINK key
编码方式:ZIPLIST、LINKEDLIST、QUICKLIST、LISTPACK
ZIPLIST:压缩列表,列表对象保存的所有字符串对象长度都小于 64 字节、列表对象元素个数少于 512 个(节约内存、局部性好)
LINKEDLIST:链表,在列表个数或节点数据长度比较大的时候使用(牺牲内存,加快添加和删除性能)
QUICKLIST:ZIPLIST 和 LINKEDLIST 的结合体,由压缩列表组成的双向链表(Redis7.0 后 ZIPLIST 优化为 LISTPACK)
2.2、ZIPLIST
ZIPLIST 结构
结构头(zlbytes、zltail、zllen)+ 数据部分(entry)+ 结尾标识(zlend)
- zlbytes:表示该 ZIPLIST 一共占了多少字节数,这个数字是包含 zlbytes 本身占据的字节的
- zltail:ZIPLIST 尾巴节点相对于 ZIPLIST 的开头(起始指针),偏移的字节数,通过这个字段可以快速定位到尾部节点
现在有一个 ZIPLIST,zl 指向它的开头,如果要获取 tail 尾巴节点,即 ZIPLIST 里的最后一个节点:可以 zl + zltail 定位到它,如果没有尾节点就定位到 zlend - zllen:表示有多少个数据节点(2 byte 65535),在本例中就有 3 个节点
- entry1 ~ entry3:表示压缩列表数据节点
- zlend:一个特殊的 entry 节点,值为 255(1111 1111),表示 ZIPLIST 的结束
ENTRY 结构
<prevlen> <encoding> <entry-data>
- prevlen:表示上一个节点的数据长度,通过这个字段可以定位上一个节点的起始地址(或者说开头)
p - prevlen 可以跳到前一个节点的开头位置,实现从后往前操作,所以压缩列表才可以从后往前遍历
如果前一节点的长度 < 254 字节,那么 prevlen 属性需要用 1 字节长的空间来保存这个长度值,255 是特殊字符,被 zlend 使用了
如果前一节点的长度 >= 254 字节,那么 prevlen 属性需要用 5 字节长的空间来保存这个长度值
注意 5 个字节中第一个字节为 11111110,也就是 254,标志这是个 5 字节的 prevlen 信息,剩下 4 字节来表示大小 - encoding:编码类型,编码类型里还包含了一个 entry 的长度信息,可用于正向遍历
如果是 String 类型,那么 encoding 有两部分,前几位标识类型、后几位标识长度(int 具体类型自带了大小,比如 int32) - entry-data:实际的数据
2.3、连锁更新
2.4、LISTPACK
ENTRY 结构
<encoding-type> <element-data> <element-tot-len>
- encoding-type 是编码类型
- element-data 是数据内容
- element-tot-len 存储整个节点除它自身之外的长度
element-tot-len 所占用的每个字节的第一个 bit 用于标识是否结束,0 是结束、1 是继续,剩下 7 个 bit 来存储数据大小
当我们需要找到当前元素的上一个元素时,我们可以从后向前依次查找每个字节,找到上一个 Entry 的 element-tot-len 字段的结束标识,就可以算出上一个节点的首位置了
3、集合 Set
3.1、使用
Redis 的 Set 是一个不重复、无序的字符串集合
如果是 INTSET,编码的时候其实是有序的,不过一般不应该依赖这个,整体还是看成无序来用比较好
SADD key member ...
SREM key member ...
SISMEMBER key member -- 查询元素是否存在
SCARD key -- 查询集合元素个数
SMEMBERS key -- 查看集合所有元素
SSCAN key cursor [MATCH pattern][COUNT count] -- 查看集合元素,可以理解为指定游标进行查询,可以指定个数,默认个数为 10
SINTER key [key ...] -- 返回在第一个集合里,同时在后面所有集合都存在的元素
SUNION key [key ...] -- 返回所有集合的并集,集合个数大于等于 2
SDIFF key [key ...] -- 返回第一个集合有,且在后续集合中不存在的元素,集合个数大于等于 2(以第一个集合和后面比,看第一个集合多了哪些元素)
3.2、编码方式
两种编码方式:INTSET、HASHTABLE
Redis 出于性能和内存的综合考虑,也支特两种编码方式
- 如果集合元素都是整数,且元素数量不超过 512 个,就可以用 INTSET 编码(节约内存)
结构如下图,可以看到 INTSET 排列比较紧凑,内存占用少,但是查询时需要二分查找 - 如果不满足 INTSET 的条件,就需要用 HASHTABLE(内存占用大、查找速度更快)
HASHTABLE 结构如下图,可以看到 HASHTABLE 查询一个元素的性能很高,O(1) 时间就能找到一个元素是否存在
4、哈希表 Hash
4.1、使用
Redis Hash 是一个 field、value 都为 string 的 hash 表,存储在 Redis 的内存中
Redis 中每个 hash 可以存储 2 ^ 32 - 1 键值对(40 多亿)
HSET key field value -- 为集合对应 field 设置 value 数据
HSETNX key field value -- 如果 field 不存在,则为集合对应 field 设置 value 数据
HDEL key field [field ...] -- 删除指定 field,可以一次删除多个
HGETALL key -- 查找全部数据
HGET key field -- 查找某个 key
HLEN key -- 查找 Hash 中元素总数
HSCAN key cursor [MATCH pattern][COUNT count]
-- 从指定位置查询一定数量的数据
-- 如果是小数据量下,处于 ZIPLIST 时,COUNT 不管填多少都是返回全部,因为 ZIPLIST 本身就用于小集合,没必要说再切分成几段来返回
4.2、编码方式
两种编码方式:ZIPLIST、HASHTABLE
- ZIPLIST:Hash 对象保存的所有值和键的长度都小于 64 字节、Hash 对象元素个数少于 512 个
- HASHTABLE:不满足 ZIPLIST 时
5、HASHTABLE
5.1、结构
// from Redis 5.0.5
// This is our hash table structure
typedef struct dictht {
dictEntry **table;
unsigned long size;
unsigned long sizemask;
unsigned long used;
} dictht;
最外层是一个封装的 dictht 结构,其中字段含义如下
- table:指向实际 hash 存储,可以看做一个数组,所以是 *table 的表示,在 C 语言中 *table 可以表示一个数组
- size:哈希表大小,就是 dictEntry 有多少元素空间
- sizemask:哈希表大小的掩码表示,总是等于 size - 1
这个属性和哈希值一起决定 "一个键应该被放到 table 数组的哪个索引上面",规则 Index = hash & sizemask - used:表示已经使用的节点数量,通过这个字段可以很方便地查询到目前 HASHTABLE 元素总量
5.2、渐进式扩容
渐进式扩容顾名思义就是一点一点慢慢扩容,而不是一股脑直接做完(操作时顺带迁移)
为了实现渐进式扩容,Redis 中没有直接把 dictht 暴露给上层,而是再封装了一层
可以看到 dict 结构里面,包含了 2 个 dictht 结构,也就是 2 个 HASHTABLE 结构,dictEntry 是链表结构,也就是用拉涟法解决 Hash 冲突,用的是头插法
//from Redis 5.0.5
typedef struct dict {
dictType *type;
void *privdata;
dictht ht[2];
long rehashidx; // rehashing not in progress if rehashidx == -1
unsigned long iterators; // number of iterators currently running
} dict;
平常使用的只有一个 HASHTABLE,在触发扩容之后,两个 HASHTABLE 同时使用:当向字典添加元素时,发现需要扩容就会进行 Rehash,Rehash 的流程大概分成三步
- 为新 Hash 表 ht[1] 分配空间,新表大小为第一个 >= 原表 2 倍 used 的 2 次方幂
原表如果 used = 500,2 倍就是 1000,那第一个 >= 1000 的 2 次方幂则为 1024
此时字典同时持有 ht[0] 和 ht[1] 两个哈希表,字典的偏移索引从静默状态 -1 设置为 0,表示 Rehash 工作正式开始 - 迁移 ht[0] 数据到 ht[1]
在 Rehash 进行期间,每次对字典执行增删查改操作,程序会顺带迁移当前 rehashidx 在 ht[0] 上的对应的数据,并更新偏移索引
与此同时,部分情况周期函数也会进行迁移,如果 rehashidx 刚好在一个已删除的空位置上,会尝试往下找(有上限) - 在 Rehash 过程中:查询先 0 后 1、新增在 1、删除和更新先 0 后 1
5.3、扩容时机
Redis 提出了一个负载因子的概念,负载因子表示目前 Redis HASHTABLE 的负载情况,是游刃有余,还是不堪重负了
我们设负载因子为 k,那么 k = ht[0].used / ht[0].size,也就是使用空间和总空间大小的比例,Redis 会根据负载因子的情况来扩容
- 负载因子 >= 1,说明此时空间已经非常紧张
新数据是在链表上叠加的,越来越多的数据其实无法在 O(1) 时间复杂度找到,还需要遍历一次链表
如果此时服务器没有执行 BGSAVE 或 BGREWRITEAOF 这两个命令,就会发生扩容(复制命令对 Redis 的影响我们后面再讲) - 负载因子 >= 5,这时候说明 HASHTABLE 真的已经不堪重负了,此时即使是有复制命令在进行,也要进行 Rehash 扩容
5.4、缩容
扩容是数据太多存不下了,那如果太富裕呢,比如原来可能数据较多,发生了扩容,但后面数据不再需要,被删除了,此时多余的空间就是一种浪费
缩容过程其实和扩容是相似的,也是渐进式缩容,这里不整述
Redis 还是用负载因子来控制什么时候缩容
当负载因子 < 0.1,即负载率小于 10%,此时进行缩容,新表大小为第一个 >= 原表 usd 的 2 次方幂
如果有 BGSAVE 或 BGREWRITEAOF 这两个复制命令,缩容也会受影响,不会进行
6、有序集合 ZSet
6.1、使用
ZSet 是有序集合,也叫 Sorted Set,是一组关联积分有序的字符串集合,这里的分数是个抽象概念,任何指标都可以抽象为分数,以满足不同场景
ZADD key score member [score member ...] -- 向 ZSet 增加数据,如果是已经存在的 Key,则更新对应的数据
XX:仅更新存在的成员,不添加新成员
NX:不更新存在的成员,只添加新成员
LT:更新新的分值比当前分值小的成员,不存在则新增
GT:更新新的分值比当前分值大的成员,不存在则新增
ZREM key member [member ...] -- 删除 ZSet 中的元素
ZCARD key -- 查看 ZSet 中成员总数
ZSCORE key member -- 查询 ZSet 中成员的分数
ZCOUNT key min max -- 计算 min - max 积分范围的成员个数
ZRANK key member -- 查看 ZSet 中 member 的排名索引,索引是从 0 开始,所以如果排第一,索引就是 0
ZRANGE key start stop [WITHSCORES] -- 查询 [start,stop] 范围的数据,WITHSCORES 选填,不填的话,输出里就只有 key,没有 score 值
ZREVRANGE key start stop [WITHSCORES] -- 即 reverse range,从大到小遍历,WITHSCORES 选填
6.2、编码方式
两种编码方式:ZIPLIST、SKIPLIST + HASHTABLE
ZIPLIST:列表对象保存的所有字符串对象长度都小于 64 字节、列表对象元素个数少于 128 个
SKIPLIST + HASHTABLE:不满足 ZIPLIST 时
7、跳表
标准的跳表:值不能重复、只有后指针没有前指针
Redis 跳表:值可以重复,有前后指针
- 跳表也算链表,不过相对普通链表,增加了多级索引,通过索引可以实现 O(logN) 的元素查找效率
- 从高级索往后查找,如果下个节点的数值比目标节点小则继续找,否则不跳过去,而是用下级索引往下找
- Redis 跳表决定每一个节点,是否能增加一层的概率为 25%
最大层数限制在 Redis 5.0 是 64 层,在 Redis 7.0 是 32 层
跳表插入数据不会影响其它节点的层高,只会影响每一层前一跳、后一跳的关联指针
typedef struct zskiplist {
struct zskiplistNode *header, *tail;
unsigned long length; // 节点数量
int level;
} zskiplist;
// from Redis 7.0.8
// ZSETs use a specialized version of Skiplists
typedef struct zskiplistNode {
sds ele;
double score;
struct zskiplistNode *backward;
struct zskiplistLevel {
struct zskiplistNode *forward;
unsigned long span;
} level[];
} zskiplistNode;
8、更多
8.1、对象过期时间
SET key value EX seconds -- 设置多少秒之后过期
SET key value PX milliseconds -- 设置多少毫秒之后过期
EXPIRE key seconds -- 设置一个 key 的过期时间,单位秒
PEXPIRE key milliseconds - -设置一个 key 的过期时间,单位毫秒
TTL key -- 查看还有多久过期
过期之后的键实际上不是立刻删除的,一般过期键清除策略有三种:定时删除、定期删除、惰性删除
- 定时删除:在设置键的过期时间的同时,创建一个定时器,让定时器在键的过期时间来临时,立即执行对键的删除操作
定时删除对内存比较友好,但是对 CPU 不友好,如果某个时间段比较多的 Key 过期,可能会影响命令处理性能 - 惰性删除:使用的时候发现 Key 过期了,此时再进行删除,这个策略的思路是对应用而言,只要不访问,过期不过期业务都无所谓
但是这样的代价就是如果某些 Key 一直不来访问,那本该过期的 Key 就变成常驻的 Key,这种策略对 CPU 最友好,对内存不太友好 - 定期删除:每隔一段时间,程序就对数据库进行一次检查,每次删除一部分过期键,这属于一种渐进式兜底策略
8.2、对象引用计数
typedef struct redisobject {
unsigned type: 4;
unsigned encoding: 4;
void *ptr;
int refcount;
unsigned lru: LRU_BITS; /* LRU time (relative to global lru clock) or
* LFU data (least significant 8 bits frequency
* and most significant 16 bits access time). */
} robj;
Redis 会在初始化服务器时,会创建 10000 个数值从 0 到 9999 的字符串对象
当服务器、新创建的键需要用到值为 0 到 9999 的字符串对象时,服务器就会使用这些共享对象,而不是新创建对象
- 0 ~ 9999 的整数,被使用的几率是很大的,复用是有场景的
- 整数存储空间比较小,而每个 redisObject 内部结构至少占 16 字节,这比整数本身占据的空间还大,频繁分配整数是比较大的开销
- 要复用对象,就需要进行数值比较,而整数对象进行比较的成本最低,如果是其它字符串,需要遍历字符串的所有字符,而其它如 List、ZSet 的对比成本就更高了
SET num 100
OBJECT REFCOUNT num
(integer) 2147483647
0 ~ 9999 这个范围的数字,都是共享对象,同时引用计数会保持在 INT_MAX
本文来自博客园,作者:lidongdongdong~,转载请注明原文链接:https://www.cnblogs.com/lidong422339/p/17741143.html