[Redis 实验室 1] Redis 源代码分析 100W 数据的内存使用和优化
前言
实验内容源自极客时间 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 结构,一个对象大概需要如下三个部分:
- len,4字节,已使用的长度;
- alloc,4字节,实际分配长度;
- 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. 引用
- 如何查看 Redis 内存占用大小
- Redis pipeline 为性能提速
- Redis 核心技术和实践:P11
5. 源码
- 本次实验的测试源码: write_100w_redis.py
上一篇: 正常的 CPU 使用率是多少?
下一篇: 哪种 json 格式化格式最好用?
推荐阅读
-
【2022新手指南】Java编程进阶之路 - 六、技术架构篇 ### MySQL索引底层解析与优化实战 - 你会讲解MySQL索引的数据结构吗?性能调优技巧知多少? - Redis深度揭秘:你知道多少?从基础到哨兵、主从复制全梳理 - Redis持久化及哨兵模式详解,还有集群搭建和Leader选举黑箱打开 - Zookeeper是个啥?特性和应用场景大公开 - ZooKeeper集群搭建攻略及 Leader选举、读写一致性、共享锁实现细节 - 探究ZooKeeper中的Leader选举机制及其在分布式环境中的作用 - Zab协议深入剖析:原理、功能与在Zookeeper中的核心地位 - RabbitMQ全方位解读:工作模式、消费限流、可靠投递与配置策略 - 设计者视角:RabbitMQ过期时间、死信队列与延时队列实践指南 - RocketMQ特性和应用场景揭示:理解其精髓与差异化优势 - Kafka详细介绍:特性及广泛应用于实时数据处理的场景解析 - ElasticSearch实力揭秘:特性概述与作为搜索引擎的广泛应用 - MongoDB认知升级:非关系型数据库的优势阐述,安装与使用实战教学 - BIO/NIO/AIO网络模型对比:掌握它们的区别与在网络编程中的实际应用 - Netty带你飞:理解其超快速度背后的秘密,包括线程模型分析 - 网络通信黑科技:Netty编解码原理与常用编解码器的应用,Protostuff实战演示 - 解密Netty粘包与拆包现象,怎样有效应对这一常见问题 - 自定义Netty心跳检测机制,轻松调整检测间隔时间的艺术 - Dubbo轻骑兵介绍:核心特性概览,服务降级实战与其实现益处 - Dubbo三大神器解读:本地存根与本地伪装的实战运用与优势呈现 ----------------------- 七、结语与回顾
-
[Redis 实验室 1] Redis 源代码分析 100W 数据的内存使用和优化