【Redis-6.0.8】Redis内存模型

 

 

目录

0.引用阅读

1.内存消耗

1.1 内存使用统计

1.1.1 我的测试机器上的内存统计情况

1.1.2 统计参数说明

1.2 内存消耗划分

1.2.1 自身内存

1.2.2 对象内存

1.2.3 缓冲内存

1.2.4 内存碎片

1.2.5 简单总结

1.3 子进程内存消耗

2.内存管理

2.1 设置内存上限

2.2 动态调整内存上限

2.3 内存回收策略

2.3.1 删除过期键对象

2.3.2 内存溢出控制策略 

3.内存优化

3.0 redisObject对象-基础前置知识

3.1 缩减键值对象

3.2 共享对象池

3.2.1 【3.0.7】版本描述

3.2.2 【6.0.8】版本描述

3.3.3 【6.0.8】版本中符合条件的整数共享对象的引用返回值为2147483647

3.3 字符串优化

3.4 编码优化

3.5 控制键的数量

4.对象与编码之间的关系

4.1 概览

4.2 测试案例

5.数据存储的测试情况

6.插入数据库的过程

7. 知识复习

 


0.引用阅读

西安邮电大学的同学的内存淘汰策略解读

Redis内存模型

Redis性能问题排查解决手册

解码Redis最易被忽视的CPU和内存占用高问题

redis info memory命令的各项参数解释汇总

Redis info memory信息说明-全面但有些描述不精确

一文读懂Redis常见对象类型的底层数据结构

【Redis】key很少但是used_memory很大--记录一个现象

Linux虚拟内存介绍,以及malloc_stats和malloc_info 监控查看内存情况

问:开启THP在fork子进程的时候到底是更快了还是更慢了?

Mit6.S081-实验6-Copy-on-Write Fork for xv6

Redis latency spikes and the Linux kernel: a few more details

开启THP导致了Redis响应延迟

Linux THP分析

当Transparent hugepage 遇到fork

不懂别瞎搞!Redis 性能优化的 13 条军规!

redis-共享对象池

redis对象内存回收和整数对象共享池

redis的maxmemory设置

Redis性能调优,影响Redis性能的因素

《Redis开发与运维-付磊》第八章-理解内存-【查找hpelbk的同名电子书】

1.内存消耗

理解Redis内存,首先需要掌握Redis内存消耗在哪些方面.
有些内存消耗是必不可少的,而有些可以通过参数调整和合理使用来规避内存浪费.
内存消耗可以分为进程自身消耗和子进程消耗。

1.1 内存使用统计

1.1.1 我的测试机器上的内存统计情况

Redis服务器端的genRedisInfoString函数中提供了info memory给客户端使用,让我们可以去查询Redis的内存相关情况,在我的测试机器上的一次查询记录如下:

1.1.2 统计参数说明

需要重点关注的指标有:
(1)used_memory_rss;
(2)used_memory;
(3)mem_fragmentation_ratio(=used_memory_rss/used_memory);

当mem_fragmentation_ratio>1时,说明used_memory_rss-used_memory多出的部分内存并没有用于数据存储,
而是被内存碎片所消耗,如果两者相差很大,说明碎片率严重.

当mem_fragmentation_ratio<1时,这是由于内存资源紧张,操作系统把Redis内存交换到硬盘导致,出现这种情
况时要格外关注,由于硬盘速度远远慢于内存,Redis性能会变得很差,甚至僵死.

 

1.2 内存消耗划分

Redis进程内消耗主要包括:
(1)自身内存;
(2)对象内存;
(3)缓冲内存;
(4)内存碎片.
其中Redis空进程自身内存消耗非常少,通常used_memory_rss在3MB左右,
used_memory在800KB左右,一个空的Redis进程消耗内存可以忽略不计.

1.2.1 自身内存

Redis空进程自身内存消耗非常少.

1.2.2 对象内存

对象内存是Redis内存占用最大的一块,存储着用户所有的数据.
Redis所有的数据都采用key-value数据类型,每次创建键值对时,至少创建两个类型对象:
key对象和value对象.

对象内存消耗可以简单理解为sizeof(keys)+sizeof(values).
键对象都是字符串,在使用Redis时很容易忽略键对内存消耗的影响,应当避免使用过长的键.
value对象更复杂些,主要包含5种基本数据类型: 字符串、列表、哈希、集合、有序集合.
其他数据类型都是建立在这5种数据结构之上实现的,如:Bitmaps和HyperLogLog使用字符串实
现,GEO使用有序集合实现等.每种value对象类型根据使用规模不同,占用内存不同.在使用时一
定要合理预估并监控value对象占用情况,避免内存溢出.

1.2.3 缓冲内存

缓冲内存主要包括:
(1)客户端缓冲;
(2)复制积压缓冲区;
(3)AOF缓冲区.

(1)客户端缓冲指的是所有接入到Redis服务器TCP连接的输入输出缓冲.

(1.1) 输入缓冲无法控制,最大空间为1G,如果超过将断开连接.


(1.2) 输出缓冲通过参数 client-output-buffer-limit控制,如下所示:
(1.2.1) 普通客户端:
        除了复制和订阅的客户端之外的所有连接,Redis的默认配置是:
        client-output-buffer-limit normal 0 0 0,Redis并没有对普通客户端的输
        出缓冲区做限制,一般普通客户端的内存消耗可以忽略不计,但是当有大量慢连接
        客户端接入时这部分内存消耗就不能忽略了,可以设置maxclients做限制.特别是
        当使用大量数据输出的命令且数据无法及时推送给客户端时,如monitor命令,容
        易造成Redis服务器内存突然飙升。

(1.2.2) 从客户端:
        主节点会为每个从节点单独建立一条连接用于命令复制,
        默认配置是:client-output-buffer-limit slave 256mb 64mb 60.
        当主从节点之间网络延迟较高或主节点挂载大量从节点时这部分内存消耗将占用很大一部
        分,建议主节点挂载的从节点不要多于2个,主从节点不要部署在较差的网络环境下,如异
        地跨机房环境,防止复制客户端连接缓慢造成溢出.
        (我的6.0.8版本中没有slave,有一个replica,不知道作者的版本是哪个,
         看看两者是不是表示同一个含义)


(1.2.3) 订阅客户端:
        当使用发布订阅功能时,连接客户端使用单独的输出缓冲区,
        默认配置为:client-output-buffer-limit pubsub 32mb 8mb 60,
        当订阅服务的消息生产快于消费速度时,输出缓冲区会产生积压造成输出缓冲区空间溢出.


输入输出缓冲区在大流量的场景中容易失控,造成Redis内存的不稳定,需要重点监控,具体细节见
《Redis开发与运维-付磊》书中的4.4节中的客户端管理部分。


(2)复制积压缓冲区:
Redis在2.8版本之后提供了一个可重用的固定大小缓冲区用于实现部分复制功能,根据
repl-backlog-size参数控制,默认1MB. 对于复制积压缓冲区整个主节点只有一个,所
有的从节点共享此缓冲区,因此可以设置较大的缓冲区空间,如100MB,这部分内存投入
是有价值的,可以有效避免全量复制,更多细节见《Redis开发与运维-付磊》书中的第6.4节.


(3)AOF缓冲区:
这部分空间用于在Redis重写期间保存最近的写入命令,具体细节见《Redis开发与运维-付磊》书中的5.2
节.AOF缓冲区空间消耗用户无法控制,消耗的内存取决于AOF重写时间和写入命令量,这部分空间占用通常
很小.

1.2.4 内存碎片

Redis默认的内存分配器采用jemalloc,可选的分配器还有:glibc、tcmalloc.
内存分配器为了更好地管理和重复利用内存,分配内存策略一般采用固定范围的
内存块进行分配.

例如jemalloc在64位系统中将内存空间划分为:小、大、巨大三个范围.
每个范围内又划分为多个小的内存块单位,
如下所示:

·小:[8byte],[16byte,32byte,48byte,...,128byte],
     [192byte,256byte,...,512byte],
     [768byte,1024byte,...,3840byte]

·大:[4KB,8KB,12KB,...,4072KB]

·巨大:[4MB,8MB,12MB,...]

比如当保存5KB对象时jemalloc可能会采用8KB的块存储,而剩下的3KB空间变为了内存碎片不能再分配给其
他对象存储。内存碎片问题虽然是所有内存服务的通病,但是jemalloc针对碎片化问题专门做了优化,一般
不会存在过度碎片化的问题,正常的碎片率(mem_fragmentation_ratio)在1.03左右.但是当存储的数据
长短差异较大时,以下场景容易出现高内存碎片问题:
·频繁做更新操作,例如频繁对已存在的键执行append、setrange等更新操作;
·大量过期键删除,键对象过期删除后,释放的空间无法得到充分利用,导致碎片率上升.


出现高内存碎片问题时常见的解决方式如下:
·数据对齐:在条件允许的情况下尽量做数据对齐,比如数据尽量采用数字类型或者固定长度字符串等,但是
这要视具体的业务而定,有些场景无法做到.
·安全重启:重启节点可以做到内存碎片重新整理,因此可以利用高可用架构,如Sentinel或Cluster,将碎片
率过高的主节点转换为从节点,进行安全重启.

1.2.5 简单总结

1.3 子进程内存消耗

子进程内存消耗主要指执行AOF/RDB重写时Redis创建的子进程内存消耗.

Redis执行fork操作产生的子进程内存占用量对外表现为与父进程相同,理论上需要一倍的物理内存来完成重写
操作,但Linux具有写时复制技术(copy-on-write),父子进程会共享相同的物理内存页,当父进程处理写请求时
会对需要修改的页复制出一份副本完成写操作,而子进程依然读取fork时整个父进程的内存快照.

Linux Kernel在2.6.38内核增加了Transparent Huge Pages(THP)机制,而有些Linux发行版即使内核达不到
2.6.38也会默认加入并开启这个功能,如Redhat Enterprise Linux在6.0以上版本默认会引入THP.虽然开启
THP可以降低fork子进程的速度(书里是这么写的,但是为什么呢?),但之后copy-on-write期间复制内存页的
单位从4KB变为2MB,如果父进程有大量写命令,会加重内存拷贝量,从而造成过度内存消耗.

例如,以下两个执行AOF重写时的内存消耗日志:

// 开启THP:
* AOF rewrite: 1039 MB of memory used by copy-on-write
// 关闭THP:
* AOF rewrite: 9 MB of memory used by copy-on-write

这两个日志出自同一Redis进程,used_memory总量为1.5GB,子进程执行期间每秒写命令量都在200左右.
当分别开启和关闭THP时,子进程内存消耗有天壤之别。如果在高并发写的场景下开启THP,子进程内存消
耗可能是父进程的数倍,极易造成机器物理内存溢出,从而触发SWAP或OOM killer.

2.内存管理

2.1 设置内存上限

Redis使用maxmemory参数限制最大可用内存,限制内存的目的主要有:
(1)用于缓存场景,当超出内存上限maxmemory时使用LRU等删除策略释放空间;
(2)防止所用内存超过服务器物理内存.

需要注意,maxmemory限制的是Redis实际使用的内存量,也就是used_memory统计项对应的内存.
由于内存碎片率的存在,实际消耗的内存可能会比maxmemory设置的更大,实际使用时要小心这
部分内存溢出.通过设置内存上限可以非常方便地实现一台服务器部署多个Redis进程的内存控制.
比如一台24GB内存的服务器,为系统预留4GB内存,预留4GB空闲内存给其他进程或Redis fork
进程,留给Redis16GB内存,这样可以部署4个maxmemory=4GB的Redis进程.得益于Redis单线程
(现在已不是单线程)架构和内存限制机制,即使没有采用虚拟化,不同的Redis进程之间也可以很
好地实现CPU和内存的隔离性.

 

2.2 动态调整内存上限

Redis的内存上限可以通过config set maxmemory进行动态修改,即修改最大可用内存.
例如之前的示例,当发现Redis-2没有做好内存预估,实际只用了不到2GB内存,而Redis-1
实例需要扩容到6GB内存才够用,这时可以分别执行如下命令进行调整:

Redis-1>config set maxmemory 6GB
Redis-2>config set maxmemory 2GB

通过动态修改maxmemory,可以实现在当前服务器下动态伸缩Redis内存的目的.这个例子
过于理想化,如果此时Redis-3和Redis-4实例也需要分别扩容到6GB,这时超出系统物理
内存限制就不能简单的通过调整maxmemory来达到扩容的目的,需要采用在线迁移数据或
者通过复制切换服务器来达到扩容的目的.

运维提示
Redis默认无限使用服务器内存,为防止极端情况下导致系统内存耗尽,建议所有的Redis
进程都要配置maxmemory.在保证物理内存可用的情况下,系统中所有Redis实例可以调整
maxmemory参数来达到自由伸缩内存的目的.

2.3 内存回收策略

Redis的内存回收机制主要体现在以下两个方面:
(1)删除到达过期时间的键对象;
(2)内存使用达到maxmemory上限时触发内存溢出控制策略.

2.3.1 删除过期键对象

Redis所有的键都可以设置过期属性,内部保存在过期字典中. 由于进程内保存大量的键,
维护每个键精准的过期删除机制会导致消耗大量的CPU,对于单线程的Redis来说成本过高,
因此Redis采用惰性删除和定时任务删除机制实现过期键的内存回收.

(1)惰性删除:
惰性删除用于当客户端读取带有超时属性的键时,如果已经超过键设置的过期时间,
会执行删除操作并返回空,这种策略是出于节省CPU成本考虑,不需要单独维护TTL链表来处理过期键
的删除.但是单独用这种方式存在内存泄露的问题,当过期键一直没有访问将无法得到及时删除,从
而导致内存不能及时释放.正因为如此,Redis还提供另一种定时任务删除机制作为惰性删除的补充.

(2)定时任务删除:
Redis内部维护一个定时任务,默认每秒运行10次(通过配置hz控制)定时任务中
删除过期键逻辑采用了自适应算法,根据键的过期比例、使用快慢两种速率模式回收键.


流程如下图:
流程说明:
(1)定时任务在每个数据库空间随机检查20个键,当发现过期时删除对应的键;
(2)如果超过检查数25%的键过期,循环执行回收逻辑直到不足25%或运行超时为止,慢模式下超时时间为25毫秒;
(3)如果之前回收键逻辑超时,则在Redis触发内部事件之前再次以快模式运行回收过期键任务,快模式下超时
时间为1毫秒且2秒内只能运行1次;
(4)快慢两种模式内部删除逻辑相同,只是执行的超时时间不同.

2.3.2 内存溢出控制策略 

当Redis所用内存达到maxmemory上限时会触发相应的溢出控制策略。
具体策略受maxmemory-policy参数控制,Redis支持6种策略,如下所示:
(1)noeviction:默认策略,不会删除任何数据,拒绝所有写入操作并返回客户端错误信息(error) OOM
 command not allowed when used memory,此时Redis只响应读操作;
(2)volatile-lru: 根据LRU算法删除设置了超时属性(expire)的键,直到腾出足够空间为止,如果没有
可删除的键对象,回退到noeviction策略;
(3)allkeys-lru: 根据LRU算法删除键,不管数据有没有设置超时属性,直到腾出足够空间为止;
(4)allkeys-random: 随机删除所有键,直到腾出足够空间为止;
(5)volatile-random: 随机删除过期键,直到腾出足够空间为止;
(6)volatile-ttl: 根据键值对象的ttl属性,删除最近将要过期数据,如果没有,回退到noeviction策略;

以上6中内存淘汰策略是从《Redis开发与运维-付磊》书中摘取的内存溢出控制策略,现在我还不知道这
本书基于什么版本,但是经过我在我研究的版本6.0.8中,已经又添加了两种内存溢出控制策略,也就是我
们的第7和第8种:

(7)volatile-lfu: 根据LFU算法删除设置了超时属性(expire)的键,直到腾出足够空间为止,如果没有
可删除的键对象,回退到noeviction策略;
(8)allkeys-lfu: 根据LFU算法删除键,不管数据有没有设置超时属性,直到腾出足够空间为止;



内存溢出控制策略可以采用config set maxmemory-policy{policy}动态配
置。Redis支持丰富的内存溢出应对策略,可以根据实际需求灵活定制,比
如当设置volatile-lru策略时,保证具有过期属性的键可以根据LRU剔除,而
未设置超时的键可以永久保留。还可以采用allkeys-lru策略把Redis变为纯缓
存服务器使用。当Redis因为内存溢出删除键时,可以通过执行info stats命令
查看evicted_keys指标找出当前Redis服务器已剔除的键数量。
每次Redis执行命令时如果设置了maxmemory参数,都会尝试执行回收
内存操作。当Redis一直工作在内存溢出(used_memory>maxmemory)的状
态下且设置非noeviction策略时,会频繁地触发回收内存的操作,影响Redis
服务器的性能。


做出以下常识说明:
(1)evict 英 [ɪˈvɪkt]   美 [ɪˈvɪkt]   
v.(尤指依法从房屋或土地上)驱逐,赶出,逐出
evict在我们redis中的意思可以理解成内存淘汰,我们的内存淘汰代码在*src\evict.c中,
代码的主逻辑在evict.c文件的freeMemoryIfNeeded函数中.
(2)LFU(Least frequently used),最不常使用,通常用于淘汰算法,最不常使用的最先被淘汰;
(3)LRU(Least Recently Used),最近最少使用,通常用于淘汰算法,最近最少使用的最先被淘汰.



回收内存逻辑伪代码如下:

def freeMemoryIfNeeded() :
int mem_used, mem_tofree, mem_freed;
//  计算当前内存总量,排除从节点输出缓冲区和 AOF 缓冲区的内存占用
int slaves = server.slaves;
mem_used = used_memory()-slave_output_buffer_size(slaves)-aof_rewrite_buffer_
size();
//  如果当前使用小于等于 maxmemory 退出
if (mem_used <= server.maxmemory) :
return REDIS_OK;
//  如果设置内存溢出策略为 noeviction (不淘汰),返回错误。
if (server.maxmemory_policy == 'noeviction') :
return REDIS_ERR;
//  计算需要释放多少内存
mem_tofree = mem_used - server.maxmemory;
//  初始化已释放内存量
mem_freed = 0;
//  根据 maxmemory-policy 策略循环删除键释放内存
while (mem_freed < mem_tofree) :
//  迭代 Redis 所有数据库空间
for (int j = 0; j < server.dbnum; j++) :
String bestkey = null;
dict dict;
if (server.maxmemory_policy == 'allkeys-lru' ||
server.maxmemory_policy == 'allkeys-random'):
//  如果策略是 allkeys-lru/allkeys-random
//  回收内存目标为所有的数据库键
dict = server.db[j].dict;
else :
//  如果策略是 volatile-lru/volatile-random/volatile-ttl
//  回收内存目标为带过期时间的数据库键
dict = server.db[j].expires;
//  如果使用的是随机策略,那么从目标字典中随机选出键
if (server.maxmemory_policy == 'allkeys-random' ||
server.maxmemory_policy == 'volatile-random') :
//  随机返回被删除键
bestkey = get_random_key(dict);
else if (server.maxmemory_policy == 'allkeys-lru' ||
server.maxmemory_policy == 'volatile-lru') :
//  循环随机采样 maxmemory_samples 次 ( 默认 5 次 ) ,返回相对空闲时间最长的键
bestkey = get_lru_key(dict);
else if (server.maxmemory_policy == 'volatile-ttl') :
//  循环随机采样 maxmemory_samples 次,返回最近将要过期的键
bestkey = get_ttl_key(dict);
//  删除被选中的键
if (bestkey != null) :
long delta = used_memory();
deleteKey(bestkey);
//  计算删除键所释放的内存量
delta -= used_memory();
mem_freed += delta;
//  删除操作同步给从节点
if (slaves):
flushSlavesOutputBuffers();
return REDIS_OK;




从伪代码可以看到,频繁执行回收内存成本很高,主要包括查找可回收键和删除键的开销,如果当前Redis有
从节点,回收内存操作对应的删除命令会同步到从节点,导致写放大的问题(可看本节附图).



运维提示:
建议线上Redis内存工作在maxmemory>used_memory状态下,避免频繁内存回收开销.
对于需要收缩Redis内存的场景,可以通过调小maxmemory来实现快速回收.
比如对一个实际占用6GB内存的进程设置maxmemory=4GB,之后第一次执行命令时,如果使用非noeviction策
略,它会一次性回收到maxmemory指定的内存量,从而达到快速回收内存的目的。注意,此操作会导致数据丢
失和短暂的阻塞问题,一般在缓存场景下使用.



版本6.0.8中maxmemory_policy_enum的枚举值的相关源码定义:
configEnum maxmemory_policy_enum[] = {
    {"volatile-lru", MAXMEMORY_VOLATILE_LRU},
    {"volatile-lfu", MAXMEMORY_VOLATILE_LFU},
    {"volatile-random",MAXMEMORY_VOLATILE_RANDOM},
    {"volatile-ttl",MAXMEMORY_VOLATILE_TTL},
    {"allkeys-lru",MAXMEMORY_ALLKEYS_LRU},
    {"allkeys-lfu",MAXMEMORY_ALLKEYS_LFU},
    {"allkeys-random",MAXMEMORY_ALLKEYS_RANDOM},
    {"noeviction",MAXMEMORY_NO_EVICTION},
    {NULL, 0}
};

3.内存优化

3.0 redisObject对象-基础前置知识

 

3.1 缩减键值对象

降低Redis内存使用最直接的方式就是缩减键(key)和值(value)的长
度。
·key长度:如在设计键时,在完整描述业务情况下,键值越短越好。如
user:{uid}:friends:notify:{fid}可以简化为u:{uid}:fs:nt:{fid}。
·value长度:值对象缩减比较复杂,常见需求是把业务对象序列化成二
进制数组放入Redis。首先应该在业务上精简业务对象,去掉不必要的属性
避免存储无效数据。其次在序列化工具选择上,应该选择更高效的序列化工
具来降低字节数组大小。以Java为例,内置的序列化方式无论从速度还是压
缩比都不尽如人意,这时可以选择更高效的序列化工具,如:protostuff、
kryo等,图8-7是Java常见序列化工具空间压缩对比。

Java常见序列化组件占用内存空间对比(单位字节)
其中java-built-in-serializer表示Java内置序列化方式,更多数据见jvm-serializers项目:https://github.com/eishay/jvm-serializers/wiki,其他语言也有
各自对应的高效序列化工具。
值对象除了存储二进制数据之外,通常还会使用通用格式存储数据比
如:json、xml等作为字符串存储在Redis中。这种方式优点是方便调试和跨
语言,但是同样的数据相比字节数组所需的空间更大,在内存紧张的情况
下,可以使用通用压缩算法压缩json、xml后再存入Redis,从而降低内存占
用,例如使用GZIP压缩后的json可降低约60%的空间。
开发提示
当频繁压缩解压json等文本数据时,开发人员需要考虑压缩速度和计算
开销成本,这里推荐使用Google的Snappy压缩工具,在特定的压缩率情况下
效率远远高于GZIP等传统压缩工具,且支持所有主流语言环境。
448

3.2 共享对象池

3.2.1 【3.0.7】版本描述

https://redis.io/commands/config-get【相关命令】

redis OBJECT REFCOUNT得到 2147483647

本章节选自《Redis开发与运维》.
共享对象池是指Redis内部维护[0-9999]的整数对象池。创建大量的整数
类型redisObject存在内存开销,每个redisObject内部结构至少占16字节,甚
至超过了整数自身空间消耗。所以Redis内存维护一个[0-9999]的整数对象
池,用于节约内存。除了整数值对象,其他类型如list、hash、set、zset内部
元素也可以使用整数对象池。因此开发中在满足需求的前提下,尽量使用整
数对象以节省内存。
整数对象池在Redis中通过变量REDIS_SHARED_INTEGERS定义,不能
通过配置修改。可以通过object refcount命令查看对象引用数验证是否启用整
数对象池技术,如下:

redis> set foo 100
OK
redis> object refcount foo
(integer) 2
redis> set bar 100
OK
redis> object refcount bar
(integer) 3

redis> set key:1 99
OK //  设置 key:1=99
redis> object refcount key:1
(integer) 2 //  使用了对象共享 , 引用数为 2
redis> config set maxmemory-policy volatile-lru
OK //  开启 LRU 淘汰策略
redis> set key:2 99
OK //  设置 key:2=99
redis> object refcount key:2
(integer) 3 //  使用了对象共享 , 引用数变为 3
redis> config set maxmemory 1GB
OK //  设置最大可用内存
redis> set key:3 99
OK //  设置 key:3=99
redis> object refcount key:3
(integer) 1 //  未使用对象共享 , 引用数为 1
redis> config set maxmemory-policy volatile-ttl
OK //  设置非 LRU 淘汰策略
redis> set key:4 99
OK //  设置 key:4=99
redis> object refcount key:4
(integer) 4 //  又可以使用对象共享 , 引用数变为 4


为什么开启maxmemory和LRU淘汰策略后对象池无效?
LRU算法需要获取对象最后被访问时间,以便淘汰最长未访问数据,每
个对象最后访问时间存储在redisObject对象的lru字段。对象共享意味着多个
引用共享同一个redisObject,这时lru字段也会被共享,导致无法获取每个对
象的最后访问时间。如果没有设置maxmemory,直到内存被用尽Redis也不
会触发内存回收,所以共享对象池可以正常工作。
综上所述,共享对象池与maxmemory+LRU策略冲突,使用时需要注
意。对于ziplist编码的值对象,即使内部数据为整数也无法使用共享对象
451
池,因为ziplist使用压缩且内存连续的结构,对象共享判断成本过高




为什么只有整数对象池?
首先整数对象池复用的几率最大,其次对象共享的一个关键操作就是判
断相等性,Redis之所以只有整数对象池,是因为整数比较算法时间复杂度
为O(1),只保留一万个整数为了防止对象池浪费。如果是字符串判断相
等性,时间复杂度变为O(n),特别是长字符串更消耗性能(浮点数在
Redis内部使用字符串存储)。对于更复杂的数据结构如hash、list等,相等
性判断需要O(n 2 )。对于单线程的Redis来说,这样的开销显然不合理,因
此Redis只保留整数共享对象池。


下载安装【3.0.7】版本:
wget https://download.redis.io/releases/redis-3.0.7.tar.gz
tar -zxvf redis-3.0.7.tar.gz
cd redis-3.0.7
make
cd src

对应版本的服务器和客户端的路径在:
/home/muten/module/redis-3.0.7/redis-3.0.7/src/redis-server
/home/muten/module/redis-3.0.7/redis-3.0.7/src/redis-cli


【3.0.7】版本中的源码:
    len = sdslen(s);
    if (len <= 21 && string2l(s,len,&value)) {
        /* This object is encodable as a long. Try to use a shared object.
         * Note that we avoid using shared integers when maxmemory is used
         * because every object needs to have a private LRU field for the LRU
         * algorithm to work well. */
        if ((server.maxmemory == 0 ||
             (server.maxmemory_policy != REDIS_MAXMEMORY_VOLATILE_LRU &&
              server.maxmemory_policy != REDIS_MAXMEMORY_ALLKEYS_LRU)) &&
            value >= 0 &&
            value < REDIS_SHARED_INTEGERS)
        {
            decrRefCount(o);
            incrRefCount(shared.integers[value]);
            return shared.integers[value];
        } else {
            if (o->encoding == REDIS_ENCODING_RAW) sdsfree(o->ptr);
            o->encoding = REDIS_ENCODING_INT;
            o->ptr = (void*) value;
            return o;
        }
    }


3.2.2 【6.0.8】版本描述

【3.2.1】节摘自《Redis开发与运维》,其8.3.3节的一个测试数据显示,作者分析的Redis版本可能是
3.0.7,据我现在研究的版本已经有所变化,下面我将列出我发现的变化,
(1)整数对象池的宏定义的名字发生了变化
之前的整数对象池在Redis中通过变量REDIS_SHARED_INTEGERS定义,【6.0.8】版本中的整数对象池
通过变量OBJ_SHARED_INTEGERS来定义;
(2)现在不仅仅只有整数共享对象池【待验证,仅供参考】.
20210418这天我还没有看过3.0.7这一版的redis源码,但《Redis开发与运维》在书中提问【为什么只有整
数对象池】,我可以将其理解成3.0.7这一个版本的redis只能支持整数共享对象池吗?假定我可以这么认为.
在6.0.8版本中,我发现了struct sharedObjectsStruct结构体,这个结构体中不仅仅包含了integers(整
数)类的共享对象池,还包含了crlf,ok,err,emptybulk等等各式各样的共享对象池,也许我可以去翻翻之
前的代码去看看,再确认一下我的想法.


共享对象池怎么才能生效呢?
(1)在你的客户端命令行查询maxmemory-policy和maxmemory的配置.
127.0.0.1:6379>  config get maxmemory-policy 
1) "maxmemory-policy"
2) "volatile-ttl"
127.0.0.1:6379>  config get maxmemory
1) "maxmemory"
2) "0"

代码中增加引用是incrRefCount函数,整数的引用是incrRefCount(shared.integers[value]).




【6.0.9】版本
/home/muten/module/redis-6.0.9/src/redis-server
/home/muten/module/redis-6.0.9/src/redis-cli

 

3.3.3 【6.0.8】版本中符合条件的整数共享对象的引用返回值为2147483647

redis OBJECT REFCOUNT一个0-9999范围的数字显示了 (integer) 2147483647

#define INT_MAX       2147483647
#define OBJ_SHARED_REFCOUNT INT_MAX  
#define MAXMEMORY_FLAG_NO_SHARED_INTEGERS \
    (MAXMEMORY_FLAG_LRU|MAXMEMORY_FLAG_LFU)
#define OBJ_FIRST_SPECIAL_REFCOUNT OBJ_STATIC_REFCOUNT

    
robj *tryObjectEncoding(robj *o) {
...
    len = sdslen(s);
    if (len <= 20 && string2l(s,len,&value)) {
        /* This object is encodable as a long. Try to use a shared object.
         * Note that we avoid using shared integers when maxmemory is used
         * because every object needs to have a private LRU field for the LRU
         * algorithm to work well. */
        if ((server.maxmemory == 0 ||
            !(server.maxmemory_policy & MAXMEMORY_FLAG_NO_SHARED_INTEGERS)) &&
            value >= 0 &&
            value < OBJ_SHARED_INTEGERS)
        {
            decrRefCount(o);
            incrRefCount(shared.integers[value]);
            return shared.integers[value];
        } else {
            if (o->encoding == OBJ_ENCODING_RAW) {
                sdsfree(o->ptr);
                o->encoding = OBJ_ENCODING_INT;
                o->ptr = (void*) value;
                return o;
            } else if (o->encoding == OBJ_ENCODING_EMBSTR) {
                decrRefCount(o);
                return createStringObjectFromLongLongForValue(value);
            }
        }
    }
...
}

void incrRefCount(robj *o) {
    if (o->refcount < OBJ_FIRST_SPECIAL_REFCOUNT) {
        o->refcount++;
    } else {
        if (o->refcount == OBJ_SHARED_REFCOUNT) {
            /* Nothing to do: this refcount is immutable. */
        } else if (o->refcount == OBJ_STATIC_REFCOUNT) {
            serverPanic("You tried to retain an object allocated in the stack");
        }
    }
}

第一次尝试调试:
(gdb) b object.c:477
Breakpoint 2 at 0x44ab13: file object.c, line 477.
(gdb) p shared.integers[value]
$8 = (robj *) 0x7ffff6c4d2d0
(gdb) p shared.integers[value].ref
There is no member named ref.
(gdb) p shared.integers[value].refcount
$9 = 2147483647
(gdb) 


第二次尝试调试:
(gdb) b object.c:473
Breakpoint 1 at 0x44ab53: file object.c, line 473.
(gdb) r
(gdb) b 475
Breakpoint 2 at 0x44ab5b: file object.c, line 475.
(gdb) s
Breakpoint 2, tryObjectEncoding () at object.c:475
475	            incrRefCount(shared.integers[value]);
(gdb) s
incrRefCount (o=<optimized out>) at object.c:350
350	    if (o->refcount < OBJ_FIRST_SPECIAL_REFCOUNT) {
(gdb) p shared.integers[value].refcount
$2 = 2147483647
(gdb) p o->refcount
value has been optimized out
(gdb) p o->refcount
$3 = 1
(gdb) p value
$4 = 1
(gdb) p shared.integers[value]
$5 = (robj *) 0x7ffff6c4d2d0
(gdb) p shared.integers[1]
$6 = (robj *) 0x7ffff6c4d2d0
(gdb) p shared.integers[2]
$7 = (robj *) 0x7ffff6c4d2e0
(gdb) p shared.integers[value]
$8 = (robj *) 0x7ffff6c4d2d0
(gdb) p shared.integers[2].refcount
$9 = 2147483647
(gdb) p shared.integers[1].refcount
$10 = 2147483647
(gdb) p shared.integers[9999].refcount
$11 = 2147483647
(gdb) p shared.integers[10000].refcount
$12 = 1

3.3 字符串优化

 

3.4 编码优化

 

3.5 控制键的数量

 

4.对象与编码之间的关系

4.1 概览

4.2 测试案例

【测试用例1-字符串的测试】
127.0.0.1:6379> set key1 8653
OK
127.0.0.1:6379> set key2 "hello,world"
OK
127.0.0.1:6379> set key3 "I am a string whose len is longer than 44 in Redis 6.0.8, let us learn Redis 6.0.8 together happily!"
OK
127.0.0.1:6379> object encoding key1
"int"
127.0.0.1:6379> object encoding key2
"embstr"
127.0.0.1:6379> object encoding key3
"raw"
127.0.0.1:6379> strlen key1
(integer) 4
127.0.0.1:6379> strlen key2
(integer) 11
127.0.0.1:6379> strlen key3
(integer) 100

【测试用例2-列表,队列的测试】
127.0.0.1:6379> rpush listkey1 e1 e2 e3
(integer) 3
127.0.0.1:6379> object encoding listkey1
"quicklist"
127.0.0.1:6379> rpush listkey2 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511
(integer) 512
127.0.0.1:6379> object encoding listkey2
"quicklist"



【测试用例3-哈希表的测试】
127.0.0.1:6379> hmset hashkey1 f1 v1 f2 v2
OK
127.0.0.1:6379> object encoding hashkey1
"ziplist"
127.0.0.1:6379> hset hashkey2 f3 "I am the value of the hashkey2's field whose name is f3 to test the encoding hashtable "
(integer) 1
127.0.0.1:6379> object encoding hashkey2
"hashtable"
127.0.0.1:6379> rpush listkey1 e1 e2 e3
(integer) 3
127.0.0.1:6379> object encoding listkey1
"quicklist"


【测试用例4-集合的测试】
127.0.0.1:6379> sadd setkey1 muten
(integer) 1
127.0.0.1:6379> sadd setkey2 0 1 2 3 4
(integer) 5
127.0.0.1:6379> sadd setkey3 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512
(integer) 513
127.0.0.1:6379> object encoding setkey1
"hashtable"
127.0.0.1:6379> object encoding setkey2
"intset"
127.0.0.1:6379> object encoding setkey3
"hashtable"
127.0.0.1:6379> strlen setkey1
(error) WRONGTYPE Operation against a key holding the wrong kind of value



【测试用例5-有序集合的测试】
127.0.0.1:6379> zadd zsetkey1 50 e1 60 e2 30 e3
(integer) 3
127.0.0.1:6379> object encoding zsetkey1
"ziplist"
127.0.0.1:6379> zadd zsetkey2 50 1234567889999988172636545463535261283738474645353224252516262525334352622 60 e2 30 e3
(integer) 3
127.0.0.1:6379> object encoding zsetkey2
"skiplist"

4.3 关于Redis数据结构中的深层思考

4.3.1 Why44?

Redis的字符串底层区别emb和raw的分界线是 44 呢?

4.3.2 

 

 

5.数据存储的测试情况

获取哈希值的函数,可以测下每次获取的哈希值是否一样:

uint64_t dictGenHashFunction(const void *key, int len) {
    return siphash(key,len,dict_hash_function_seed);
}
客户端做以下设置以后:
set key1 1 
set key2 1 
set key3 1 
set key4 1 
set key5 1 
set key6 1 
set key7 1 
set key8 1 



服务器端调试过程:
(gdb) p server.db->dict->ht[0]->size
(gdb) p server.db->dict->ht[0]->used
(gdb) p (char*)server.db->dict->ht[0]->table[0]->key--查看key值存的是什么
(gdb) p (char*)server.db->dict->ht[0]->table[1]->key
(gdb) p (char*)server.db->dict->ht[0]->table[2]->key--空
(gdb) p (char*)server.db->dict->ht[0]->table[3]->key
(gdb) p (char*)server.db->dict->ht[0]->table[4]->key--空
(gdb) p (char*)server.db->dict->ht[0]->table[5]->key
(gdb) p (char*)server.db->dict->ht[0]->table[6]->key
(gdb) p (char*)server.db->dict->ht[0]->table[7]->key--空
(gdb) p (char*)server.db->dict->ht[0]->table[0]->next->key //0号槽位冲突,有三个dictEntry对象
(gdb) p (char*)server.db->dict->ht[0]->table[0]->next->next->key 
(gdb) p (char*)server.db->dict->ht[0]->table[3]->next->key //3号槽位冲突,有两个dictEntry对象
(gdb) p server.db->dict->ht[0]->table[0]->next
(gdb) p server.db->dict->ht[0]->table[0]->next->next
(gdb) p dictGenHashFunction("key7",4)--获取哈希值

客户端set 1 1 之后,在服务器端调试
(gdb) p (char*)server.db->dict->ht[0]->table[0]->key
$1 = 0x7ffff4c098a9 "1"
(gdb) p (robj*)server.db->dict->ht[0]->table[0]->v->val
$2 = (robj *) 0x7ffff4c4d2d0
(gdb) p ((robj*)server.db->dict->ht[0]->table[0]->v->val)->type
$4 = 0
(gdb) p ((robj*)server.db->dict->ht[0]->table[0]->v->val)->encoding
$5 = 1
(gdb) p ((robj*)server.db->dict->ht[0]->table[0]->v->val)->lru
$6 = 7845839
(gdb) p ((robj*)server.db->dict->ht[0]->table[0]->v->val)->refcount
$7 = 2147483647
(gdb) p ((robj*)server.db->dict->ht[0]->table[0]->v->val)->ptr
$8 = (void *) 0x1

这里的是我在数据库中set 1 1,想要通过gdb打印出在ht[0]中对应的value值,为什么打印出来的是这个样子的?加编码了吗? 是的,加了编码的,server.db->dict->ht[0]->table[0]->v->val指向的其实是一个robj结构体【VIP】,所以要通过这个值来进行一个解码.

6.插入数据库的过程

【一级指针与二级指针】【指针数组与二级指针的区别】【指针数组与二级指针比较

subject->encoding == OBJ_ENCODING_QUICKLIST
subject->encoding==OBJ_ENCODING_ZIPLIST
subject->encoding == OBJ_ENCODING_HT
subject->encoding == OBJ_ENCODING_INTSET

listTypePush(中调用了subject->encoding == OBJ_ENCODING_QUICKLIST)---->listTypePush被
pushGenericCommand调用-->pushGenericCommand被lpushCommand和rpushCommand调用.



void listTypePush(robj *subject, robj *value, int where) {
    if (subject->encoding == OBJ_ENCODING_QUICKLIST) {
        int pos = (where == LIST_HEAD) ? QUICKLIST_HEAD : QUICKLIST_TAIL;
        value = getDecodedObject(value);
        size_t len = sdslen(value->ptr);
        quicklistPush(subject->ptr, value->ptr, len, pos);
        decrRefCount(value);
    } else {
        serverPanic("Unknown list encoding");
    }
}


void quicklistPush(quicklist *quicklist, void *value, const size_t sz,
                   int where) {
    if (where == QUICKLIST_HEAD) {
        quicklistPushHead(quicklist, value, sz);
    } else if (where == QUICKLIST_TAIL) {
        quicklistPushTail(quicklist, value, sz);
    }
}


int quicklistPushHead(quicklist *quicklist, void *value, size_t sz) {
    quicklistNode *orig_head = quicklist->head;
    if (likely(
            _quicklistNodeAllowInsert(quicklist->head, quicklist->fill, sz))) {
        quicklist->head->zl =
            ziplistPush(quicklist->head->zl, value, sz, ZIPLIST_HEAD);
        quicklistNodeUpdateSz(quicklist->head);
    } else {
        quicklistNode *node = quicklistCreateNode();
        node->zl = ziplistPush(ziplistNew(), value, sz, ZIPLIST_HEAD);

        quicklistNodeUpdateSz(node);
        _quicklistInsertNodeBefore(quicklist, quicklist->head, node);
    }
    quicklist->count++;
    quicklist->head->count++;
    return (orig_head != quicklist->head);
}

7. 知识复习

PDE:Page Directory Entry
PTE:Page Table Entry(记录着虚拟地址的真实物理地址)

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值