数据库抽象 redisDb
redisDb 中 dict 存储 key-value,expires 存储 key-ttl。
typedef struct redisDb {
dict *dict; /* 存放所有key及value的地方,也被称为keyspace*/
dict *expires; /* 存放每一个key及其对应的TTL存活时间,只包含设置了TTL的key*/
dict *blocking_keys; /* Keys with clients waiting for data (BLPOP)*/
dict *ready_keys; /* Blocked keys that received a PUSH */
dict *watched_keys; /* WATCHED keys for MULTI/EXEC CAS */
int id; /* Database ID,0~15 */
long long avg_ttl; /* 记录平均TTL时长 */
unsigned long expires_cursor; /* expire检查时在dict中抽样的索引位置. */
list *defrag_later; /* 等待碎片整理的key列表. */
} redisDb;
内存分配
Redis 基于内存存储数据,所有数据存储在 redisDb 结构体的 dict 中,key 固定为 string 类型,value 类型可选,内存分配取决于数据类型具体实现。
内存使用限制
Redis之所以性能强,最主要的原因就是基于内存存储。然而单节点的Redis其内存大小不宜过大,会影响持久化或主从同步性能。
我们可以通过修改配置文件来设置Redis的最大内存:
# 格式:
# maxmemory <bytes>
# 例如:
maxmemory 1gb
当内存使用达到上限时,就无法存储更多数据了。为了解决这个问题,Redis提供了一些策略实现内存回收:
内存回收
Redis 内存回收发生在两个场景:
- key 过期后
- 内存使用达到阈值
key 过期回收
惰性删除
不是在TTL到期后就立刻删除,而是在访问一个key的时候,检查该key的存活时间,如果已经过期才执行删除。
// 查找一个key执行写操作
robj *lookupKeyWriteWithFlags(redisDb *db, robj *key, int flags) {
// 检查key是否过期
expireIfNeeded(db,key);
return lookupKey(db,key,flags);
}
// 查找一个key执行读操作
robj *lookupKeyReadWithFlags(redisDb *db, robj *key, int flags) {
robj *val;
// 检查key是否过期 if (expireIfNeeded(db,key) == 1) {
// ...略
}
return NULL;
}
int expireIfNeeded(redisDb *db, robj *key) {
// 判断是否过期,如果未过期直接结束并返回0
if (!keyIsExpired(db,key)) return 0;
// ... 略
// 删除过期key
deleteExpiredKeyAndPropagate(db,key);
return 1;
}
周期删除
通过一个定时任务,周期性的抽样部分过期的key,然后执行删除。极端情况下,惰性删除可能过期后没有人再访问(可能业务逻辑判定),周期删除解决该问题。周期删除有两种模式:
SLOW:Redis服务初始化函数initServer()中设置定时任务,按照server.hz的频率来执行过期key清理,模式为SLOW
FAST:Redis的每个事件循环前会调用beforeSleep()函数,执行过期key清理,模式为FAST
SLOW模式规则:
- 执行频率受server.hz影响,默认为10,即每秒执行10次,每个执行周期100ms。
- 执行清理耗时不超过一次执行周期的25%.默认slow模式耗时不超过25ms
- 逐个遍历db,逐个遍历db中的bucket,抽取20个key判断是否过期
- 如果没达到时间上限(25ms)并且过期key比例大于10%,再进行一次抽样,否则结束
FAST模式规则(过期key比例小于10%不执行 ):
- 执行频率受beforeSleep()调用频率影响,但两次FAST模式间隔不低于2ms
- 执行清理耗时不超过1ms
- 逐个遍历db,逐个遍历db中的bucket,抽取20个key判断是否过期
- 如果没达到时间上限(1ms)并且过期key比例大于10%,再进行一次抽样,否则结束
内存不足回收
当Redis内存使用达到设置的上限时,主动挑选部分key删除以释放更多内存的流程。Redis会在处理客户端命令的方法processCommand()中尝试做内存回收
int processCommand(client *c) {
// 如果服务器设置了server.maxmemory属性,并且并未有执行lua脚本
if (server.maxmemory && !server.lua_timedout) {
// 尝试进行内存淘汰performEvictions
int out_of_memory = (performEvictions() == EVICT_FAIL);
// ...
if (out_of_memory && reject_cmd_on_oom) {
rejectCommand(c, shared.oomerr);
return C_OK;
}
// ....
}
}
Redis支持8种不同策略来选择要删除的key:
- noeviction: 不淘汰任何key,但是内存满时不允许写入新数据,默认就是这种策略。
- volatile-ttl: 对设置了TTL的key,比较key的剩余TTL值,TTL越小越先被淘汰
- allkeys-random:对全体key ,随机进行淘汰。也就是直接从db->dict中随机挑选
- volatile-random:对设置了TTL的key ,随机进行淘汰。也就是从db->expires中随机挑选。
- allkeys-lru: 对全体key,基于LRU算法进行淘汰
- volatile-lru: 对设置了TTL的key,基于LRU算法进行淘汰
- allkeys-lfu: 对全体key,基于LFU算法进行淘汰
- volatile-lfu: 对设置了TTL的key,基于LFI算法进行淘汰
比较容易混淆的有两个:
- LRU(Least Recently Used),最少最近使用。用当前时间减去最后一次访问时间,这个值越大则淘汰优先级越高。
- LFU(Least Frequently Used),最少频率使用。会统计每个key的访问频率,值越小淘汰优先级越高。
typedef struct redisObject {
unsigned type:4; // 对象类型
unsigned encoding:4; // 编码方式
unsigned lru:LRU_BITS; // LRU:以秒为单位记录最近一次访问时间,长度24bit
// LFU:高16位以分钟为单位记录最近一次访问时间,低8位记录逻辑访问次数
int refcount; // 引用计数,计数为0则可以回收
void *ptr; // 数据指针,指向真实数据
} robj;
LFU的访问次数之所以叫做逻辑访问次数,是因为并不是每次key被访问都计数,而是通过运算:
- 生成0~1之间的随机数R
- 计算 (旧次数 * lfu_log_factor + 1),记录为P
- 如果 R < P ,则计数器 + 1,且最大不超过255
- 访问次数会随时间衰减,距离上一次访问时间每隔 lfu_decay_time 分钟,计数器 - 1
内存不足回收逻辑
# Memory
# 已使用内存
used_memory:2600040
used_memory_human:2.48M
# 以系统角度返回Redis内存使用量
used_memory_rss:3350528
used_memory_rss_human:3.20M
# 已使用内存峰值
used_memory_peak:2657512
used_memory_peak_human:2.53M
used_memory_peak_perc:97.84%
used_memory_overhead:2491632
used_memory_startup:1408824
used_memory_dataset:108408
used_memory_dataset_perc:9.10%
allocator_allocated:2567200
allocator_active:3317760
allocator_resident:3317760
# 总系统内存
total_system_memory:1907679232
total_system_memory_human:1.78G
used_memory_lua:32768
used_memory_lua_human:32.00K
used_memory_scripts:0
used_memory_scripts_human:0B
number_of_cached_scripts:0
# 最大使用内存
# 通过maxmemory来设置内存使用上限,也就是限制used_memory中各项内存大小的总和,由于存在内存碎片,Redis在系统中实际使用的内存要大于maxmemory
maxmemory:0
maxmemory_human:0B
# 最大内存策略,内存达到最大使用量maxmemory时执行的内存回收策略
maxmemory_policy:noeviction
allocator_frag_ratio:1.29
allocator_frag_bytes:750560
allocator_rss_ratio:1.00
allocator_rss_bytes:0
rss_overhead_ratio:1.01
rss_overhead_bytes:32768
# 内存分片率
mem_fragmentation_ratio:1.31
mem_fragmentation_bytes:783328
mem_not_counted_for_evict:20
mem_replication_backlog:1048584
mem_clients_slaves:0
mem_clients_normal:34096
mem_aof_buffer:24
# 内存分配器
mem_allocator:libc
active_defrag_running:0
lazyfree_pending_objects:0
lazyfreed_objects:0
内存碎片
内存碎片
内存碎片率通过mem_fragmentation_ratio = used_memory_rss / used_memory计算获得
| 内存碎片率 | 含义 | 措施 |
|---|---|---|
| >1 | 说明used_memory_rss-used_memory多出 的部分内存并没有用于数据存储,而是被内存碎片所消耗,如果两者相差很 大,说明碎片率严重 | 及时进行碎片清理,一般1~1.5属于正常范围 |
| <1 | 这种情况一般出现在操作系统把Redis 内存交换(Swap)到硬盘导致,出现这种情况时要格外关注,由于硬盘速度远远慢于内存,Redis性能会变得很差,甚至僵死 | 已经开始使用硬盘进行存储,需要考虑扩容内存 |
为何会有碎片问题产生?
- Redis内部有自己的内存管理器,为了提高内存使用的效率,来对内存的申请和释放进行管理。
- Redis中的值删除的时候,并没有把内存直接释放,交还给操作系统,而是交给了Redis内部有内存管理器。
- Redis中申请内存的时候,也是先看自己的内存管理器中是否有足够的内存可用。
- Redis的这种机制,提高了内存的使用率,但是会使Redis中有部分自己没在用,却不释放的内存,导致了内存碎片的发生。
内存构成
Redis的内存主要由自身内存、对象内存、缓冲内存、内存碎片几部分构成。
- 自身内存 自身进程内存占用很小,可以忽略不计
- 对象内存
对象内存是Redis中内存消耗的主要部分,所有用户数据都存储在对象内存中,Redis支持字符串、列表、哈希、集合、有序集合五种数据类型,不同数据类型又由不同的数据结构提供底层实现的 - 缓冲内存
缓冲缓存主要包括客户端缓冲、复制积压缓冲区、AOF缓冲区 - 内存碎片
Redis中默认的内存分配器是jemalloc,其他内存分配器还有glibc、tcmalloc。
内存管理器一般会以块形式对内存进行管理和空间分配。首先按照不同的大小范围进行范围内块大小的定义,根据实际对象的大小来进行内存空间的分配
共享对象池
共享对象池是指Redis内部维护[0-9999]的整数对象池。
创建大量的整数类型redisObject存在内存开销,每个redisObject内部结构至少占16字节,甚至超过了整数自身空间消耗。所以Redis内存维护一个[0-9999]的整数对象 池,用于节约内存。除了整数值对象,其他类型如list、hash、set、zset内部元素也可以使用整数对象池。因此开发中在满足需求的前提下,尽量使用整数对象以节省内存。整数对象池在Redis中通过变量REDIS_SHARED_INTEGERS定义,不能通过配置修改
共享对象池失效的场景:
- 当设置
maxmemory并启用 LRU相关淘汰策略如:volatile-lru,allkeys-lru时,Redis禁止使用共享对象池 - 对于
ziplist编码的值对象,即使内部数据为整数也无法使用共享对象池,因为ziplist使用压缩且内存连续的结构,对象共享判断成本过高