[推荐阅读]Redis 内存使用优化与存储

 

Redis 经常使用数据类型mysql

Redis 最为经常使用的数据类型主要有如下五种:redis

• Stringsql

• Hash数据库

• List数组

• Set数据结构

• Sorted set并发

在具体描述这几种数据类型以前,咱们先经过一张图了解下 Redis 内部内存管理中是如何描述这些不一样数据类型的:运维

 

 

Redis

Redis性能

首先 Redis 内部使用一个 redisObject 对象来表示全部的 key 和 value,redisObject 最主要的信息如上图所示:type  表明一个 value 对象具体是何种数据类型,encoding 是不一样数据类型在 redis 内部的存储方式,好比:type=string 表明 value 存储的是一个普通字符串,那么对应的 encoding 能够是 raw 或者是 int,若是是 int 则表明实际 redis 内部是按数值型类存储和表示这个字符串的,固然前提是这个字符串自己能够用数值表示,好比:"123" "456"这样的字符串。大数据

这里须要特殊说明一下 vm 字段,只有打开了 Redis 的虚拟内存功能,此字段才会真正的分配内存,该功能默认是关闭状态的,该功能会在后面具体描述。经过上图咱们能够发现 Redis 使用 redisObject 来表示全部的 key/value 数据是比较浪费内存的,固然这些内存管理成本的付出主要也是为了给 Redis 不一样数据类型提供一个统一的管理接口,实际做者也提供了多种方法帮助咱们尽可能节省内存使用,咱们随后会具体讨论。

下面咱们先来逐一的分析下这五种数据类型的使用和内部实现方式:

String

经常使用命令:

set,get,decr,incr,mget 等。

应用场景:

String 是最经常使用的一种数据类型,普通的 key/value 存储均可以归为此类,这里就不所作解释了。

实现方式:

String 在 redis 内部存储默认就是一个字符串,被 redisObject 所引用,当遇到 incr,decr 等操做时会转成数值型进行计算,此时 redisObject 的 encoding 字段为int。

Hash

经常使用命令:

hget,hset,hgetall 等。

应用场景:

咱们简单举个实例来描述下 Hash 的应用场景,好比咱们要存储一个用户信息对象数据,包含如下信息:

用户 ID 为查找的 key,存储的 value 用户对象包含姓名,年龄,生日等信息,若是用普通的 key/value 结构来存储,主要有如下2种存储方式:

Redis

Redis

第一种方式将用户 ID 做为查找 key,把其余信息封装成一个对象以序列化的方式存储,这种方式的缺点是,增长了序列化/反序列化的开销,而且在须要修改其中一项信息时,须要把整个对象取回,而且修改操做须要对并发进行保护,引入CAS等复杂问题。

方法二

方法二

第二种方法是这个用户信息对象有多少成员就存成多少个 key-value 对儿,用用户 ID +对应属性的名称做为惟一标识来取得对应属性的值,虽然省去了序列化开销和并发问题,可是用户 ID 为重复存储,若是存在大量这样的数据,内存浪费仍是很是可观的。

那么 Redis 提供的 Hash 很好的解决了这个问题,Redis 的 Hash 实际是内部存储的 Value 为一个 HashMap,并提供了直接存取这个 Map 成员的接口,以下图:

hash

hash

也就是说,Key 仍然是用户 ID,value 是一个 Map,这个 Map 的 key 是成员的属性名,value 是属性值,这样对数据的修改和存取均可以直接经过其内部 Map 的 Key(Redis 里称内部 Map 的 key 为 field),也就是经过 key(用户 ID) + field(属性标签)就能够操做对应属性数据了,既不须要重复存储数据,也不会带来序列化和并发修改控制的问题。很好的解决了问题。

这里同时须要注意,Redis 提供了接口(hgetall)能够直接取到所有的属性数据,可是若是内部 Map 的成员不少,那么涉及到遍历整个内部 Map 的操做,因为 Redis 单线程模型的缘故,这个遍历操做可能会比较耗时,而另其它客户端的请求彻底不响应,这点须要格外注意。

实现方式:

上面已经说到 Redis Hash 对应 Value 内部实际就是一个 HashMap,实际这里会有2种不一样实现,这个 Hash 的成员比较少时 Redis 为了节省内存会采用相似一维数组的方式来紧凑存储,而不会采用真正的 HashMap 结构,对应的 value redisObject 的 encoding 为 zipmap,当成员数量增大时会自动转成真正的 HashMap,此时 encoding 为 ht。

List

经常使用命令:

lpush,rpush,lpop,rpop,lrange等。

应用场景:

Redis list 的应用场景很是多,也是 Redis 最重要的数据结构之一,好比 twitter 的关注列表,粉丝列表等均可以用 Redis 的 list 结构来实现,比较好理解,这里再也不重复。

实现方式:

Redis list 的实现为一个双向链表,便可以支持反向查找和遍历,更方便操做,不过带来了部分额外的内存开销,Redis 内部的不少实现,包括发送缓冲队列等也都是用的这个数据结构。

Set

经常使用命令:

sadd,spop,smembers,sunion 等。

应用场景:

Redis set 对外提供的功能与 list 相似是一个列表的功能,特殊之处在于 set 是能够自动排重的,当你须要存储一个列表数据,又不但愿出现重复数据时,set 是一个很好的选择,而且 set 提供了判断某个成员是否在一个 set 集合内的重要接口,这个也是 list 所不能提供的。

实现方式:

set 的内部实现是一个 value 永远为 null 的 HashMap,实际就是经过计算 hash 的方式来快速排重的,这也是 set 能提供判断一个成员是否在集合内的缘由。

Sorted set

经常使用命令:

zadd,zrange,zrem,zcard等

使用场景:

Redis sorted set 的使用场景与 set 相似,区别是 set 不是自动有序的,而 sorted set 能够经过用户额外提供一个优先级(score)的参数来为成员排序,而且是插入有序的,即自动排序。当你须要一个有序的而且不重复的集合列表,那么能够选择 sorted set 数据结构,好比 twitter 的 public timeline 能够以发表时间做为 score 来存储,这样获取时就是自动按时间排好序的。

实现方式:

Redis sorted set 的内部使用 HashMap 和跳跃表(SkipList)来保证数据的存储和有序,HashMap 里放的是成员到 score 的映射,而跳跃表里存放的是全部的成员,排序依据是 HashMap 里存的 score,使用跳跃表的结构能够得到比较高的查找效率,而且在实现上比较简单。

经常使用内存优化手段与参数

经过咱们上面的一些实现上的分析能够看出 redis 实际上的内存管理成本很是高,即占用了过多的内存,做者对这点也很是清楚,因此提供了一系列的参数和手段来控制和节省内存,咱们分别来讨论下。

首先最重要的一点是不要开启 Redis 的 VM 选项,即虚拟内存功能,这个原本是做为 Redis 存储超出物理内存数据的一种数据在内存与磁盘换入换出的一个持久化策略,可是其内存管理成本也很是的高,而且咱们后续会分析此种持久化策略并不成熟,因此要关闭 VM 功能,请检查你的 redis.conf 文件中 vm-enabled 为 no。

其次最好设置下 redis.conf 中的 maxmemory 选项,该选项是告诉 Redis 当使用了多少物理内存后就开始拒绝后续的写入请求,该参数能很好的保护好你的 Redis 不会由于使用了过多的物理内存而致使 swap,最终严重影响性能甚至崩溃。

另外 Redis 为不一样数据类型分别提供了一组参数来控制内存使用,咱们在前面详细分析过 Redis Hash 是 value 内部为一个 HashMap,若是该 Map 的成员数比较少,则会采用相似一维线性的紧凑格式来存储该 Map,即省去了大量指针的内存开销,这个参数控制对应在 redis.conf 配置文件中下面2项:

1

2

3

hash-max-zipmap-entries 64

hash-max-zipmap-value 512

hash-max-zipmap-entries

含义是当 value 这个 Map 内部不超过多少个成员时会采用线性紧凑格式存储,默认是64,即 value 内部有64个如下的成员就是使用线性紧凑存储,超过该值自动转成真正的 HashMap。

hash-max-zipmap-value 含义是当 value 这个 Map 内部的每一个成员值长度不超过多少字节就会采用线性紧凑存储来节省空间。

以上2个条件任意一个条件超过设置值都会转换成真正的 HashMap,也就不会再节省内存了,那么这个值是否是设置的越大越好呢,答案固然是否认的,HashMap 的优点就是查找和操做的时间复杂度都是 O(1) 的,而放弃 Hash 采用一维存储则是 O(n) 的时间复杂度,若是成员数量不多,则影响不大,不然会严重影响性能,因此要权衡好这个值的设置,整体上仍是最根本的时间成本和空间成本上的权衡。

一样相似的参数还有:

1

list-max-ziplist-entries 512

说明:list 数据类型多少节点如下会采用去指针的紧凑存储格式。

1

list-max-ziplist-value 64

说明:list 数据类型节点值大小小于多少字节会采用紧凑存储格式。

1

set-max-intset-entries 512

说明:set 数据类型内部数据若是所有是数值型,且包含多少节点如下会采用紧凑格式存储。

最后想说的是 Redis 内部实现没有对内存分配方面作过多的优化,在必定程度上会存在内存碎片,不过大多数状况下这个不会成为 Redis 的性能瓶 颈,不过若是在 Redis 内部存储的大部分数据是数值型的话,Redis 内部采用了一个 shared integer 的方式来省去分配内存的开销,即在系统启动时先分配一个从 1~n 那么多个数值对象放在一个池子中,若是存储的数据刚好是这个数值范围内的数据,则直接从池子里取出该对象,而且经过引用计数的方式来共享,这样在系统存储了大量数值下,也能必定程度上节省内存而且提升性能,这个参数值 n 的设置须要修改源代码中的一行宏定义 REDIS_SHARED_INTEGERS,该值 默认是 10000,能够根据本身的须要进行修改,修改后从新编译就能够了。

Redis 的持久化机制

Redis 因为支持很是丰富的内存数据结构类型,如何把这些复杂的内存组织方式持久化到磁盘上是一个难题,因此 Redis 的持久化方式与传统数据库的方式有比较多的差异,Redis 一共支持四种持久化方式,分别是:

- 定时快照方式(snapshot)

- 基于语句追加文件的方式(aof)

- 虚拟内存(vm)

- Diskstore 方式

在设计思路上,前两种是基于所有数据都在内存中,即小数据量下提供磁盘落地功能,然后两种方式则是做者在尝试存储数据超过物理内存时,即大数据量的数据存储,截止到本文,后两种持久化方式仍然是在实验阶段,而且 vm 方式基本已经被做者放弃,因此实际能在生产环境用的只有前两种,换句话说 Redis 目前还只能做为小数据量存储(所有数据可以加载在内存中),海量数据存储方面并非 Redis 所擅长的领域。下面分别介绍下这几种持久化方式:

定时快照方式(snapshot):

该持久化方式实际是在 Redis 内部一个定时器事件,每隔固定时间去检查当前数据发生的改变次数与时间是否知足配置的持久化触发的条件,若是知足则经过操做系统 fork 调用来建立出一个子进程,这个子进程默认会与父进程共享相同的地址空间,这时就能够经过子进程来遍历整个内存来进行存储操做,而主进程则仍然能够提供服务,当有写入时由操做系统按照内存页(page)为单位来进行 copy-on-write 保证父子进程之间不会互相影响。

该持久化的主要缺点是定时快照只是表明一段时间内的内存映像,因此系统重启会丢失上次快照与重启之间全部的数据。

基于语句追加方式(aof

aof 方式实际相似 mysql 基于语句的 binlog 方式,即每条会使 Redis 内存数据发生改变的命令都会追加到一个 log 文件中,也就是说这个 log 文件就是 Redis 的持久化数据。

aof 的方式的主要缺点是追加 log 文件可能致使体积过大,当系统重启恢复数据时若是是 aof 的方式则加载数据会很是慢,几十G的数据可能须要几小时才能加载完,固然这个耗时并非由于磁盘文件读取速度慢,而是因为读取的全部命令都要在内存中执行一遍。另外因为每条命令都要写 log,因此使用 aof 的方式,Redis 的读写性能也会有所降低。

虚拟内存方式:

虚拟内存方式是 Redis 来进行用户空间的数据换入换出的一个策略,此种方式在实现的效果上比较差,主要问题是代码复杂,重启慢,复制慢等等,目前已经被做者放弃。

diskstore 方式:

diskstore 方式是做者放弃了虚拟内存方式后选择的一种新的实现方式,也就是传统的 B-tree 的方式,目前仍在实验阶段,后续是否可用咱们能够拭目以待。

Redis 持久化磁盘 IO 方式及其带来的问题

有 Redis 线上运维经验的人会发现 Redis 在物理内存使用比较多,但尚未超过实际物理内存总容量时就会发生不稳定甚至崩溃的问题,有人认为是基于快照方式持久化的 fork 系统调用形成内存占用加倍而致使的,这种观点是不许确的,由于 fork 调用的 copy-on-write 机制是基于操做系统页这个单位的,也就是只有有写入的脏页会被复制,可是通常你的系统不会在短期内全部的页都发生了写入而致使复制,那么是什么缘由致使 Redis 崩溃的呢?

答案是 Redis 的持久化使用了 Buffer IO 形成的,所谓 Buffer IO 是指 Redis 对持久化文件的写入和读取操做都会使用物理内存的 Page Cache,而大多数数据库系统会使用 Direct IO 来绕过这层 Page Cache 并自行维护一个数据的 Cache,而当 Redis 的持久化文件过大(尤为是快照文件),并对其进行读写时,磁盘文件中的数据都会被加载到物理内 存中做为操做系统对该文件的一层 Cache,而这层 Cache 的数据与 Redis 内存中管理的数据实际是重复存储的,虽然内核在物理内存紧张时会作 Page Cache 的剔除工做,但内核极可能认为某块 Page Cache 更重要,而让你的进程开始 Swap,这时你的系统就会开始出现不稳定或者崩溃了。咱们的经验是当你的 Redis 物理内存使用超过内存总容量的3/5时就会开始比较危险了。

下图是 Redis 在读取或者写入快照文件 dump.rdb 后的内存数据图:

虚拟地址到物理地址间的映射

虚拟地址到物理地址间的映射

总结:

1. 根据业务须要选择合适的数据类型,并为不一样的应用场景设置相应的紧凑存储参数。

2. 当业务场景不须要数据持久化时,关闭全部的持久化方式能够得到最佳的性能以及最大的内存使用量。

3. 若是须要使用持久化,根据是否能够容忍重启丢失部分数据在快照方式与语句追加方式之间选择其一,不要使用虚拟内存以及 diskstore 方式。

4. 不要让你的 Redis 所在机器物理内存使用超过实际内存总量的3/5。

本文转自:http://www.linkedkeeper.com/detail/blog.action?bid=121

相关文章
相关标签/搜索