Redis缓存数据库详解
Redis最为常⽤的数据类型主要有以下五种:
1)String
2)Hash
3)List
4)Set
5)Sorted set
在具体描述这⼏种数据类型之前,我们先通过⼀张图了解下Redis内部内存管理中是如何描述这些不同数据类型的:
⾸先Redis内部使⽤⼀个redisObject对象来表⽰所有的key和value,redisObject最主要的信息如上图所⽰:type代表⼀个value对象具体是何种数据类型,encoding是不同数据类型在redis内部的存储⽅式,⽐如:type=string代表value存储的是⼀个普通字符串,那么对应的encoding可以是raw或者是int,如果是int则代表实际redis内部是按数值型类存储和表⽰这个字符串的,当然前提是这个字符串本⾝可以⽤数值表⽰,⽐如:"123" "456"这样的字符串。
redis支持的数据结构这⾥需要特殊说明⼀下vm字段,只有打开了Redis的虚拟内存功能,此字段才会真正的分配内存,该
功能默认是关闭状态的,该功能会在后⾯具体描述。通过上图我们可以发现Redis使⽤redisObject来表⽰所有的key/value数据是⽐较浪费内存的,当然这些内存管理成本的付出主要也是为了给Redis不同数据类型提供⼀个统⼀的管理接⼝,实际作者也提供了多种⽅法帮助我们尽量节省内存使⽤,我们随后会具体讨论。
下⾯我们先来逐⼀的分析下这五种数据类型的使⽤和内部实现⽅式:
1)String
常⽤命令:
set,get,decr,incr,mget 等。
应⽤场景:
String是最常⽤的⼀种数据类型,普通的key/value存储都可以归为此类,这⾥就不所做解释了。
实现⽅式:
String在redis内部存储默认就是⼀个字符串,被redisObject所引⽤,当遇到incr,decr等操作时会转成数值型进⾏计算,此时redisObject的encoding字段为int。
2)Hash
常⽤命令:
hget,hset,hgetall 等。
应⽤场景:
我们简单举个实例来描述下Hash的应⽤场景,⽐如我们要存储⼀个⽤户信息对象数据,包含以下信息:
⽤户ID为查的key,存储的value⽤户对象包含姓名,年龄,⽣⽇等信息,如果⽤普通的key/value结构来存储,主要有以下2种存储⽅式:
第⼀种⽅式将⽤户ID作为查key,把其他信息封装成⼀个对象以序列化的⽅式存储,这种⽅式的缺点是,增加了序列化/反序列化的开销,并且在需要修改其中⼀项信息时,需要把整个对象取回,并且修改操作需要对并发进⾏保护,引⼊CAS等复杂问题。
第⼆种⽅法是这个⽤户信息对象有多少成员就存成多少个key-value对⼉,⽤⽤户ID+对应属性的名称作为唯⼀标识来取得对应属性的值,虽然省去了序列化开销和并发问题,但是⽤户ID为重复存储,如果存在⼤量这样的数据,内存浪费还是⾮常可观的。
那么Redis提供的Hash很好的解决了这个问题,Redis的Hash实际是内部存储的Value为⼀个HashMap,并提供了直接存取这个Map成员的接⼝。
也就是说,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。
3)List
常⽤命令:
lpush,rpush,lpop,rpop,lrange等。
应⽤场景:
Redis list的应⽤场景⾮常多,也是Redis最重要的数据结构之⼀,⽐如twitter的关注列表,粉丝列表等都可以⽤Redis的list结构来实现,⽐较好理解,这⾥不再重复。
实现⽅式:
Redis list的实现为⼀个双向链表,即可以⽀持反向查和遍历,更⽅便操作,不过带来了部分额外的内存开销,Redis内部的很多实现,包括发送缓冲队列等也都是⽤的这个数据结构。
4)Set
常⽤命令:
sadd,spop,smembers,sunion 等。
应⽤场景:
Redis set对外提供的功能与list类似是⼀个列表的功能,特殊之处在于set是可以⾃动排重的,当你需要存储⼀个列表数据,⼜不希望出现重复数据时,set是⼀个很好的选择,并且set提供了判断某个成员是否在⼀个set集合内的重要接⼝,这个也是list所不能提供的。
实现⽅式:
set 的内部实现是⼀个 value永远为null的HashMap,实际就是通过计算hash的⽅式来快速排重的,这也是set能提供判断⼀个成员是否在集合内的原因。
5)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实际上的内存管理成本⾮常⾼,即占⽤了过多的内存,作者对这点也⾮常清楚,所以提供了⼀系列的参数和⼿段来控制和节省内存。
1)⾸先最重要的⼀点是不要开启Redis的VM选项,即虚拟内存功能,这个本来是作为Redis存储超出物理内存数据的⼀种数据在内存与磁盘换⼊换出的⼀个持久化策略,但是其内存管理成本也⾮常的⾼,并且我们后续会分析此种持久化策略并不成熟,所以要关闭VM功能,请检查你的f⽂件中 vm-enabled 为 no。
2)其次最好设置下f中的maxmemory选项,该选项是告诉Redis当使⽤了多少物理内存后就开始拒绝后续的写⼊请求,该参数能很好的保护好你的Redis不会因为使⽤了过多的物理内存⽽导致swap,最终严重影响性能甚⾄崩溃。
3)另外Redis为不同数据类型分别提供了⼀组参数来控制内存使⽤,我们在前⾯详细分析过Redis Ha
sh是value内部为⼀个HashMap,如果该Map的成员数⽐较少,则会采⽤类似⼀维线性的紧凑格式来存储该Map, 即省去了⼤量指针的内存开销,这个参数控制对应在f配置⽂件中下⾯2项:
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)的时间复杂度,如果
成员数量很少,则影响不⼤,否则会严重影响性能,所以要权衡好这个值的设置,总体上还是最根本的时间成本和空间成本上的权衡。
同样类似的参数还有:
list-max-ziplist-entries 512说明:list数据类型多少节点以下会采⽤去指针的紧凑存储格式。
list-max-ziplist-value 64 说明:list数据类型节点值⼤⼩⼩于多少字节会采⽤紧凑存储格式。
set-max-intset-entries 512 说明:set数据类型内部数据如果全部是数值型,且包含多少节点以下会采⽤紧凑格式存储。
4)最后想说的是Redis内部实现没有对内存分配⽅⾯做过多的优化,在⼀定程度上会存在内存碎⽚,不过⼤多数情况下这个不会成为Redis的性能瓶颈,不过如果在Redis内部存储的⼤部分数据是数值型的话,Redis内部采⽤了⼀个shared integer的⽅式来省去分配内存的开销,即在系统启动时先分配⼀个从1~n 那么多个数值对象放在⼀个池⼦中,如果存储的数据恰好是这个数值范围内的数据,则直接从池⼦⾥取出该对象,并且通过引⽤计数的⽅式来共享,这样在系统存储了⼤量数值下,也能⼀定程度上节省内存并且提⾼性能,这个参数值n的设置需要修改源代码中的⼀⾏宏定义REDIS_SHARED_INTEGERS,该值默认是10000,可以根据⾃⼰的需要进⾏修改,修改后重新编译就可以了。
Redis的持久化机制
Redis由于⽀持⾮常丰富的内存数据结构类型,如何把这些复杂的内存组织⽅式持久化到磁盘上是⼀个难题,所以Redis的持久化⽅式与传统数据库的⽅式有⽐较多的差别,Redis⼀共⽀持四种持久化⽅式,分别是:
1定时快照⽅式(snapshot)
2)基于语句追加⽂件的⽅式(aof)
3)虚拟内存(vm)
4)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后的内存数据图:

版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。