欢迎您访问 最编程 本站为您分享编程语言代码,编程技术文章!
您现在的位置是: 首页

[Redis 实验室 1] Redis 源代码分析 100W 数据的内存使用和优化

最编程 2024-05-31 19:48:21
...

前言

实验内容源自极客时间 Redis 核心技术和实践:P11

1. 实验内容

业务假设,需要保存 100w 个 K-V 值。其中 K 是一个 long 类型,V 是一个 String 类型。

  • 使用普通的 K-V 存储,观察内存占用及效率;
  • 使用 Hash 结构存储,观察内存占用及效率;
  • 调整 Redis 参数,重复实验,观察内存占用;

2. 实验过程

2.1 python + redis 往 Redis 写入 K-V 字符串 100w 条

def write100wkv_with_pipline():
    print("begin write 100w")
    pip = client.pipeline(transaction=False)

    before = getRedisUsedMemory()
    for k in range(0, 1000000):
        pip.set(k, 'v' + str(k))
    pip.execute()
    after = getRedisUsedMemory()
    print("end write 100w, using: " + str(after - before))

在这个过程中,必须开启 pipeline 去进行通信。

begin write 100w
before: used_memory_human:1.87M
before: used_memory_human:70.90M
end write 100w, using: 72380608

100w 行的 k-v 数据,使用了约 70m 的存储空间。

我尝试实用 keys 对 Redis 的 keys 进行一次遍历,结果需要查询 9s

....
 999998) "301947"
 999999) "869004"
1000000) "524681"
(9.03s)

2.2 Redis 的 String 存储原理分析

按照上述统计,一个 KV 大概所消耗的内存为 72 字节。那存储的部分都是哪些内容呢?

除了记录实际数据,String 类型还需要额外的内存空间记录数据长度、空间使用等信息,这些信息也叫作元数据。 —— 《极客时间 Redis实践 11 讲》

Redis 在存储 String 的时候,使用的是 动态字符串 SDS 结构,一个对象大概需要如下三个部分:

  1. len,4字节,已使用的长度;
  2. alloc,4字节,实际分配长度;
  3. buf,实际数据存储,结尾"\0";

除了 SDS 的空间使用外,还需要占用一个 RedisObject 对象。

// redis 源码:server.h:620
typedef struct redisObject {
    unsigned type:4;	4个 bit
    unsigned encoding:4;	4个 bit
    unsigned lru:LRU_BITS; /* lru time (relative to server.lruclock) */ 24个 bit
    int refcount; 32 bit
    void *ptr;	64 bit
} robj

这个对象已经需要占用 16 字节。示意图如下

一方面,当保存的是 Long 类型整数时,RedisObject 中的指针就直接赋值为整数数据了,这样就不用额外的指针再指向整数了,节省了指针的空间开销。另一方面,当保存的是字符串数据,并且字符串小于等于 44 字节时,RedisObject 中的元数据、指针和 SDS 是一块连续的内存区域,这样就可以避免内存碎片。

小结一下,我们已经计算来 SDS(动态字符串) + redisObject 两个对象的数据,先假设大约为 40 Byte,我们剩下计算 32 bit 的数据。

2.3 Redis 的全局 Hash 表

Redis 本质上也是一个大的 Hash 表,也就是说每增加一个对象,就需要增加一个如红箭头标记的一个对象。Redis 源码将其命名为: dictEntry

typedef struct dictEntry {
    void *key;
    void *val;
    struct dictEntry *next;
} dictEntry

从源码上看,一个有三个指针,因为使用的是 jemalloc 内存分配库,所以其会分配 32 字节,而不是 24 字节。

至此,就可以比较完整地统计好数据存储的位置了。

2.4 优化分析A 底层存储优化

从此可见,有效的 KV 信息其实并不长,假如是两个 Long 类型的数据,有效的信息只有 16 字节。Redis 设计来一种新的底层存储结构,压缩列表(ziplist),本质上就是将随机指针地址转换为连续地址,通过偏移量进行识别,完成存储。随机寻址和连续寻址也是计算机中最底层的两种技术选型。

使用连续寻址,就可以将多个 KV,共同存储在一个 dictEntry 上。

2.5 优化分析B 数据结构结合业务优化

假如存储的数据就是 K-V 值,K 做一次拆分。譬如每 1000 个为个 Hash 集合,采用 Hash 数据进行存储。本质上,这是一个分治思想,分级存储

def write100wkv_with_pipline_hash():
    print("begin write 100w in hashway")
    pip = client.pipeline(transaction=False)

    before = getRedisUsedMemory()
    for k in range(0, 1000000):
        pip.hset(int(k/1000), k % 1000, 'v' + str(k))
    pip.execute()
    after = getRedisUsedMemory()
    print("end write 100w, using: " + str(after - before)
begin write 100w in hashway
used_memory_human:865.74K
used_memory_human:53.82M
end write 100w, using: 55544192

在默认配置下,hash 存储这批数据,需要 53M,比 72M 优化来一部分。由于切分过程中,可以保证每个 Hash 的 KV 数量为1000,且 KV 值大小是一致的。所以可以增加以下配置,保证 Hash 一直使用 ziplist 做底层数据存储结构。

# 表示用压缩列表保存时哈希集合中的最大元素个数。
hash-max-ziplist-entries 1000
# 表示用压缩列表保存时哈希集合中单个元素的最大长度
hash-max-ziplist-value 64

这两个配置可以保证本次存储一致保持在 ziplist 存储。这样空间利用率是最高效的。存储如下,大概比第一版的存储节省5倍的空间。

begin write 100w in hashway
used_memory_human:865.13K
used_memory_human:14.37M
end write 100w, using: 14185344

3. 实验结论

3.1 Redis 的新认知

这是第一次针对 Redis 实践。结合 Redis 的源码,分析了 Redis 的数据对象存储情况。Redis 的难点并不在业务,而是在于高效,Redis 的接口是简单的。

3.2 Redis 以后用法

这次实验说明了,假如认真对业务分析,再结合 Redis 的数据结构和参数进行定制,是可以获得量级优化的。

4. 引用

  1. 如何查看 Redis 内存占用大小
  2. Redis pipeline 为性能提速
  3. Redis 核心技术和实践:P11

5. 源码

  • 本次实验的测试源码: write_100w_redis.py

推荐阅读