Redis- 内部编码

阅读指南

  • 本篇主要分为两部分
  • 第一部分会笼统的讲述一遍所有的redis中所有的内部编码结构
  • 第二部分会根据redis不同数据类型来描述使用了哪些编码格式

一. 内部编码

  • redis所有值对象在内部都定义为redisObject结构体
    typedef struct redisObject {
        unsigned type:4;
        unsigned encoding:4;
        unsigned lru:REDIS_LRU_BITS; /* lru time (relative to server.lruclock) */
        int refcount;
        void *ptr;
    } robj;
    
  • [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-0Hb2o22h-1604208083346)(http://oowanghan.cn/usr/uploads/redis/redis4.png)]
  • type
    • 表示当前对象使用的数据类型,Redis主要支持5种数据类型:string,hash,list,set,zset。
    • 可以使用type {key}命令查看对象所属类型,type命令返回的是值对象类型,键都是string类型。
  • encoding
    • 表示Redis内部编码类型,encoding在Redis内部使用,代表当前对象内部采用哪种数据结构实现。
    • 理解Redis内部编码方式对于优化内存非常重要 ,同一个对象采用不同的编码实现内存占用存在明显差异
  • lru
    • 记录对象最后一次的访问时间,当配置了maxmemory 和 maxmemory-policy=volatile-lru | allkeys-lru时, 用于辅助LRU算法删除键数据。
    • 可以使用object idletime {key}命令在不更新lru字段情况下查看当前键的空闲时间。
  • refcount
    • 记录当前对象被引用的次数,用于通过引用次数回收内存,当refcount=0时,可以安全回收当前对象空间。
    • 使用object refcount {key}获取当前对象引用。当对象为整数且范围在[0-9999]时,Redis可以使用共享对象的方式来节省内存。具体细节见之后共享对象池部分。
  • *ptr
    • 与对象的数据内容相关,如果是整数直接存储数据,否则表示指向数据的指针。

1. dictEntry

  • Redis 在使用一个全局哈希表保存所有键值对数据,注意这里的哈希表不是redis的数据结构哈希,而是redis存放键值对数据的一种方式,哈希表的每一项是一个 dictEntry 的结构体,用来指向一个键值对。dictEntry 结构中有三个 8 字节的指针,分别指向 key、value 以及下一个 dictEntry,三个指针共 24 字节
  • [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-lSAj74xr-1604208083348)(http://oowanghan.cn/usr/uploads/redis/redis8.jpg)]
  • 这三个指针只有 24 字节,但是在内存当中会占用了 32 字节,这是因为 Redis 使用的内存分配库 jemallocjemalloc 在分配内存时,会根据我们申请的字节数 N,找一个比 N 大,但是最接近 N 的 2 的幂次数作为分配的空间,这样可以减少频繁分配的次数。举个例子,如果你申请 6 字节空间,jemalloc 实际会分配 8 字节空间;如果你申请 24 字节空间,jemalloc 则会分配 32 字节。

2. int

  • string数据类型使用的编码格式
  • 保存 64 位有符号整数时,String 类型会把它保存为一个 8 字节的 Long 类型整数,内部使用int编码

3. embstr

  • string数据类型使用的编码格式
  • embstr: 简单的动态字符串(Simple Dynamic String,SDS)结构体来保存,并且和redisObject结构体在一个连续的存储空间中,然后让ptr指针指向这个sds结构体,可以避免内存碎片,
  • [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-bBPjqXXB-1604208083350)(http://oowanghan.cn/usr/uploads/redis/redis6.jpg)]
    • buf:字节数组,保存实际数据。为了表示字节数组的结束,Redis 会自动在数组最后加一个“\0”,这就会额外占用 1 个字节的开销。
    • len:占 4 个字节,表示 buf 的已用长度。
    • alloc:也占个 4 字节,表示 buf 的实际分配长度,一般大于 len。

4. raw

  • string数据类型使用的编码格式
  • raw: raw也会使用SDS结构体来保存字符串数据,但是不同的是raw会给sds结构体重新分配一块独立空间存放,然后让redisObject的ptr指针指向这个空间。

5. 压缩列表详解(ziplist)

  • 一种非常节省内存的结构。表头有三个字段 zlbyteszltailzllen,分别表示列表长度、列表尾的偏移量,以及列表中的 entry 个数。压缩列表尾还有一个 zlend,表示列表结束。
  • [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-nCc8SSCE-1604208083351)(http://oowanghan.cn/usr/uploads/redis/redis9.jpg)]
  • 压缩列表之所以能节省内存,就在于它是用一系列连续的 entry 保存数据。连续的概念就是不需要指针空间来指向下一个entry的地址,每个 entry 的元数据包括下面几部分。
    • prev_len,表示前一个 entry 的长度。prev_len 有两种取值情况:1 字节或 5 字节。取值 1 字节时,表示上一个 entry 的长度小于 254 字节。虽然 1 字节的值能表示的数值范围是 0 到 255,但是压缩列表中 zlend 的取值默认是 255,因此,就默认用 255 表示整个压缩列表的结束,其他表示长度的地方就不能再用 255 这个值了。所以,当上一个 entry 长度小于 254 字节时,prev_len 取值为 1 字节,否则,就取值为 5 字节。
    • len:表示自身长度,4 字节;
    • encoding:表示编码方式,1 字节;
    • content:保存实际数据。

6. 跳跃表

二. 数据类型对应的内部编码

1. 字符串(strings)

  • 编码格式汇总图 [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-jsMYVKFr-1604208083353)(http://oowanghan.cn/usr/uploads/redis/redis7.jpg)]
  • Redis没有采用原生C语言的字符串类型而是自己实现了字符串结构,内部简单动态字符串(simple dynamic string),简称SDS。
    struct sdshdr {
        unsigned int len;
        unsigned int free;
        char buf[];
    };
    
  • 可用于保存字节数组,支持安全的二进制数据存储。
  • 内部实现空间预分配机制,降低内存再分配次数。惰性删除机制,字符串缩减后的空间不释放,作为预分配空间保留。
  • 字符串类型的内部编码有3种
    • int: 8个字节的长整型
      • 保存 64 位有符号整数时,String 类型会把它保存为一个 8 字节的 Long 类型整数,内部使用int编码
    • embstr: 小于等于44个字节的字符串,使用简单的动态字符串(Simple Dynamic String,SDS)结构体来保存
    • raw: 大于44个字节的字符串
  • 编码区别
    • Redis的embstr编码方式和raw编码方式在3.0版本之前是以39字节为分界的, 而在3.2版本之后,则变成了44字节为分界。
    • 当保存的是 Long 类型整数时,RedisObject 中的指针就直接赋值为整数数据了,这样就不用额外的指针再指向整数了,节省了指针的空间开销。
    • 当保存的是字符串数据,并且字符串小于等于 44 字节时,RedisObject 中的元数据、指针和 SDS 是一块连续的内存区域,这样就可以避免内存碎片。这种布局方式也被称为 embstr 编码方式。
    • 当字符串大于 44 字节时,SDS 的数据量就开始变多了,Redis 就不再把 SDS 和 RedisObject 布局在一起了,而是会给 SDS 分配独立的空间,并用指针指向 SDS 结构。这种布局方式被称为 raw 编码模式。
    • 扩展
      • Redis在3.2之后对值对象是字符串且长度<=44字节的数据,内部编码为embstr类型,字符串sds和redisObject一起分配,从而只要一次内存操作。所以高并发写入场景中,在条件允许的情况下建议字符串长度控制在39字节以内,减少创建redisObject内存分配次数从而提高性能
      • 为何之前是39字节,因为 redisObject占用16个字节,embstr应该占用64 - 16 = 48 字节,两个unsigned的int类型占8位,还有40位,多一位结束符‘\0’,正好是39位。
      • 那为何之后是44位,是因为len和free的长度变短了,sds改成了sds16,sds32,sds64,里面的unsigned int 变成了uint8_t,uint16_t.。。。(还加了一个char flags)这样更加优化小sds的内存使用。本身就是针对短字符串的embstr自然会使用最小的sdshdr8,所以free + len = 1 + 1 + 1 = 3,原来是8个字节,所以从39 -> 44了
  • 内存优化
    • 预分配机制
      • 因为字符串(SDS)存在预分配机制,日常开发中要小心预分配带来的内存浪费
      • 字符串之所以采用预分配的方式是防止修改操作需要不断重分配内存和字节数据拷贝。但同样也会造成内存的浪费。字符串预分配每次并不都是翻倍扩容,空间预分配规则如下:
      • 第一次创建len属性等于数据实际大小,free等于0,不做预分配。
      • 修改后如果已有free空间不够且数据小于1M,每次预分配一倍容量。如原有len=60byte,free=0,再追加60byte,预分配120byte,总占用空间:60byte+60byte+120byte+1byte。
      • 修改后如果已有free空间不够且数据大于1MB,每次预分配1MB数据。如原有len=30MB,free=0,当再追加100byte ,预分配1MB,总占用空间:1MB+100byte+1MB+1byte。
      • 开发提示:尽量减少字符串频繁修改操作如append,setrange, 改为直接使用set修改字符串,降低预分配带来的内存浪费和内存碎片化。
    • 字符串重构
      • 不一定把每份数据作为字符串整体存储,像json这样的数据可以使用hash结构,使用二级结构存储也能帮我们节省内存。同时可以使用hmget,hmset命令支持字段的部分读取修改,而不用每次整体存取
      • 注意这里如果使用hash的话,也要注意hash的编码格式,如果为hashtable则会占用较多内存,如果为ziplist,则会更节省内存,当然查询效率没有hashtable好,后面hash处会详细介绍两者区别

2. 字符串列表(lists)

  • list内部有三种编码格式
    • ziplist(压缩列表):当元素个数小于list-max-ziplist-entries配置(默认512个),同时列表中每个元素都小于list-max-ziplist-value配置时(默认64字节),Redis会使用ziplist来作为列表的内部实现来减少内存占用
    • linkedlist(链表):满足任意条件 value最大空间(字节)>list-max-ziplist-value,链表长度>list-max-ziplist-entries
    • quicklist():3.2版本新编码: 废弃list-max-ziplist-entries和list-max-ziplist-value配置 使用新配置: list-max-ziplist-size:表示最大压缩空间或长度,最大空间使用[-5-1]范围配置,默认-2表示8KB,正整数表示最大压缩长度,list-compress-depth:表示最大压缩深度,默认=0不压缩

3. 字符串集合(sets)

  • sets内部有两种编码格式
    • intset(整数集合):当集合中的元素都是整数且元素个数小于set-max-intset-entries配置(默认512个)时,Redis会选用intset来作为集合的内部编码,以减少内存的使用
    • hashtable(哈希表):当不满足上述条件时,会使用hashtable来作为集合的内部编码

4. 有序字符串集合(sorted sets)

  • sorted sets内部有两种编码格式
    • ziplist(压缩列表):当有序集合的元素个数小于zset-max-ziplost-entries(默认128个),同时每个元素的值小于zset-max-ziplist-value配置(默认64字节)时
    • skiplist(跳跃表):当ziplist条件不满足时,有序集合会使用skiplist,因为此时ziplist的读写效率会下降

5. 哈希(hashes)

  • hash内部有两种编码格式
    • ziplist(压缩列表):当哈希类型元素个数小于hash-max-ziplist-entries配置(默认512个),同时所有值小于hash-max-ziplist-value配置(默认64字节)时,Redis会使用ziplist作为哈希的内部实现,ziplist更加紧凑的结构实现元素的连续存储,在内存方面更加的节省空间
    • hashtable(哈希表):当哈希类型无法满足ziplist的条件时,Redis会使用hashtable作为哈希内部实现,因为此时ziplist的读写效率会下降。而hash的读写复杂度时O(1),但是在内存中占用更多的空间
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值