关于后端:Redis的String类型原来这么占内存

12次阅读

共计 4589 个字符,预计需要花费 12 分钟才能阅读完成。

Redis 的 String 类型,原来这么占内存

存一个 Long 类型这么占内存,Redis 的内存开销都花在哪儿了?

1、场景介绍

假如当初咱们要开发一个图片存储系统,要求这个零碎可能依据图片 ID 疾速查找到图片存储对象 ID。图片 ID 和图片存储对象 ID 的样例数据如下:

photo_id: 1101000060
photo_obj_id: 3302000080

在这种场景下,图片 ID 和图片存储对象 ID 刚好是一对一的关系,是典型的“键 – 单值”模式,Redis 的 String 类型提供了“一个键对应一个值的数据”的保留模式,在这种场景下刚好实用。

确定应用 String 类型后,接下来咱们通过实战,来看看它的内存应用状况。首先通过上面命令连贯上 Redis。

本文我应用的 Redis Server 及下文源码都是 6.2.4 版本。

redis-cli -h 127.0.0.1 -p 6379

而后执行上面的命令查看 Redis 的初始内存应用状况。

127.0.0.1:6379> info memory
# Memory
used_memory:871840

接着插入 10 条数据:

10.118.32.170:0> set 1101000060 3302000080
10.118.32.170:0> set 1101000061 3302000081
10.118.32.170:0> set 1101000062 3302000082
10.118.32.170:0> set 1101000063 3302000083
10.118.32.170:0> set 1101000064 3302000084
10.118.32.170:0> set 1101000065 3302000085
10.118.32.170:0> set 1101000066 3302000086
10.118.32.170:0> set 1101000067 3302000087
10.118.32.170:0> set 1101000068 3302000088
10.118.32.170:0> set 1101000069 3302000089

再次查看内存:

127.0.0.1:6379> info memory
# Memory
used_memory:872528

能够看到,存储 10 个图片,内存应用了 688 个字节。一个图片 ID 和图片存储对象 ID 的记录均匀用了 68 字节。

但问题是,一组图片 ID 及其存储对象 ID 的记录,理论只须要 16 字节就能够了。图片 ID 和图片存储对象 ID 都是 10 位数,而 8 字节的 Long 类型最大能够示意 2 的 64 次方的数值,必定能够示意 10 位数。这样算下来只需 16 字节就能够了,为什么 String 类型却用了 68 字节呢?

为了一探到底,咱们不得不从 String 类型的底层实现扒起。

2、String 类型的底层实现

当你保留的数据中蕴含字符时,String 类型就会用简略动静字符串(Simple Dynamic String,SDS)构造体来保留。

2.1 SDS

SDS 的构造定义在 sds.h 文件中,在 Redis 3.2 版本之后,SDS 由一种数据结构变成了 5 种数据结构。

/* Note: sdshdr5 is never used, we just access the flags byte directly.
 * However is here to document the layout of type 5 SDS strings. */
struct __attribute__ ((__packed__)) hisdshdr5 {
    unsigned char flags; /* 3 lsb of type, and 5 msb of string length */
    char buf[];};
struct __attribute__ ((__packed__)) hisdshdr8 {
    uint8_t len; /* used */
    uint8_t alloc; /* excluding the header and null terminator */
    unsigned char flags; /* 3 lsb of type, 5 unused bits */
    char buf[];};
struct __attribute__ ((__packed__)) hisdshdr16 {
    uint16_t len; /* used */
    uint16_t alloc; /* excluding the header and null terminator */
    unsigned char flags; /* 3 lsb of type, 5 unused bits */
    char buf[];};
struct __attribute__ ((__packed__)) hisdshdr32 {
    uint32_t len; /* used */
    uint32_t alloc; /* excluding the header and null terminator */
    unsigned char flags; /* 3 lsb of type, 5 unused bits */
    char buf[];};
struct __attribute__ ((__packed__)) hisdshdr64 {
    uint64_t len; /* used */
    uint64_t alloc; /* excluding the header and null terminator */
    unsigned char flags; /* 3 lsb of type, 5 unused bits */
    char buf[];};

这 5 种数据结构顺次存储不同长度的内容,Redis 会依据 SDS 存储的内容长度来抉择不同的构造。

  • sdshdr5:存储大小为 32 字节(2 的 5 次方),只被利用在了 Redis 中的 key 中。
  • sdshdr8:存储大小为 256 字节(2 的 8 次方)。
  • sdshdr16:存储大小为 64KB(2 的 16 次方)。
  • sdshdr32:存储大小为 4GB(2 的 32 次方)。
  • sdshdr64:存储大小为 2 的 64 次方字节。

以 sdshdr8 为例。

  • buf:字节数组,保留理论数据。为了示意字节数组的完结,Redis 会主动在数组最初加一个'\0',这就会额定占用 1 个字节的开销。
  • len:占 4 个字节,示意 buf 的已用长度,不包含'\0'
  • alloc:也占 4 个字节,示意 buf 的理论调配长度,不包含'\0'
  • flags:占 1 个字节,标记以后字节数组的属性,是 sdshdr8 还是 sdshdr16 等。(flags 值的定义能够看上面代码)

在源码 sds.h 中,flags 值定义如下:

#define HI_SDS_TYPE_5  0 
#define HI_SDS_TYPE_8  1
#define HI_SDS_TYPE_16 2
#define HI_SDS_TYPE_32 3
#define HI_SDS_TYPE_64 4

2.2 RedisObject

因为 Redis 的数据类型有很多,而且,不同数据类型都有些雷同的元数据要记录,所以,值对象并不是间接存储,而是被包装成 redisObject 对象,它的定义如下。

typedef struct redisObject {
    unsigned type:4;// 对象类型(4 位 =0.5 字节)unsigned encoding:4;// 编码(4 位 =0.5 字节)unsigned lru:LRU_BITS;// 记录对象最初一次被应用程序拜访的工夫(24 位 = 3 字节)int refcount;// 援用计数。等于 0 时示意能够被垃圾回收(32 位 = 4 字节)void *ptr;// 指向底层理论的数据存储构造,如:sds 等(8 字节)
} robj;

上面能够帮忙咱们了解:

为了节俭内存空间,Redis 还做了一些优化。

当保留的是 Long 类型整数时,RedisObject 中的指针就间接赋值为整数数据了,这样就不必额定的指针再指向整数了。这种保留形式通常也叫作 int 编码方式。

当保留的是字符串数据,并且字符串小于等于 44 字节时,RedisObject 中的元数据、指针和 SDS 是一块间断的内存区域,这样就能够防止内存碎片。这种布局形式也被称为 embstr 编码方式。

当字符串大于 44 字节时,SDS 的数据量就开始变多了,Redis 就不再把 SDS 和 RedisObject 布局在一起了,而是会给 SDS 调配独立的空间,并用指针指向 SDS 构造。这种布局形式被称为 raw 编码模式。

应用 OBJECT ENCODING 命令能够查看一个数据库键的值对象的编码:

127.0.0.1:6379> SET msg "hello world"
OK
127.0.0.1:6379> OBJECT ENCODING msg
"embstr"
127.0.0.1:6379> SET story "long long long ago..."
OK
127.0.0.1:6379> OBJECT ENCODING story
"raw"
127.0.0.1:6379> SADD numbers 1 3 5
(integer) 3
127.0.0.1:6379> OBJECT ENCODING numbers
"intset"
127.0.0.1:6379> SADD numbers "seven"
(integer) 1
127.0.0.1:6379> OBJECT ENCODING numbers
"hashtable"

留神这个命令SET story "long long long ago...",省略号指的是省略了很多字符。

晓得了 SDS 和 RedisObject 额定元数据开销,当初,咱们就能够计算 String 类型的内存使用量了。

图片存储对象 ID 是 Long 类型整数,所以能够间接用 int 编码的 RedisObject 保留。每个 int 编码的 RedisObject 元数据局部占 8 字节,指针局部被间接赋值为 8 字节的整数了。图片 ID 应用 sdshdr5 数据结构来保留,会为 10 位的图片 ID 调配 16 个字节,结束符 ‘\0’ 占 1 个字节。

共占用 34 个字节。与上文所说的一个图片 ID 和图片存储对象 ID 的记录均匀用了 68 字节相差有点大啊,另外的开销去哪儿了?

2.3 全局哈希表

为了实现从键到值的快速访问,Redis 应用了一个哈希表来保留所有键值对。因为这个哈希表保留了所有的键值对,所以,也称为全局哈希表。哈希表的每一项是一个 dictEntry 的构造体,用来指向一个键值对。dictEntry 构造中有三个 8 字节的指针,别离指向 key、value 以及下一个 dictEntry,三个指针共 24 字节,如下图所示:

jemalloc 在分配内存时,会调配一个最靠近 2 的 N 次方的数值。举个例子。如果你申请 6 字节空间,jemalloc 理论会调配 2 的 4 次方即 8 字节空间;如果你申请 24 字节空间,jemalloc 则会调配 32 字节。

最终咱们剖析进去的内存开销,为 66 字节,比拟靠近上文场景中的平均值 68 了。

最初

既然 String 类型这么占内存,那么你有好的计划来节俭内存吗?

这篇文章内容我筹备了一周,如果对你有帮忙,能够点个「在看」吗?你的点赞会让作者兴奋得一早晨睡不着觉。

对前面的内容感兴趣,也能够关注公众号「杨同学 technotes」,感激反对!

参考资料

  • 文中的一些命令,参考菜鸟教程:https://www.runoob.com/redis/…
  • Redis 的 key 也是 SDS 类型的,参考:https://www.cnblogs.com/lonel…
  • SDS 的定义,参考:https://juejin.cn/post/684490…
  • 文章纲要,参考极客工夫《Redis 核心技术与实战》
  • 《Redis 设计与实现》

正文完
 0