1.Redis对象头
Redis中每个对象都由一个RedisObject的结构表示:
struct RedisObject{
int4 type;//数据类型
int4 encoding;//
int24 lru;//lru信息
int32 refcount;//引用计数器
void *ptr;//指向实现当前数据类型的数据结构首地址
}robj
type记录当前对象的类型:
define OBJ_STRING 0 /*字符串对象*/
define OBJ_LIST 1 /*列表对象*/
define OBJ_SET 2 /*集合对象*/
define OBJ_ZSET 3 /*有序集合对象*/
define OBJ_HASH 4 /*哈希对象*/
encoding记录当前对象的底层数据结构:
define OBJ_ENCODING_RAW 0 /* SDS 字符串 */
define OBJ_ENCODING_INT 1 /* 整数 */
define OBJ_ENCODING_HT 2 /* 字典结构 */
define OBJ_ENCODING_ZIPMAP 3 /* 压缩map,已经废弃 */
define OBJ_ENCODING_LINKEDLIST 4 /* LinkedList 双端链表,废弃了 */
define OBJ_ENCODING_ZIPLIST 5 /* 压缩列表 */
define OBJ_ENCODING_INTSET 6 /* 整数集合 */
define OBJ_ENCODING_SKIPLIST 7 /* 跳跃表 */
define OBJ_ENCODING_EMBSTR 8 /* 短字符串 */
define OBJ_ENCODING_QUICKLIST 9 /* 压缩链表和双向链表组成的快速列表 */
refcount属性是一个引用计数属性,可以用于内存回收和对象共享 lru属性,记录了对象最后一次被命令程序访问的时间,可以计算出某个键的空转时长;
2.字符串对象
字符串:SDS(Simple Dynamic String)动态字符串
struct SDS<T>{
T capacity;//数组容量
T len;//实际长度
byte flags//标志位
byte[] content;//实际数据
}
obj encoding int
如果字符串为int类型,会将*ptr(对象头指针)转为long型,存储value,继而分配内存.
embstr & raw
embstr和raw都是基于SDS结构来表示,embstr只会调用一次内存分配并分配一块连续的内存空间,raw会调用两次内存分配来创建RedisObject和SDS空间。 字符串比较短是采用embstr形式存储,当超过44字节采用raw形式存储
44字节的原因:jemalloc、tcmalloc等分配内存大小的单位都是2/4/8/16/32/64,RedisObject为16字节,SDS3个字节、结束符1个字节,字符串对象超过64字节就会转为raw的形式,因此一个最大的embstr字符串为64-(16+3+1) = 44字节
EMBSTR是不可修改的,当对EMBSTR编码的字符串append,总会先将其转换成RAW编码再进行修改, int->string 同样转为raw的存储形式 注:Redis 规定了字符串的长度不得超过 512 MB,在字符串长度小于1MB之前,扩容空间采用加倍策略,超过1MB之后,为了避免加倍后的冗余空间过大而导致浪费,每次扩容只会多分配1MB的空间
3.List对象
List对象得编码可以是OBJ_ENCODING_LINKEDLIST和OBJ_ENCODING_ZIPLIST及OBJ_ENCODING_QUICKLIST,其中OBJ_ENCODING_LINKEDLIST基本已经被QUICKLSIT取代了。 使用zipList作为底层结构,对象头ptr指针则会执行一个zipList数据结构,则每个压缩列表节点(entry)保存了一个列表元素。 使用LinkedList作为底层实现,每个双端链表节点(node)都保存了一个字符串对象,而每个字符串对象都保存了一个列表元素。
3.1 编码转换
当列表对象可以同时满足以下两个条件时,列表对象使用ziplist编码:
- 列表对象保存的所有字符串元素的长度都小于64字节;
- 列表对象保存的元素数量小于512个;不能满足这两个条件的列表对象需要使用linkedlist编码。当满足一定条件底层数据结构会进行转换,
以上两个条件都可以通过参数配置:
list-max-ziplist-value 512 #对象保存的元素数量512个 list-max-ziplist-entries 64 #对象保存的所有字符串元素的长度都64字节
3.2版本后quickList结构取代了linkedList,将多个linkedList切割成一个zipList加上pre和next组成quickNode,如果zipList元素太小,quickNode节点就会增多,内存碎片就会增多,如果过大连续分配较大内存也会影响效率,Redis提供了参数进行配置zipList的大小:
list-max-ziplist-size -2
参数的含义解释,取正值时表示quicklist节点ziplist包含的数据项。取负值表示按照占用字节来限定quicklist节点ziplist的长度。
-5: 每个quicklist节点上的ziplist大小不能超过64 Kb。
-4: 每个quicklist节点上的ziplist大小不能超过32 Kb。
-3: 每个quicklist节点上的ziplist大小不能超过16 Kb。
-2: 每个quicklist节点上的ziplist大小不能超过8 Kb。(默认值)
-1: 每个quicklist节点上的ziplist大小不能超过4 Kb。
list-compress-depth 0 #压缩深度
0: 是个特殊值,表示都不压缩。这是Redis的默认值。
1: 表示quicklist两端各有1个节点不压缩,中间的节点压缩。
2: 表示quicklist两端各有2个节点不压缩,中间的节点压缩。
4.Hash对象
哈希对象的编码可以是ziplist或者hashtable。 ziplist编码的哈希对象使用压缩列表作为底层实现,每当有新的键值对插入,程序先把由key组成的zipList的节点插入到压缩列表的队尾,然后再把value组成zipList节点插入到压缩列表的队尾,所以具有如下特点:
hash键值对总是存储两个zipList节点挨着存储;
zipList列表总是按时间先后顺序依次存储hash的键值对;
hashTable编码的哈希对象使用字典作为底层实现,每个键值对都是用dict键值对存储;
字典的每个键值对都是一个字符串对象结构;
4.1 编码转换
当哈希对象可以同时满足以下两个条件时,哈希对象使用ziplist编码:
- 哈希对象保存的所有键值对的键和值的字符串长度都小于64字节;
- 哈希对象保存的键值对数量小于512个;
不能满足这两个条件的哈希对象需要使用hashtable编码,以上两个条件都可以通过参数配置:
hash-max-ziplist-value 64 #所有键值对的键和值的字符串长度都64字节; hash-max-ziplist-entries 512 #的键值对数量512个
5.Set对象
集合对象的编码可以是intset或者hashtable。 当Set对象存储的全是整数,会使用整数集合作为底层实现,集合对象包含的所有元素都被保存在整数集合里面。 hashtable编码的集合对象使用字典作为底层实现,字典的每个键都是一个字符串对象,每个字符串对象包含了一个集合元素,而字典的值则全部被设置为NULL。
5.1 编码转换
当集合对象可以同时满足以下两个条件时,对象使用intset编码:
- 集合对象保存的所有元素都是整数值;
- 集合对象保存的元素数量不超过512个。
不能满足这两个条件的集合对象需要使用hashtable编码,可通过参数设置整数据集合元素数量:
set-max-intset-entries 512 #Intset集合保存的元素数量超过512,转成hashTable编码。
6.ZSet对象
有序集合的编码可以是ziplist或者skiplist。 ziplist编码的哈希对象使用压缩列表作为底层实现,当插入zset元素是,会生成元素成员(member)和score两个zipListNode节点紧挨着存储; 压缩列表内的集合元素按分值从小到大进行排序,分值较小的元素被放置在靠近表头的方向,而分值较大的元素则被放置在靠近表尾的方向。 skiplist编码的哈希对象使用跳跃列表作为底层实现,skiplist编码的有序集合对象使用zset结构作为底层实现,一个zset结构同时包含一个字典和一个跳跃表:
struct zset{
skipList *zsl;
dict *dict
}zset
zsl跳跃表按分值从小到大保存了所有集合元素,每个跳跃表节点都保存了一个集合元素:跳跃表节点的object属性保存了元素的成员,而跳跃表节点的score属性则保存了元素的分值。通过这个跳跃表,程序可以对有序集合进行范围型操作,比如ZRANK、ZRANGE等命令就是基于跳跃表API来实现的。
zset结构中的dict字典为有序集合创建了一个从成员到分值的映射,字典中的每个键值对都保存了一个集合元素:字典的键保存了元素的成员,而字典的值则保存了元素的分值。通过这个字典,程序可以用O(1)复杂度查找给定成员的分值,ZSCORE命令就是根据这一特性实现的,而很多其他有序集合命令都在实现的内部用到了这一特性。
6.1 编码转换
当有序集合对象可以同时满足以下两个条件时,对象使用ziplist编码:
- 有序集合保存的元素数量小于128个
- 有序集合保存的所有元素成员的长度都小于64字节;
以上两个条件都可以通过参数配置:
zset-max-ziplist-entries 128 #集合保存的元素数量小于128个
zset-max-ziplist-value 64 #所有元素成员的长度都小于64字节
7.键类型检查
Redis的指令一般分为两类:
针对所有类型的键执行的指令,如expire 、del等
针对特定类型的键执行的指令,如lpush、set、hset等
类型特定命令所进行的类型检查是通过redisObject结构的type属性来实现的,在执行一个键值对的插入时候,先会检查键对象的type类型是否是执行命令的类型,如果是的话执行指令,否则返回错误类型提示客户端。 除了会检查对象的类型,同时还会根据type选择执行对应的方法,如del指令,如果是zipList的编码,怎会执行zipist编码对应的删除实现。
8.内存回收
8.1 引用计数
Redis在自己的对象系统中构建了一个引用计数(reference counting)技术实现的内存回收机制,通过这一机制,程序可以通过跟踪对象的引用计数信息,在适当的时候自动释放对象并进行内存回收。 对象的引用计数信息会随着对象的使用状态而不断变化:
- 在创建一个新对象时,引用计数的值会被初始化为1;
- 当对象被一个新程序使用时,它的引用计数值会被增一;
- 当对象不再被一个程序使用时,它的引用计数值会被减一;
- 当对象的引用计数值变为0时,对象所占用的内存会被释放。
8.2 LRU & LFU
在Redis当内存使用达到限制时,系统默认会根据一定的规则自动清理旧数据,Redis默认采用近似LRU(Least Recently Used)算法进行缓存淘汰。
8.2.1 最大内存设置
maxmemory 配置选项使用来配置 Redis 的存储数据所能使用的最大内存限制。可以通过在内置文件redis.conf中配置,也可在Redis运行时通过命令CONFIG SET来配置
maxmemory 0 #表示没有内存限制
当存储数据达到限制时,Redis 会根据情形选择不同策略,或者返回errors(这样会导致浪费更多的内存),或者清除一些旧数据回收内存来添加新数据。
8.2.2 内存回收策略
当内存达到限制时,Redis 具体的回收策略是通过 maxmemory-policy 配置项配置的:
- noenviction #不清除数据,只是返回错误,这样会导致浪费掉更多的内存,对大多数写命令(DEL 命令和其他的少数命令例外)默认
- allkeys-lru #从所有的数据集(server.db[i].dict)中挑选最近最少使用的数据淘汰,以供新数据使用
- volatile-lru #从已设置过期时间的数据集(server.db[i].expires)中挑选最近最少使用的数据淘汰,以供新数据使用
- allkeys-random #从所有数据集(server.db[i].dict)中任意选择数据淘汰,以供新数据使用
- volatile-random #从已设置过期时间的数据集(server.db[i].expires)中任意选择数据淘汰,以供新数据使用
- volatile-ttl #从已设置过期时间的数据集(server.db[i].expires)中挑选将要过期的数据淘汰,以供新数据使用
对LRF新增以下两个配置:
- volatile-lfu #从所有配置了过期时间的键中驱逐使用频率最少的键
- allkeys-lfu #从所有键中驱逐使用频率最少的键
8.2.3 空转时长
Redis对象头存储了Lru字段,初始值为server.lrulock,server.lrulock默认是Unix时间戳对2的 24次方进行取模,server.lrulock在定时任务serverCron里主动设置,每100ms更新一次,server.lrulock与lru的差就是空转时长,每次对象更新则会更新lru为server.lrulock。 OBJECT IDLETIME命令可以打印出给定键的空转时长;
8.2.4 近似LRU算法
Redis中的LRU与常规的LRU实现并不相同,常规LRU会准确的淘汰掉队头的元素,但是Redis的LRU并不维护队列,只是根据配置的策略要么从所有的key中随机选择N个(N可以配置)要么从所有的设置了过期时间的key中选出N个键,然后再从这N个键中选出空转时间最长的一个key进行淘汰。 随机个数设置:
maxmemory-samples 5 #默认5个元素
Redis3.0之后又改善了算法的性能,会提供一个待淘汰候选key的pool,里面默认有maxmemory-samples个key,按照空闲时间排好序。更新时从Redis键空间随机选择N个key,分别计算它们的空闲时间idle,key只会在pool不满或者空闲时间大于pool里最小的时,才会进入pool,然后从pool中选择空闲时间最大的key淘汰掉。
8.2.5 LFU算法
在4.0之后支持配置缓存淘汰使用LFU(Least Frequently Used, 最少使用算法) Redis中实现LFU算法的时候,有这个两个重要的可配置参数:
lfu-log-factor 10 #能够影响计数的量级范围;
lfu-decay-time 1 #控制LFU计数衰减的参数,衰减因子
在RedisObject对象头中还是采用lru字段来记录LFU存储的两个值 ldt和logc:
logc:用前8位存储访问计数,初始计数值会直接就是LFU_INIT_VAL(5),8个bit位最大为255,从Redis通过一个复杂的公式,通过配置lfu_log_factor和server.lfu_decay_time两个参数来调整数据的递增速度,不同的lfu_log_factor的值能够控制计数代表的量级的范围,当factor为100时,能够最大代表10M,也就是千万级别的命中数。 ldt:16个bit存储上次访问时间,因为只有16个bit,只精确到分钟,通过该参数可计算出空转时间
计数衰减
logc的更新:在缓存被访问时,会更新数据的访问计数,更新的步骤是:先在现有数据的计数上进行计数衰减,再对完成衰减后的计数进行增加。
ldt的更新:在Redis的淘汰逻辑进行时进行更新,淘汰逻辑只会是在内存达到maxmemory设置时才触发,和LRU一样,随机挑选n个key,淘汰热度最低的key,然后更新ldt值为空转时间/lfu-decay-time,在更新ldt时,同样也会对logc进行衰减,logc-空转时间/lfu-decay-time
8.2 过期缓存淘汰
redisDb结构的expires字典保存了数据库中所有键的过期时间,我们称这个字典为过期字典
struct redisDb{
dict* expires;
}
过期字典的键是一个指针,这个指针指向键空间中的某个键对象(也即是某个数据库键),过期字典的值是一个long类型的整数,这个整数保存了键所指向的数据库键的过期时间——一个毫秒精度的UNIX时间戳。 Redis采用以下两种方式进行过期数据淘汰:
- 惰性删除:在每个访问数据时,会进行过期时间判断,若到期则删除,同时向用户返回nil;
- 定时删除:Redis配置项hz定义了serverCron任务的执行周期,默认为10,即CPU空闲时每秒执行10次,每次过期key清理的时间不超过CPU时间的25%,即若hz=1,则一次清理时间最大为250ms,若hz=10,则一次清理时间最大为25ms,每次随机取20个key判断过期,是则清除,如果超过5个key过期则重复随机选取key淘汰;
由于算法采用的随机取key判断是否过期的方式,故几乎不可能清理完所有的过期Key; 调高hz参数可以提升清理的频率,过期key可以更及时的被删除,但hz太高会增加CPU时间的消耗,为了保证不会循环过度,导致卡顿,扫描时间上限默认不超过25ms。 系统中应避免大量的key同时过期,给要过期的key设置一个随机范围,避免最大清除时间;
9.总结
- Redis数据库中的每个键值对的键和值都是一个对象。
- Redis共有字符串、列表、哈希、集合、有序集合五种类型的对象,每种类型的对象至少都有两种或以上的编码方式,不同的编码可以在不同的使用场景上优化对象的使用效率。
- 服务器在执行某些命令之前,会先检查给定键的类型能否执行指定的命令,而检查一个键的类型就是检查键的值对象的类型
- 在每个对象头都会有引用计数及Lru信息,用于回收内存