文章摘要(AI生成)
Redis内存模型使用hash表来管理存放在Redis中的键值对。数据的扩容和缩容是通过多次渐进式的rehash过程完成的。具体步骤如下:首先为ht[1]分配空间,字典同时持有ht[0]和ht[1]两个哈希表,然后通过维持一个索引计数器变量rehashidx来表示rehash工作开始。在rehash进行期间,每次对字典执行操作时,程序会将ht[0]哈希表在rehashidx索引上的所有键值对rehash到ht[1],完成后将rehashidx的值增一。随着字典操作的执行,最终ht[0]的所有键值对都会被rehash至ht[1],此时将rehashidx设为-1,表示rehash操作完成。整个Redis的存储模型包括dictEntry结构存放数据、redisObject对象维护键值对的值和指向具有不同数据结构的数据对象的数据指针。常用的数据类型和编码方式包括整型字符串、小于等于44字节的简单动态字符、大于44字节的简单动态字符串、压缩列表实现的列表对象等。
REDIS内存模型
对redis有了解的同学都知道,redis内部底层使用hash表管理我们存放到redis中的键值对,通过对两个hash表完成数据的扩容和缩容。当然,数据由一个表到另一个表的rehash过程是通过多次渐进式的rehash过程完成的。
以下是哈希表渐进式 rehash 的详细步骤:
- 为
ht[1]
分配空间, 让字典同时持有ht[0]
和ht[1]
两个哈希表。- 在字典中维持一个索引计数器变量
rehashidx
, 并将它的值设置为0
, 表示 rehash 工作正式开始。- 在 rehash 进行期间, 每次对字典执行添加、删除、查找或者更新操作时, 程序除了执行指定的操作以外, 还会顺带将
ht[0]
哈希表在rehashidx
索引上的所有键值对 rehash 到ht[1]
, 当 rehash 工作完成之后, 程序将rehashidx
属性的值增一。- 随着字典操作的不断执行, 最终在某个时间点上,
ht[0]
的所有键值对都会被 rehash 至ht[1]
, 这时程序将rehashidx
属性的值设为-1
, 表示 rehash 操作已完成。
整个redis的存储模型如下:
其中,存放数据的dictEntry
结构,存放了我们放入的键值对,其中键值对中的值redis通过redisObject
对象进行维护,主要用于表述实际值的不同数据类型,和指向具有不同数据结构的数据对象的数据指针。
常用的数据类型以及数据编码如下(参考官方文档):
类型 | 编码 | OBJECT ENCODING命令输出对象 | 备注 |
---|---|---|---|
REDIS_STRING(整型) | REDIS_ENCODING_INT | int |
使用整数值实现的字符串对象 |
REDIS_STRING(<=44字节) | REDIS_ENCODING_EMBSTR | embstr |
使用embstr编码的简单动态字符(redisObject和sds连续,只需要分配一次内存空间) |
REDIS_STRING(>44字节) | REDIS_ENCODING_RAW | raw |
使用简单动态字符串实现的字符串对象(redisObject和sds不连续,需要分配两次次内存空间) |
REDIS_LIST(redis3.0之前) | REDIS_ENCODING_ZIPLIST | ziplist |
使用压缩列表实现的列表对象 |
REDIS_LIST(redis3.0之前) | REDIS_ENCODING_LINKEDLIST | linkedlist |
使用快速链表实现的列表对象 |
REDIS_LIST(redis3.0之后) | REDIS_ENCODING_QUICKLIST | quicklist |
使用双端链表实现的列表对象 |
REDIS_HASH(元素<512&k、v长度<64) | REDIS_ENCODING_ZIPLIST | ziplist |
使用压缩列表实现的哈希对象 |
REDIS_HASH(元素>=512||k、v长度>=64) | REDIS_ENCODING_HT | hashtable |
使用字典实现的哈希对象 |
REDIS_SET(元素<512&元素全为整数) | REDIS_ENCODING_INTSET | intset |
使用整数集合实现的集合对客 |
REDIS_SET(元素>=512&元素不全是整数 | REDIS_ENCODING_HT | hashtable |
使用字典实现的集合对象 |
REDIS_ZSET(元素<128&元素长度<64) | REDIS_ENCODING_ZIPLIST | ziplist |
使用压缩列表实现的有序集合对 |
REDIS_ZSET(元素>=128&元素长度>=64) | REDIS_ENCODING_SKIPLIST | skiplist |
使用跳跃表和字典实现的有序集合对象 |
此外还有几种拓展数据类型使用场景较小,如下:
类型 | type 命令输出对象 |
作用 | 用途 |
---|---|---|---|
BitMap |
string |
对字符串执行按位运算 | 状态统计 |
HyperLogLog |
---- | 提供大型集合的基数(即元素数量)的概率估计 | 记录网站IP注册数,每日访问的IP数,页面实时UV、在线用户人数 |
Geospatial |
zset |
存储地理空间索引 | 查找给定地理半径或边界框内的位置 |
REDIS设计优化
jemalloc
作为Redis的默认内存分配器,在减小内存碎片方面做的相对比较好。jemalloc
在64位系统中,将内存空间划分为小、大、巨大三个范围,每个范围内又划分了许多小的内存块单位;当Redis存储数据时,会选择大小最合适的内存块进行存储。
示例,假如我们有一个大小为190个字节的对象,jemalloc
会一律为我们划分吃192(8X2+16X7+32X2)个字节。
内存使用估算
每个dictEntry
占据的空间包括:
dictEntry
结构,24字节,jemalloc
会分配32字节的内存块(64位操作系统下,一个指针8字节,一个dictEntry
由三个指针组成)key
的字节数为sds字节数,而sds字节数=4+字符串key自身长度redisObject
的固定结构会占用16字节,jemalloc
会分配16字节的内存块(4bit+4bit+24bit+4Byte+8Byte=16Byte)value
大小根据我们的数据结构而定,结构越复杂占用字节数越多
优化内存占用
- 利用
jemalloc
特性优化,设置k-v对象时,尽可能为8的整数倍,使jemalloc
可以连续分配,避免占用内存波动 - 使用整型/长整型数据,节省更多空间
- 使用整型数据时,可以调整
OBJ_SHARED_INTEGERS
参数设置合适的共享对象个数来减少对象创建,节省内存空间 - 缩短键值对的存储长度,键值对越长,写入越慢
查看内存用量
使用info memory
命令即可查看redis内存使用情况,在输出信息中,我们通常只关注下面几个信息:
used_memory
:由Redis内存分配器分配的数据内存和缓冲内存的内存总量 (单位是字节),包括使用的虚拟内存 (即swap)used memory_rss
:记录的是由操作系统分配的Redis进程内存和Redis内存中无法再被jemalloc分配的内存碎片 (单位是字节)mem_fragmentation_ratio
:由于在实际应用中,Redis的数据量会比较大,此时进程运行占用的内存与Redis数据量和内存碎片相比,都会小得多;因此used_memory_rss
和used_memory
的比例,便成了衡量Redis内存碎片率的参数内存碎片比率,mem_fragmentation_ratio
一般大于1,且该值越大,内存碎片比例越大。mem_fragmentation ratio
<1,说明Redis使用了虚拟内存,由于虚拟内存的媒介是磁盘,比内存速度要慢很多,当这种情况出现时,应该及时排查,如果内存不足应该及时处理,如增加Redis节点、增加Redis服务器的内存、优化应用等。一般来说,mem _fragmentation_ratio
在1.03左右是比较健康的状态 (对于jemalloc
来说);刚开始的mem_fragmentation_ratio
值很大,是因为还没有向Redis中存入数据,Redis进程本身运行的内存使得used_memory_rss
比used memory
大得多mem_allocator
Redis使用的内存分配器,在编译时指定;可以是 libcjemalloc或者tcmalloc,默认是iemalloc;
used_memory
和used memory_rss
的区别:
前者是从Redis角度得到的量,后者是从操作系统角度得到的量。二者之所以有所不同,一方面是因为内存碎片和Redis进程运行需要占用内存,使得前者可能比后者小,另一方面虚拟内存的存在,使得前者可能比后者大。
评论区