Redis是目前最流行的内存数据库之一,通过在内存中读写数据,大大提高了读写速度,可以说Redis是实现网站高并发不可或缺的一部分。

我们使用Redis时,会接触Redis的5种对象类型(字符串、哈希、列表、集合、有序集合),丰富的类型是Redis相对于Memcached等的一大优势。在了解Redis的5种对象类型的用法和特点的基础上,进一步了解Redis的内存模型,对Redis的使用有很大帮助,例如:

1、估算Redis内存使用量。目前为止,内存的使用成本仍然相对较高,使用内存不能无所顾忌;根据需求合理的评估Redis的内存使用量,选择合适的机器配置,可以在满足需求的情况下节约成本。

2、Redis内存优化占用。了解Redis内存模型可以选择更合适的数据类型和编码,更好地利用Redis内存。

3、Redis内存耗尽处理机制。当Redis出现内存不足、耗尽时等问题时,它的底层处理机制是怎么样的。

本文将主要从以上三个方向介绍Redis的内存模型,包括Redis占用内存的情况及如何查询、不同的对象类型在内存中的编码方式、内存划分、Redis使用过程中如何节省内存、Redis内存耗尽处理机制等。

一、Redis内存统计

工欲善其事必先利其器,在说明Redis内存之前首先说明如何统计Redis使用内存的情况。在客户端通过redis-cli连接服务器后(后面如无特殊说明,客户端一律使用redis-cli),通过info命令可以查看内存使用情况。 Description

其中,info命令可以显示redis服务器的许多信息,包括服务器基本信息、CPU、内存、持久化、客户端连接信息等等;memory是参数,表示只显示内存相关的信息。返回结果中比较重要的几个说明如下:

1、used_memory:Redis分配器分配的内存总量(单位是字节),包括使用的虚拟内存(即swap);Redis分配器后面会介绍。used_memory_human只是显示更友好。

2、used_memory_rss:Redis进程占据操作系统的内存(单位是字节),与top及ps命令看到的值是一致的;除了分配器分配的内存之外,used_memory_rss还包括进程运行本身需要的内存、内存碎片等,但是不包括虚拟内存。

3、mem_fragmentation_ratio:内存碎片比率,该值是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内存优化占用

众所周知,Redis 的性能之所以如此之高,原因就在于它的数据都存储在「内存」中,所以访问 Redis 中的数据速度极快。

但从资源利用率层面来说,机器的内存资源相比于磁盘,还是比较昂贵的。

当你的业务应用在 Redis 中存储数据很少时,你可能并不太关心内存资源的使用情况。但随着业务的发展,你的业务存储在 Redis 中的数据就会越来越多。

如果没有提前制定好内存优化策略,那么等业务开始增长时,Redis 占用的内存也会开始膨胀。

所以,提前制定合理的内存优化策略,对于资源利用率的提升是很有必要的。

那在使用 Redis 时,怎样做才能更节省内存呢?这里我给你总结了 6 点建议,我们依次来看:

1、控制 key 的长度

最简单直接的内存优化,就是控制 key 的长度。

在开发业务时,你需要提前预估整个 Redis 中写入 key 的数量,如果 key 数量达到了百万级别,那么,过长的 key 名也会占用过多的内存空间。

所以,你需要保证 key 在简单、清晰的前提下,尽可能把 key 定义得短一些。

例如,原有的 key 为 user:book:123,则可以优化为 u:bk:123。

这样一来,你的 Redis 就可以节省大量的内存,这个方案对内存的优化非常直接和高效。

2、避免存储 bigkey

除了控制 key 的长度之外,你同样需要关注 value 的大小,如果大量存储 bigkey,也会导致 Redis 内存增长过快。

除此之外,客户端在读写 bigkey 时,还有产生性能问题(下文会具体详述)。

所以,你要避免在 Redis 中存储 bigkey,我给你的建议是:

String:大小控制在 10KB 以下

List/Hash/Set/ZSet:元素数量控制在 1 万以下

3、选择合适的数据类型

Redis 提供了丰富的数据类型,这些数据类型在实现上,也对内存使用做了优化。具体来说就是,一种数据类型对应多种数据结构来实现:

Description

例如,String、Set 在存储 int 数据时,会采用整数编码存储。Hash、ZSet 在元素数量比较少时(可配置),会采用压缩列表(ziplist)存储,在存储比较多的数据时,才会转换为哈希表和跳表。

作者这么设计的原因,就是为了进一步节约内存资源。

那么你在存储数据时,就可以利用这些特性来优化 Redis 的内存。这里我给你的建议如下:

String、Set:尽可能存储 int 类型数据

Hash、ZSet:存储的元素数量控制在转换阈值之下,以压缩列表存储,节约内存

4、把 Redis 当作缓存使用

Redis 数据存储在内存中,这也意味着其资源是有限的。你在使用 Redis 时,要把它当做缓存来使用,而不是数据库。

所以,你的应用写入到 Redis 中的数据,尽可能地都设置「过期时间」。

业务应用在 Redis 中查不到数据时,再从后端数据库中加载到 Redis 中。

Description

采用这种方案,可以让 Redis 中只保留经常访问的「热数据」,内存利用率也会比较高。

5、实例设置 maxmemory + 淘汰策略

虽然你的 Redis key 都设置了过期时间,但如果你的业务应用写入量很大,并且过期时间设置得比较久,那么短期间内 Redis 的内存依旧会快速增长。

如果不控制 Redis 的内存上限,也会导致使用过多的内存资源。

对于这种场景,你需要提前预估业务数据量,然后给这个实例设置 maxmemory 控制实例的内存上限,这样可以避免 Redis 的内存持续膨胀。

配置了 maxmemory,此时你还要设置数据淘汰策略,而淘汰策略如何选择,你需要结合你的业务特点来决定:

volatile-lru / allkeys-lru:优先保留最近访问过的数据

volatile-lfu / allkeys-lfu:优先保留访问次数最频繁的数据(4.0+版本支持)

volatile-ttl :优先淘汰即将过期的数据

volatile-random / allkeys-random:随机淘汰数据

6、数据压缩后写入 Redis

以上方案基本涵盖了 Redis 内存优化的各个方面。

如果你还想进一步优化 Redis 内存,你还可以在业务应用中先将数据压缩,再写入到 Redis 中(例如采用 snappy、gzip 等压缩算法)。

当然,压缩存储的数据,客户端在读取时还需要解压缩,在这期间会消耗更多 CPU 资源,你需要根据实际情况进行权衡。

四、内存耗尽后Redis会发生什么

在了解Redis内存耗尽前,我们先来了解这三个概念,内存回收,过期策略,淘汰策略。

1、内存回收

使用Redis 服务时,很多情况下某些键值对只会在特定的时间内有效,为了防止这种类型的数据一直占有内存,我们可以给键值对设置有效期。Redis中可以通过 4个独立的命令来给一个键设置过期时间:

expire key ttl:将 key值的过期时间设置为 ttl秒。

pexpire key ttl:将 key值的过期时间设置为 ttl 毫秒。

expireat key timestamp:将 key 值的过期时间设置为指定的 timestamp 秒数。

pexpireat key timestamp:将 key值的过期时间设置为指定的 timestamp 毫秒数。

PS:不管使用哪一个命令,最终 Redis 底层都是使用 pexpireat 命令来实现的。另外,set 等命令也可以设置 key 的同时加上过期时间,这样可以保证设值和设过期时间的原子性。

设置了有效期后,可以通过 ttl 和 pttl 两个命令来查询剩余过期时间(如果未设置过期时间则下面两个命令返回 -1,如果设置了一个非法的过期时间,则都返回 -2):

ttl key 返回 key 剩余过期秒数。

pttl key 返回 key 剩余过期的毫秒数。

2、过期策略

如果将一个过期的键删除,我们一般都会有三种策略:

定时删除:为每个键设置一个定时器,一旦过期时间到了,则将键删除。这种策略对内存很友好,但是对 CPU 不友好,因为每个定时器都会占用一定的 CPU 资源。

惰性删除:不管键有没有过期都不主动删除,等到每次去获取键时再判断是否过期,如果过期就删除该键,否则返回键对应的值。这种策略对内存不够友好,可能会浪费很多内存。

定期扫描:系统每隔一段时间就定期扫描一次,发现过期的键就进行删除。这种策略相对来说是上面两种策略的折中方案,需要注意的是这个定期的频率要结合实际情况掌控好,使用这种方案有一个缺陷就是可能会出现已经过期的键也被返回。

在 Redis 当中,其选择的是策略 2 和策略 3 的综合使用。不过 Redis 的定期扫描只会扫描设置了过期时间的键,因为设置了过期时间的键 Redis 会单独存储,所以不会出现扫描所有键的情况:

typedef struct redisDb {
    dict *dict; //所有的键值对
    dict *expires; //设置了过期时间的键值对
   dict *blocking_keys; //被阻塞的key,如客户端执行BLPOP等阻塞指令时
   dict *watched_keys; //WATCHED keys
   int id; //Database ID
   //... 省略了其他属性
} redisDb;

3、淘汰策略

假如 Redis当中所有的键都没有过期,而且此时内存满了,那么客户端继续执行 set等命令时 Redis会怎么处理呢?Redis当中提供了不同的淘汰策略来处理这种场景。

首先 Redis 提供了一个参数 maxmemory来配置 Redis最大使用内存:maxmemory 。或者也可以通过命令 config set maxmemory 1GB 来动态修改。如果没有设置该参数,那么在 32 位的操作系统中 Redis 最多使用 3GB 内存,而在 64 位的操作系统中则不作限制。

Redis 中提供了 8 种淘汰策略,可以通过参数 maxmemory-policy 进行配置:

Description

PS:淘汰策略也可以直接使用命令 config set maxmemory-policy <策略> 来进行动态配置。其中LRU算法和LFU算法比较重要,更多详情可以自行查阅。