原文链接: http://www.infoq.com/cn/articles/tq-redis-memory-usage-optimization-storage
Redis常见数据模型的使用场景以及在内存优化方面和性能优化方面的分析:
常见类型:String、 Hash、 set、 sorted set、 list 五种。。。。。
五种数据类型是在内存管理中的描述:
首先Redis内部使用一个redisObject对象来表示所有的key和value,如下图所讲,type代表一个value对象具体是何种数据类型,encoding
是不同数据类型在redis内部的存储方式,比如,type=string代表value存储的是一个普通字符串,那么对应的encoding可以是raw或int,如果是int则代表实际redis内部是按数值类型存储和表示这个type的string。。当然这个字符串本是可以用数值表示。
vm字段:redis的虚拟内存功能只有打开了,此字段才会真正的分配内存,该功能默认是关闭的。
分析五中数据类型的使用和内部实现方式:
String :常用命令:set-- get---decr---incr---mget--等
应用场景:String是最常用的一种数据类型,普通的key'value存储都可以归为此类,
实现方式:String在redis内部默认是就是一个字符串,被redisObject所引用,当遇到incr,decr等操作时,会转成数值型进行计算。此时的redisObject的encoding字段为int。
Hash :常用命令----hget,hset,hgetall等
应用场景:用存储一个用户信息对象数据为例:
用户ID为查找的key,存储value用户对象包含姓名,年龄,生日等信息。如果用普通的key,value结构来存储,主要是下面两种存储方式:
这种方式将用户ID作为查找key,把其他信息封装成一个对象,以序列化的方式存储,这种方式的缺点,增加了序列化\反序列化的开销,并且在需要修改其中一项信息时,需要把整个对象取回,并且修改操作需要对并发进行保护,引入CAS等复杂问题。
上面的第二种方法是这个用户信息对象有多少成员就存成多少个key-value对儿。用用户ID+对应属性的名称作为以为标识来取得属性的值。但这样造成了ID重复存取。
这样的话,使用Hash结构就可以更好的处理了:
Redis的hash实际是内部存储的value为一个hashmap,并踢狗了直接存取这个Map成员的接口,如下图:
这样key仍然是用户ID,value是一个Map,这个Map的key是成员的属性名,value是属性值。这样 对数据的修改和存取都可以直接通过内部Map的key(Redis里称内部map的key为field)。也就是通过key(用户id)+field(属性标签)就可以操作对应的属性数据了。既不需要重复存储数据,也不会带来序列化和并发修改控制的问题。
但这里需注意的是hgetall命令,这个接口命令可以取到全部的属性数据,但是如果内部Map的成员很多,那么涉及到遍历整个内部Map的操作,由于Redis单线程模型的缘故,这个遍历操作可能会比较耗时。。
实现方式:
上面提到Redis的hash对应的内部value内部实际就是一个HashMap,而实际这里会有两种不同实现,这个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等
应用场景:对外提供的功能与list类似是一个列表的功能。特殊之处,在于set是可以自动排重的,当你需要存储一个列表数据,又不希望出现重复数据时,set是一个很好的选择,并且set提供了判断某个成员是否在一个set集合内的重要接口,这个也是list所不能提供的。
实现方式:set 的内部实现是一个value 永远为nullHashMap,实际就是通过计算hash的方式来快速排重的,这也是set能提供判断一个成员是否在集合内的原因。
Sorted set :常用命令:zadd、zrange、zrem、zcard等。
使用场景:使用与set类似。区别是set不是自动有序的。而sorted set可以通过用户额外提供一个优先级score 的参数来为成员排序,并且插入是有序的。即自动排序。当你需要一个有序的并且不重复的集合列表,那么可以选择sorted set数据结构。比如twitter的public timeline可以以发表时间作为score来存储,这样获取时就是自动按时间排序的。
实现方式:redis sorted set的内部霍思燕那个hashMap和跳跃表(SkipList)来保证数据的存储和有序,hashMap里放的是成员到score的映射,而跳跃表里存放的是所有的成员,排序依据的是hashMap里存放的score,使用跳跃表的结构可以获得比较高的查找效率,并且在实现上比较简单。
常用内存优化手段与参数:
通过上面的实现上的分析,可以看出redis的内存管理成本比较高,即占用了过多的内存,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配置文件中下面两项:
hash-max-zipmap-entries 64
hash-max-zipmap-value 512
hash-max-zipmap-entres
含义是当value这个map内部不超过多少成员时会采用线性紧凑格式存储,默认是64,即 alue内部有64个以下的成员就是使用线性紧凑存储,超过该值就自动转成真正的hashMap。
hash-max-zipmap-value 含义是当alue 这个map内部的每个成员值长度不超过多少字节就会采用线性紧凑存储来节省空间。
以上两个条件,任意一条超过设置就会转成真正的hashmap,也就不会再节省内存了,那么这个值是不是设置的越大越好呢。答案当然是否定的,hashmap的优势就是查找和操作的时间复杂度都是o(1)的,而放弃hash采用一维存储则是o(n)的时间复杂度,如果成员数量很少,则影响不大,否则严重影响性能,所以要权衡这个值的设置。总体上是最根本的时间成本和空间成本上的权衡。
同类参数还有:
list-max-ziplist-entries 512
说明:list数据类型多少节点以下会采用去指针的紧凑存储格式。
list-max-ziplist-value 64
说明:list数据类型节点值大小系哦啊与多少字节会采用紧凑存储格式。
set-max-inset-entries 512
说明set数据类型内部数据如果全部是数值型,且包含多少字节点以下,会采用紧凑存储格式。
redis内部实现没有对内存分配方面做过多的优化,一定程度上回存在内存碎片,不过大多数的情况下,这个不会成为redis的性能瓶颈。不过如果在redis内部存储的大部分是数值型的话,redis内部采用了一个shared integer的方式来省去分配内存的开销,即在系统启动是先分配一个从1~n那么多个数值对象放在一个池子中,如果存储的数据恰好是这个数值范围内的数据,则直接诶从池子里取出对象。并且通过引用技术的方式来分享。这样在系统存储了大量数值下,也能在一定程度上节省内存并且提高ixngneng,这个参数值n的设置需要修改源代码中的一行宏定义:REDIS_SHARED_INTERGERS,该值默认为10000,可以根据自己的需要进行修改,修改后重新编译就可以了。
redis的持久化机制:
四种持久化方式:
定时快照方式---snapshot----------定时器事件---固定时间点检查当前数据发生的改变次数与时间是否满足触发持久化的条件。满 足时,就通过fork调用来创建一个子进程。
这个子进程默认会与父进程共享相同的地址空间,这时就可以通过子进程来遍历整个内存来进行存储操作,而主进程则仍然可 以提供服务,当有写入时由操作系统按照内存页(page)为单位来进行copy-on-write保证父子进程之间不会互相影响。
该持久化的主要 缺点是定时快照只是代表一段时间内的内存映像,所以系统重启会丢失上次快照与重启之间所有的数据
基于语句追加文件的方式------aof-------类似mysql基于语句的binlog方式,即每条会使redis内存数据发生改变的命令都会追加到 一个log文件中,也就是说这个log文件就是redis的持久化数据。
缺点是:追加log文件可能导致体积过大,当系统重启恢复数据时如果是aof的方式则加载数据会非常缓慢
虚拟内存----vm--------已被遗弃。。。。
Diskstore方式-------B-tree
设计思路上,前两种基于全部数据都在内存中,即小数据量下提供磁盘落地功能。
后两种方式则是作者在尝试存储数据超过物理内存时,即大数据量的数据存储。仍在实验阶段
redis持久化磁盘IO方式及其带来的问题。
redis 崩溃的一个原因:
redis的持久化使用了buffer IO造成的,所谓buffer IO指redis对持久化文件的写入和读取操作都会使用物理内存page cache 而大多数数据库系统会使用direct IO来绕过这层page cache 并自行维护一个数据cache,。而当redis的持久化文件过大,尤其是快照文件。并对其读写时,磁盘文件中数据都会被加载到物理内存中作为操作系统对该文件的一层的cache。而这层cache的数据与redis内存中管理的数据实际是重复存储的,虽然内核在物理内存紧张时会做page cache 的剔除工作,
但内核很可能会认为某个page cache 会更重要,而让你的进程开始swap,这时你的系统就会开始出现不稳定或者崩溃了。
补充:
page cache,又称pcache,其中文名称为
页高速缓冲存储器
,简称页高缓
。page cache的大小为一页,通常为4K。在linux读 写文件时,它用于缓存文件的逻辑内容,从而加快对磁盘上映像和数据的访问。
经验:当你的redis物理内存使用超过内存总容量的3/5,就会开始比较危险了
总结:
1、根据业务需要选择核实的数据类型,并为不同的应用场景设置相应的紧凑存储参数
2、当业务场景不需要数据持久化时,关闭所有的持久化方式可以获得最佳的性能以及做大的内存使用量。
3、如果需要持久化,根据是否可以容忍重启丢失部分数据在快照方式与语句追加方式之间选择其一,不要使用虚拟内存以及diskstore方式
4、不要让你的redis所在机器物理内存使用超过实际内存总量的3/5。。