前言
Redis 是目前最火爆的内存数据库之一, 通过在内存中读写数据, 大大提高了读写速度, 可以说 Redis 是实现网站高并发不可或缺的一部分.
我们使用 Redis 时, 会接触 Redis 的 5 种对象类型(字符串, 哈希, 列表, 集合, 有序集合), 丰富的类型是 Redis 相对于 Memcached 等的一大优势. 在了解 Redis 的 5 种对象类型的用法和特点的基础上, 进一步了解 Redis 的内存模型, 对 Redis 的使用有很大帮助, 例如:
1, 估算 Redis 内存使用量. 目前为止, 内存的使用成本仍然相对较高, 使用内存不能无所顾忌; 根据需求合理的评估 Redis 的内存使用量, 选择合适的机器配置, 可以在满足需求的情况下节约成本.
2, 优化内存占用. 了解 Redis 内存模型可以选择更合适的数据类型和编码, 更好的利用 Redis 内存.
3, 分析解决问题. 当 Redis 出现阻塞, 内存占用等问题时, 尽快发现导致问题的原因, 便于分析解决问题.
这篇文章主要介绍 Redis 的内存模型(以 3.0 为例), 包括 Redis 占用内存的情况及如何查询, 不同的对象类型在内存中的编码方式, 内存分配器(jemalloc), 简单动态字符串(SDS),RedisObject 等; 然后在此基础上介绍几个 Redis 内存模型的应用.
一, Redis 内存统计
工欲善其事必先利其器, 在说明 Redis 内存之前首先说明如何统计 Redis 使用内存的情况.
在客户端通过 redis-cli 连接服务器后(后面如无特殊说明, 客户端一律使用 redis-cli), 通过 info 命令可以查看内存使用情况:
info memory
其中, info 命令可以显示 redis 服务器的许多信息, 包括服务器基本信息, CPU, 内存, 持久化, 客户端连接信息等等; memory 是参数, 表示只显示内存相关的信息.
返回结果中比较重要的几个说明如下:
(1)used_memory:Redis 分配器分配的内存总量(单位是字节), 包括使用的虚拟内存(即 swap);Redis 分配器后面会介绍. used_memory_human 只是显示更友好.
(2)used_memory_rss:Redis 进程占据操作系统的内存(单位是字节), 与 top 及 ps 命令看到的值是一致的; 除了分配器分配的内存之外, used_memory_rss 还包括进程运行本身需要的内存, 内存碎片等, 但是不包括虚拟内存.
因此, used_memory 和 used_memory_rss, 前者是从 Redis 角度得到的量, 后者是从操作系统角度得到的量. 二者之所以有所不同, 一方面是因为内存碎片和 Redis 进程运行需要占用内存, 使得前者可能比后者小, 另一方面虚拟内存的存在, 使得前者可能比后者大.
由于在实际应用中, Redis 的数据量会比较大, 此时进程运行占用的内存与 Redis 数据量和内存碎片相比, 都会小得多; 因此 used_memory_rss 和 used_memory 的比例, 便成了衡量 Redis 内存碎片率的参数; 这个参数就是 mem_fragmentation_ratio.
(3)mem_fragmentation_ratio: 内存碎片比率, 该值是 used_memory_rss / used_memory 的比值.
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 大得多.
(4)mem_allocator:Redis 使用的内存分配器, 在编译时指定; 可以是 libc ,jemalloc 或者 tcmalloc, 默认是 jemalloc; 截图中使用的便是默认的 jemalloc.
二, Redis 内存划分
Redis 作为内存数据库, 在内存中存储的内容主要是数据(键值对); 通过前面的叙述可以知道, 除了数据以外, Redis 的其他部分也会占用内存.
Redis 的内存占用主要可以划分为以下几个部分:
1, 数据
作为数据库, 数据是最主要的部分; 这部分占用的内存会统计在 used_memory 中.
Redis 使用键值对存储数据, 其中的值 (对象) 包括 5 种类型, 即字符串, 哈希, 列表, 集合, 有序集合. 这 5 种类型是 Redis 对外提供的, 实际上, 在 Redis 内部, 每种类型可能有 2 种或更多的内部编码实现; 此外, Redis 在存储对象时, 并不是直接将数据扔进内存, 而是会对对象进行各种包装: 如 redisObject,SDS 等; 这篇文章后面将重点介绍 Redis 中数据存储的细节.
2, 进程本身运行需要的内存
Redis 主进程本身运行肯定需要占用内存, 如代码, 常量池等等; 这部分内存大约几兆, 在大多数生产环境中与 Redis 数据占用的内存相比可以忽略. 这部分内存不是由 jemalloc 分配, 因此不会统计在 used_memory 中.
补充说明: 除了主进程外, Redis 创建的子进程运行也会占用内存, 如 Redis 执行 AOF,RDB 重写时创建的子进程. 当然, 这部分内存不属于 Redis 进程, 也不会统计在 used_memory 和 used_memory_rss 中.
3, 缓冲内存
缓冲内存包括:
客户端缓冲区, 复制积压缓冲区, AOF 缓冲区等;
其中, 客户端缓冲存储客户端连接的输入输出缓冲;
复制积压缓冲用于部分复制功能;
AOF 缓冲区用于在进行 AOF 重写时, 保存最近的写入命令.
在了解相应功能之前, 不需要知道这些缓冲的细节; 这部分内存由 jemalloc 分配, 因此会统计在 used_memory 中.
4, 内存碎片
内存碎片是 Redis 在分配, 回收物理内存过程中产生的. 例如, 如果对数据的更改频繁, 而且数据之间的大小相差很大, 可能导致 redis 释放的空间在物理内存中并没有释放, 但 redis 又无法有效利用, 这就形成了内存碎片. 内存碎片不会统计在 used_memory 中.
内存碎片的产生与对数据进行的操作, 数据的特点等都有关; 此外, 与使用的内存分配器也有关系: 如果内存分配器设计合理, 可以尽可能的减少内存碎片的产生. 后面将要说到的 jemalloc 便在控制内存碎片方面做的很好.
如果 Redis 服务器中的内存碎片已经很大, 可以通过安全重启的方式减小内存碎片: 因为重启之后, Redis 重新从备份文件中读取数据, 在内存中进行重排, 为每个数据重新选择合适的内存单元, 减小内存碎片.
三, Redis 数据存储的细节
1, 概述
关于 Redis 数据存储的细节, 涉及到内存分配器(如 jemalloc), 简单动态字符串(SDS),5 种对象类型及内部编码, redisObject. 在讲述具体内容之前, 先说明一下这几个概念之间的关系.
下图是执行 set hello world 时, 所涉及到的数据模型.
- 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;
- struct sdshdr {
- int len;
- int free;
- char buf[];
- };
- typedef struct dictEntry{
- void *key;
- union{
- void *val;
- uint64_tu64;
- int64_ts64;
- }v;
- struct dictEntry *next;
- }dictEntry;
- typedef struct dictht{
- dictEntry **table;
- unsigned long size;
- unsigned long sizemask;
- unsigned long used;
- }dictht;
- typedef struct dict{
- dictType *type;
- void *privdata;
- dictht ht[2];
- int trehashidx;
- } dict;
- typedef struct intset{
- uint32_t encoding;
- uint32_t length;
- int8_t contents[];
- } intset;
- public class RedisTest {
- public static Jedis jedis = new Jedis("localhost", 6379);
- public static void main(String[] args) throws Exception{
- Long m1 = Long.valueOf(getMemory());
- insertData();
- Long m2 = Long.valueOf(getMemory());
- System.out.println(m2 - m1);
- }
- public static void insertData(){
- for(int i = 10000; i < 100000; i++){
- jedis.set("aa" + i, "aa" + i); //key 和 value 长度都是 7 字节, 且不是整数
- }
- }
- public static String getMemory(){
- String memoryAllLine = jedis.info("memory");
- String usedMemoryLine = memoryAllLine.split("\r\n")[1];
- String memory = usedMemoryLine.substring(usedMemoryLine.indexOf(':') + 1);
- return memory;
- }
- }
- public static void insertData(){
- for(int i = 10000; i < 100000; i++){
- jedis.set("aaa" + i, "aaa" + i); //key 和 value 长度都是 8 字节, 且不是整数
- }
- }
- https://redis.io/documentation
- http://redisdoc.com/server/info.html
- https://www.cnblogs.com/lhcpig/p/4769397.html
- https://searchdatabase.techtarget.com.cn/7-20218/
- http://www.cnblogs.com/mushroom/p/4738170.html
- http://www.imooc.com/article/3645
- http://blog.csdn.net/zhengpeitao/article/details/76573053
来源: http://database.51cto.com/art/201807/578934.htm