Redis
..........Redis
的理解?Redis
是ANSI C
语言编写的一个基于内存的高性能键值对(key-value
)的NoSQL
数据库,一般用于架设在Java程序与数据库之间用作缓存层,为了防止DB磁盘IO效率过低造成的请求阻塞、响应缓慢等问题,用来弥补DB与Java程序之间的性能差距,同时,也可以在DB吞吐跟不上系统并发量时,避免请求直接落入DB从而起到保护DB的作用。Redis
一般除了缓存DB数据之外还可以利用它丰富的数据类型及指令来实现一些其他功能,比如:计数器、用户在线状态、排行榜、session
存储等,同时Redis
的性能也非常可观,通过官方给出的数据显示能够达到10w/s的QPS处理,但是在生产环境的实测结果大概读取QPS在7-9w/s,写入QPS在6-8w/s左右(注:与机器性能也有关),同时Redis
也提供事务、持久化、高可用等一些机制的支持。Redis
的一些常用指令。Redis
常用的一些命令的话一般是都是对于基本数据类型的操作指令以及一些全局指令.....叭啦叭啦叭......,如下:当然了,一般也是记得一些常用的命令,但是 更多命令参考:Redis命令大全,因为Redis 命令和JVM参数一样,只要记得可以这样做就行了,但是具体的可以去参考相关文档资料。 |
Redis
的基本数据类型以及你是在项目中怎么使用它们的吧!Redis
数据类型在之前是五种,但是现在的版本中存在九种,分别为:字符串(strings/string
)、散列(hashes/hash
)、列表(lists/list
)、集合(sets/set
)、有序集合(sorted sets/zset
)以及后续的四种数据类型:bitmaps、hyperloglogs
、地理空间(geospatial
)、消息(Streams
),不过无论是哪种数据类型Redis
都不会直接将它放在内存中存储,而是转而内部使用RedisObject
来存储以及表示所有类型的key-value
(说着说着我拿出了纸和笔,给面试官画了一张图):Redis
内部使用一个RedisObject
对象来表示所有的key
和value
,RedisObject
最主要的信息如上图所示:type
表示一个value
对象具体是何种数据类型,encoding
是不同数据类型在Redis
内部的存储方式。比如:type=string
表示value
存储的是一个普通字符串,那么encoding
可以是raw
或者int
,而关于其他数据类型的内部编码实现我顿时再拿起笔chua~ chua~ chua
:
Redis
的基本数据类型以及它们的应用场景:
PS:
HyperLogLog
的优点是,在输入元素的数量或者体积非常非常大时,计算基数所需的空间总是固定 的、并且是很小的。在Redis
里面,每个HyperLogLog
键只需要花费12 KB
内存,就可以计算接近2^64
个不同元素的基数。这和计算基数时,元素越多耗费内存就越多的集合形成鲜明对比。但是,因为HyperLogLog
只会根据输入元素来计算基数,而不会储存输入元素本身,所以HyperLogLog
不能像集合那样,返回输入的各个元素(核心是基数估算算法,最终数值存在一定误差误差范围:基数估计的结果是一个带有0.81%
标准错误的近似值,耗空间极小,每个hyperloglog key
占用了12K的内存用于标记基数,pfadd
命令不是一次性分配12K
内存使用,会随着基数的增加内存逐渐增大,Pfmerge
命令合并后占用的存储空间为12K
,无论合并之前数据量多少)
面试官提问: 那么你们在使用Redis
做为缓存层的时候是怎么通过Java操作Redis
的呢?
我的心理: 这问题不是送命题吗.....
我: Java操作Redis
的客户端有很多,比如springData
中的RedisTemplate
,也有SpringCache
集成Redis
后的注解形式,当然也会有一些Jedis、Lettuce、Redisson
等等,而我们使用的是Lettuce
以及Redisson........
面试官提问: 那你们在使用Redis
作为缓存的时候有没有遇到什么问题呢?
我: 咳咳,是的,确实遇到了以及考虑到了一些问题,比如缓存一致性、雪崩、穿透与击穿,关于Redis
与MySQL
之间的数据一致性问题其实也考虑过很多方案,比如先删后改,延时双删等等很多方案,但是在高并发情况下还是会造成数据的不一致性,所以关于DB与缓存之间的强一致性一定要保证的话那么就对于这部分数据不要做缓存,操作直接走DB,但是如果这个数据比较热点的话那么还是会给DB造成很大的压力,所以在我们的项目中还是采用先删再改+过期的方案来做的,虽然也会存在数据的不一致,但是勉强也能接受,因为毕竟使用缓存访问快的同时也能减轻DB压力,而且本身采用缓存就需要接受一定的数据延迟性和短暂的不一致性,我们只能采取合适的策略来降低缓存和数据库间数据不一致的概率,而无法保证两者间的强一致性。合适的策略包括合适的缓存更新策略,合适的缓存淘汰策略,更新数据库后及时更新缓存、缓存失败时增加重试机制等。
面试官话锋一转: 打断一下,你刚刚提到了使用缓存能让访问变快,那么你能不能讲讲Redis
为什么快呢?
我的心理: 好家伙,这一手来的我猝不及防......
硬着头发回答: Redis
快的原因嘛其实可以从多个维度来看待:
Redis
完全基于内存Redis
整个结构类似于HashMap
,查找和操作复杂度为O(1)
,不需要和MySQL
查找数据一样需要产生随机磁盘IO或者全表Redis
对于客户端的处理是单线程的,采用单线程处理所有客户端请求,避免了多线程的上下文切换和线程竞争造成的开销select/epoll
多路复用的高效非阻塞IO模型RESP
,简单易读,避免了复杂请求的解析开销面试官露出姨父般的慈笑: 嗯嗯,还不错,那你继续谈谈刚刚的缓存雪崩、穿透与击穿的问题吧
我: 好的,先说缓存雪崩吧,缓存雪崩造成的原因是因为我们在做缓存时为了保证内存利用率,一般在写入数据时都会给定一个过期时间,而就是因为过期时间的设置有可能导致大量的热点key在同一时间内全部失效,此时来了大量请求访问这些key,而Redis
中却没有这些数据,从而导致所有请求直接落入DB查询,造成DB出现瓶颈或者直接被打宕导致雪崩情况的发生。关于解决方案的的话也可以从多个维度来考虑:
面试官: 那缓存穿透呢?指的是什么?又该怎么解决?
我喝了口水接着回答: 缓存穿透这个问题是由于请求参数不合理导致的,比如对外暴露了一个接口getUser?userID=xxx
,而数据库中的userID
是从1开始的,当有黑客通过这个接口携带不存在的ID请求时,比如:getUser?userID=-1
,请求会先来到Redis
中查询缓存,但是发现没有对应的数据从而转向DB查询,但是DB中也无此值, 所以也无法写入数据到缓存,而黑客就通过这一点利用“肉鸡”等手段疯狂请求这个接口,导致出现大量Redis
不存在数据的请求落入DB,从而导致DB出现瓶颈或者直接被打宕机,整个系统陷入瘫痪。
面试官: 嗯,那又该如果避免这种情况呢?
我: 解决方案也有好几种呢:
Redis
中“Not Data”
并设置短暂的过期时间,下次请求能够直接被拦截在Redis
而不会落入DB面试官: 那接下来的缓存击穿呢?又是怎么回事?怎么解决?
我: 这个简单,缓存击穿和缓存雪崩有点类似,都是由于请求的key过期导致的问题,但是不同点在于失效key
的数量,对于雪崩而言指的是大量的key
失效导致大量请求落入DB,而对于击穿而言,指的是某一个热点key突然过期,而这个时候又突然又大量的请求来查询它,但是在Redis
中却并没有查询到结果从而导致所有请求全部打向DB,导致在这个时刻DB直接被打穿。解决方案的话也是有多种:
Redis
监控,请求串行化访问(性能较差)mutex
锁机制:就是在缓存失效的时候(判断拿出来的值为空),不是立即去load db
,而是先使用缓存工具的某些带成功操作返回值的操作(比如Redis
的SETNX
或者Memcache
的ADD
)去set
一个mutex key
,当操作返回成功时,再进行load db
的操作并回设缓存;否则,就重试整个get
缓存的方法,代码实现如下:public Result get(int ID){
RedisResult = Redis.get(ID);
if(RedisResult != null){
return RedisResult;
}
if(Redis.setnx("update:" + ID) != "0"){
DBResult = DB.selectByID(ID);
if(DBResult != null){ // 避免缓存穿透
Redis.set(ID,DBResult);
Redis.del("update:" + ID);
return DBResult;
}
Redis.set(ID,"Not Data");
return "抱歉,当前查询暂时没有找到数据......";
}
Thread.sleep(2);
return get(ID);
}
复制代码
Redis
的数据是全部放在内存中的,那么有些数据我也没有设置过期时间,导致了大量的内存浪费,当我有新的数据需要写入内存不够用了怎么办?Redis
淘汰策略这么拐弯抹角.......Redis
在5.0之前为我们提供了六种淘汰策略,而5.0为我们提供了八种,但是大体上来说这些lru、lfu、random、ttl
四种类型,如下:Redis
中,数据有一部分访问频率较高,其余部分访问频率较低,或者无法预测数据的使用频率时,设置allkeys-lru
是比较合适的。allkeys-random
。volatile-ttl
策略。volatile-lru
或volatile-random
都是比较不错的。expire
会消耗额外的内存,如果计划避免Redis
内存在此项上的浪费,可以选用allkeys-lru
策略,这样就可以不再设置过期时间,高效利用内存了。maxmemory-policy
:参数配置淘汰策略。maxmemory
:限制内存大小。Redis
的Key
删除策略有了解过吗?Redis
删除Key
的策略策略有三种:
key
过期后任然留在内存中不做处理,当有请求操作这个key
的时候,会检查这个key
是否过期,如果过期则删除,否则返回key
对应的数据信息。(惰性删除对CPU是友好的,因为只有在读取的时候检测到过期了才会将其删除。但对内存是不友好,如果过期键后续不被访问,那么这些过期键将积累在缓存中,对内存消耗是比较大的。)Redis
数据库默认每隔100ms
就会进行随机抽取一些设置过期时间的key
进行检测,过期则删除。(定期删除是定时删除和惰性删除的一个折中方案。可以根据实际场景自定义这个间隔时间,在CPU资源和内存资源上作出权衡。)Redis
默认采用定期+惰性删除策略。Redis
为了保证性能会将所有数据放在内存,那么机器突然断电或宕机需要重启,内存中的数据岂不是没有了?Redis
的确是将数据存储在内存的,但是也会有相关的持久化机制将内存持久化备份到磁盘,以便于重启时数据能够重新恢复到内存中,避免数据丢失的风险。而Redis
持久化机制由三种,在4.X版本之前Redis
只支持AOF
以及RDB
两种形式持久化,但是因为AOF
与RDB
都存在各自的缺陷,所以在4.x
版本之后Redis
还提供一种新的持久化机制:混合型持久化(但是最终生成的文件还是.AOF
)。RDB
持久化把内存中当前进程的数据生成快照(.rdb
)文件保存到硬盘的过程,有手动触发和自动触发:
Redis
RDB持久化默认开启save 900 1
-- 900s内存在1个写操作save 300 10
-- 300s内存在10个写操作save 60 10000
-- 60s内存在10000个写操作save
:阻塞当前 Redis
,直到RDB
持久化过程完成为止,若内存实例比较大会造成长时间阻塞,线上环境不建议用它 - bgsave
:Redis
进程执行fork
操作创建子进程,由子进程完成持久化,阻塞时 间很短(微秒级),是save
的优化,在执行Redis-cli shutdown
关闭Redis
服务时或执行flushall
命令时,如果没有开启AOF
持久化,自动执行bgsave,bgsave
执行流程如下:
而且RDB 是在某个时间点将数据写入一个临时文件,持久化结束后,用这个临时文件替换上次持久化的文件,重启时加载这个文件达到数据恢复。
Redis
的高性能;而且RDB
文件存储的是压缩的二进制文件,适用于备份、全量复制,可用于灾难备份,同时RDB
文件的加载速度远超于AOF
文件。RDB
是间隔一段时间进行持久化,如果持久化之间的时间内发生故障,会出现数据丢失。所以这种方式更适合数据要求不严谨的时候,因为RDB
无法做到实时持久化,而且每次都要创建子进程,频繁创建成本过高;备份时占用内存,因为Redis
在备份时会独立创建一个子进程,将数据写入到一个临时文件(需要的内存是原本的两倍);还有一点,RDB
文件保存的二进制文件存在新老版本不兼容的问题。AOF
持久化方式能很好的解决RDB
持久化方式造成的数据丢失,AOF
持久化到硬盘中的并不是内存中的数据快照,而是和MySQL
的binlog
日志一样记录写入命令,AOF
的持久化策略也有三种:
appendfsync always
:同步持久化形式,每次发生数据更改都将命令追加到AOF
文件,因为每次写入时都记录会产生大量磁盘IO,从而性能会受到影响,但是数据最安全。appendfsync everysec
:Redis
开启AOF
后的缺省配置,异步操作,每秒将写入命令追加到AOF
文件,如果在刚持久化之后的一秒内宕机,会造成1S的数据丢失。appendfsync no
:Redis
并不直接调用文件同步,而是交给操作系统来处理,操作系统可以根据buffer
填充情况/通道空闲时间等择机触发同步;这是一种普通的文件操作方式。性能较好,在物理服务器故障时,数据丢失量会因OS
配置有关。AOF
持久化机制优缺点:
fsync
策略可以保证数据丢失风险降到最低,数据能够保证是最新的,fsync
是后台线程在处理,所以对于处理客户端请求的线程并不影响。RDB
大上很多,而且数据恢复时也需要重新执行指令,在重启时恢复数据的时间往往会慢很多。虽然fsync
并不是共用处理客户端请求线程的资源来处理的,但是这两个线程还是在共享同一台机器的资源,所以在高并发场景下也会一定受到影响。Redis
在线上运行的时间越来越久,客户端执行的命令越来越多,AOF
的文件也会越来越大,当AOF
达到一定程度大小之后再通过AOF
文件恢复数据是异常缓慢的,那么对于这种情况Redis
在开启AOF
持久化机制的时候会存在AOF
文件的重写,缺省配置是当AOF
文件比上一次重写时的文件大小增长100%
并且文件大小不小于64MB
时会对整个AOF
文件进行重写从而达到“减肥”的目的(这里的100%
和64MB
可以通过auto-aof-rewrite-percentage 100
与 auto-aof-rewrite-min-size 64mb
来调整)。而AOF rewrite
操作就是“压缩”AOF
文件的过程,当然 Redis
并没有采用“基于原aof
文件”来重写的方式,而是采取了类似snapshot
的方式:基于copy-on-write
,全量遍历内存中数据,然后逐个序列到aof
文件中。因此AOF rewrite
能够正确反应当前内存数据的状态,这正是我们所需要的;*rewrite
过程中,对于新的变更操作将仍然被写入到原 AOF
文件中,同时这些新的变更操作也会被 Redis
收集起来(buffer,copy-on-write
方式下,最极端的可能是所有的key
都在此期间被修改,将会耗费2
倍内存),当内存数据被全部写入到新的aof
文件之后,收集的新的变更操作也将会一并追加到新的aof
文件中,此后将会重命名新的aof
文件为appendonly.aof
, 此后所有的操作都将被写入新的aof
文件。如果在rewrite
过程中,出现故障,将不会影响原AOF
文件的正常工作,只有当rewrite
完成之后才会切换文件,因为rewrite
过程是比较可靠的,触发rewrite
的时机可以通过配置文件来声明,同时Redis
中可以通过bgrewriteaof
指令人工干预。
Redis
采用的是那种持久化方式呢?Redis
中不仅仅只是用来做缓存,其中还存储着一些MySQL
中不存在的数据,所以数据的安全性要求比较高,而RDB
因为并不是实时的持久化,会出现数据丢失,但是采用AOF
形式在重启、灾备、迁移的时候过程异常耗时,也并不理想,所以在我们线上是同时采用两种形式的,而AOF+RDB
两种模式同时开启时Redis
重启又该加载谁呢?(说着说着我又掏出了纸笔给面试官画了如下一幅图):当然在Redis4.x
之后推出了混合型持久化机制,因为RDB
虽然加载快但是存在数据丢失,AOF
数据安全但是加载缓慢,Redis
为了解决这个问题,带来了一个新的持久化选项——混合持久化。将RDB
文件的内容和增量的AOF
日志文件存在一起。这里的AOF
日志不再是全量 的日志,而是自持久化开始到持久化结束的这段时间发生的增量AOF
日志,通常这部分AOF
日志很小。Redis
重启的时候,可以先加载RDB
的内容,然后再重放增量AOF
日志,就可以完全替代之前的AOF
全量文件重放,恢复效率因此大幅得到提升(混合型持久化最终生成的文件后缀是.aof
,可以通过redis.conf
文件中aof-use-rdb-preamble yes
配置开启)。 - 混合型持久化优点:结合了RDB
和AOF
的优点,使得数据恢复的效率大幅提升 - 混合型持久化缺点:兼容性不好,Redis-4.x
新增,虽然最终的文件也是.aof
格式的文件,但在4.0
之前版本都不识别该aof
文件,同时由于前部分是RDB
格式,阅读性较差
Redis
的事务机制Redis
是数据库,那么它支不支持事务呢?Redis
作为数据库当然是支持事务的,只不过Redis
的事务机制是弱事务,相对来说比较鸡肋,官方给出如下几个指令来进行Redis
的事务控制:
MULTI
:标记一个事务块的开始DISCARD
:取消事务,放弃执行事务块内的所有命令EXEC
:执行所有事务块内的命令UNWATCH
:取消WATCH
命令对所有key
的监视WATCH key [key ...]
:监视一个(或多个)key
,如果在事务执行之前这个(或这些)key
被其他命令所改动,那么事务将被打断Redis
内存模型及内存划分Redis
的内存模型以及内存的划分有去了解过嘛?Redis
的内存模型我们可以通过客户端连接之后使用内存统计命令info memory
去查看,如下:
Redis
分配器分配的内存总量,包括使用的虚拟内存(稍后会详解)Redis
进程占据操作系统的内存;除了分配器分配的内存之外,used_memory_rss
还包括进程运行本身需要的内存、内存碎片等,但是不包括虚拟内存Redis
角度得到的量,used_memory_rss
是从操作系统角度得到的量。二者之所以有所不同,一方面是因为内存碎片和Redis
进程运行需要占用内存,使得used_memory_rss
可能更大;另一方面虚拟内存的存在,使得used_memory
可能更大Redis
使用了虚拟内存,由于虚拟内存的媒介是磁盘,比内存速度要慢很多,当这种情况出现时,应该及时排查,如果内存不足应该及时处理,如增加Redis
节点、增加Redis
服务器的内存、优化应用等;一般来说,mem_fragmentation_ratio
在1.03
左右是比较健康的状态(对于jemalloc
分配器来说),由于在实际应用中,Redis
的数据量会比较大,此时进程运行占用的内存与Redis
数据量和内存碎片相比,都会小得多,mem_fragmentation_ratio
便成了衡量Redis
内存碎片率的参数Redis
使用的内存分配器,在编译时指定;可以是libc 、jemalloc或tcmalloc
,默认是jemalloc
Redis
作为内存数据库,在内存中存储的内容主要是数据,但除了数据以外,Redis
的其他部分也会占用内存。Redis
的内存占用可以划分为以下几个部分:
used_memory
中Redis
主进程本身运行肯定需要占用内存,如代码、常量池等等,这部分内存大约几兆,在大多数生产环境中与Redis
数据占用的内存相比可以忽略。这部分内存不是由jemalloc
分配,因此不会统计在used_memory
中。除了主进程外,Redis
创建的子进程运行也会占用内存,如Redis
执行AOF、RDB
重写时创建的子进程。当然,这部分内存不属于Redis
进程,也不会统计在used_memory
和used_memory_rss
中。jemalloc
分配,因此会统计在used_memory
中。Redis
在分配、回收物理内存过程中产生的。例如,如果对数据的更改频繁,而且数据之间的大小相差很大,可能导致Redis
释放的空间在物理内存中并没有释放,但Redis
又无法有效利用,这就形成了内存碎片。内存碎片不会统 计在used_memory
中。
Redis
服务器中的内存碎片已经很大,可以通过安全重启的方式减小内存碎片:因为重启之后,Redis
重新从备份文件中读取数据,在内存中进行重排,为每个数据重新选择合适的内存单元,减小内存碎片。Redis
的共享对象你有了解过吗?RedisObject
对象中有一个refcount
,refcount
记录的是该对象被引用的次数,类型为整型。refcount
的作用,主要在于对象的引用计数和内存回收。当创建新对象时,refcount
初始化为1;当有新程序使用该对象时,refcount加1;当对象不再被一个新程序使用时,refcount
减1;当refcount
变为0时,对象占用的内存会被释放。Redis
中被多次使用的对象(refcount>1
),称为共享对象。Redis
为了节省内存,当有一些对象重复出现时,新的程序不会创建新的对象,而是仍然使用原来的对象。这个被重复使用的对象,就是共享对象。目前共享对象仅支持整数值的字符串对象。 - 共享对象的具体实现:Redis
的共享对象目前只支持整数值的字符串对象。之所以如此,实际上是对内存和CPU(时间)的平衡:共享对象虽然会降低内存消耗,但是判断两个对象是否相等却需要消耗额外的时间。对于整数值,判断操作复杂度为O(1)
;对于普通字符串,判断复杂度为O(n)
;而对于哈希、列表、集合和有序集合,判断的复杂度为O(n^2)
。 虽然共享对象只能是整数值的字符串对象,但是5种类型都可能使用共享对象(如哈希、列表等的元素可以使用)。
就目前的实现来说,Redis
服务器在初始化时,会创建10000
个字符串对象,值分别是0-9999
的整数值;当Redis
需要使用值为0-9999
的字符串对象时,可以直接使用这些共享对象。10000
这个数字可以通过调整参数Redis_SHARED_INTEGERS
(4.0中是OBJ_SHARED_INTEGERS
)的值进行改变。
共享对象的引用次数可以通过object refcount
命令查看。
Redis
的虚拟内存,那你能详细讲讲它是怎么会事吗?Redis
的虚拟内存与操作系统虚拟内存不是一码事,但是思路和目的都是相冋的。就是暂时把不经常访问的数据从內存交换到磁盘中,从而腾出宝贵的内存空间。对于Redis
这样的内存数据库,内存总是不够用的。除了可以将数据分割到多个Redis
实例以外。另外的能够提高数据库容量的办法就是使用虚拟内存技术把那些不经常访问的数据交换到磁盘上。如果我们存储的数据总是有少部分数据被经常访问,大部分数据很少被访问,对于网站来说确实总是只有少量用户经常活跃。当少量数据被经常访问时,使用虚拟内存不但能提高单台 Redis
数据库服务器的容量,而且也不会对性能造成太多影响Redis
没有使用操作系统提供的虚拟内存机制而是自己在用户态实现了自己的虚拟内存机制。主要的理由有以下两点:
4k
/页为最小单位进行交换的。而Redis
的大多数对象都远小于4k
,所以一个操作系统页上可能有多个Redis
对象。另外 Redis
的集合对象类型如list,set
可能行在于多个操作系统页上。最终可能造成只有10%
的key被经常访问,但是所有操作系统页都会被操作系统认为是活跃的,这样只有内存真正耗尽时操作系统才会进行页的交换Redis
可以将被交换到磁盘的对象进行压缩,保存到磁盘的对象可以去除指针和对象元数据信息。一般压缩后的对象会比内存中的对象小10
倍。这样Redis
的虛拟内存会比操作系统的虚拟内存少做很多I0操作Redis
虚拟内存的配置也存在于redis.conf
文件中,如下:
vm-enabled ves
:#开启虚拟内存功能vm-swap-file ../redis.swap
:#交换出来value
保存的文件路径Vm-max-memory 268435456
:# Redis
使用的最大内存上限(256MB
),超过上限后Redis
开始交换value
到磁盘swap
文件中。建议设置为系统空闲内存的60%-80%
vm-page-size 32
:#每个 Redis
页的大小32
个字节vm-pages 134217728
:#最多在文件中使用多少个页,交换文件的大小vm-max-threads 8
:#用于执行value
对象换入换出的工作线程数量,0表示不使用工作线程(详情后面介绍)。Redis
的虚拟内存在设计上为了保证key
的查询速度,只会将value
交换到swap
文件。如果是由于太多key
很小的value
造成的内存问题,那么Redis
的虚拟内存并不能解决问题。和操作系统一样 Redis
也是按页来交换对象的。Redis
规定同一个页只能保存一个对象。但是一个对象可以保存在多个页中。在Redis
使用的内存没超过vm-max-memory
之前是不会交换任何value
的。当超过最大内存限制后,Redis
会选择把较老的对象交换到swap
文件中去。如果两个对象一样老会优先交换比较大的对象,精确的交换计算公式swappability=age*1og(size_Inmemory)
。对于vm-page-size
的设置应该根据自己应用将页的大小设置为可以容纳大多数对象的尺寸。太大了会浪费磁盘空间,太小了会造成交换文件出现过多碎片。对于交换文件中的每个页, Redis
会在内存中用一个1bit
值来对应记录页的空闲状态。所以像上面配置中页数量(vm pages134217728
)会占用16MB
内存用来记录页的空內状态。vm-max-threads
表示用做交换任务的工作线程数量。如果大于0推荐设为服务器的cpu的核心数。如果是0则交换过程在上线程进行。具体工作模式如下:
vm-max-threads=0
):
swap
文件中,并释放对象占用的内存空间,此过程会一直重复直到下面条件满足。
swap
文件满了value
时,主线程会以阳塞的方式从swap
文件中加载对应的value
对象,加载时此时会阻塞所客户端。然后处理该客户端的请求vm-max-threads>0
):
key
已终被换出了,主线程会先阳塞发出命令的客户端,然后将加载对象的信息放到一个队列中,让工作线程去加载。加载完毕后工作线程通知主线程。主线程再执行客户端的命令。这种方式只阻塞请求的value是已经被 换出key的客户端总的来说阻塞方式的性能会好些,因为不需要线程同步、创建线程和恢复被阻塞的客户端等开销。但是也相应的牺牡了响应性。工作线稈方式主线程不会阳塞在磁盘1O上,所以响应性更好。如果我们的应用不太经常发生换入换出,而且也不太在意有点延迟的话推荐使用阻塞方式(详细介绍参考)。Redis
客户端通信RESP协议Redis
的客户端通信的RESP协议吧RESP
是Redis
序列化协议,Redis
客户端RESP
协议与Redis
服务器通信。RESP
协议在Redis 1.2
中引入,但在Redis 2.0
中成为与Redis
服务器通信的标准方式。这个通信方式就是Redis
客户端实现的协议。RESP实际上是一个序列化协议,它支持以下数据类型:简单字符串、错误、整数、大容量字符串和数组。当我们在客户端中像Redis
发送操作命令时,比如:set name 竹子爱熊猫
这条命令,不会直接以这种格式的形式发送到Redis Server
,而是经过RESP
的序列化之后再发送给Redis
执行,而AOF持久化机制持久化之后生成的AOF文件中也并不是存储set name 竹子爱熊猫
这个指令,而是存储RESP
序列化之后的指令,RESP
的特点如下:
Redis
作为缓存层,那么会在Java程序与DB之间多出一层访问,假设Redis
挂了那么Java程序这边又会抛出异常导致所有请求死在这里从而导致整个系统的不可用,那么怎么避免Redis
出现这类的单点故障呢?Redis
既然这么受欢迎那么这些问题它都提供了相关的解决方案的,Redis
有提供了主从、哨兵、代理集群与分片集群的高可用机制来保证出现单点问题时能够及时的切换机器以保障整个系统不受到影响。但是后续的三种高可用机制都是基于主从的基础上来实现的,所以我先说说Redis
的主从复制。虽然我们之前讲到过持久化机制可以保证数据重启情况下也不丢失,但是由于是存在于一台服务器上的,如果机器磁盘坏了、机房爆炸(玩笑~)等也会导致数据丢失,而主从复制可以将数据同步到多台不同机器,也能够保证在主节点宕机时任然对外提供服务,还可以做到通过读写分离的形式提升整体缓存业务群吞吐量。一般在线上环境时我们去搭建主从环境时,为了保证数据一致性,从节点是不允许写的,而是通过复制主节点数据的形式保障数据同步。所以在整个Redis
节点群中只能同时运行存在一台主,其他的全为从节点,示意图如下(读的QPS可以通过对从节点的线性扩容来提升): Redis2.8
之前使用sync[runId][offset]
同步命令,Redis2.8
之后使用psync[runId][offset]
命令。两者不同在于,sync
命令仅支持全量复制过程,psync
支持全量和部分复制。介绍同步之前,先介绍几个概念:
runId
:每个Redis
节点启动都会生成唯一的uuid
,每次Redis
重启后,runId
都会发生变化offset
:主节点和从节点都各自维护自己的主从复制偏移量offset
,当主节点有写入命令时,offset=offset+命令的字节长度
。从节点在收到主节点发送的命令后,也会增加自己的offset
,并把自己的offset
发送给主节点。这样,主节点同时保存自己的offset
和从节点的offset
,通过对比offset
来判断主从节点数据是否一致repl_back_buffer
:复制缓冲区,用来存储增量数据命令psync
命令除了支持全量复制之外还支持部分复制,因为在做主从数据同步时会导致主从机器网络带宽开销非常大,而在2.8之前Redis
仅支持全量复制,这样非常容易导致Redis
在线上出现网络瓶颈,而在2.8之后的增量(部分)复制,用于处理在主从复制中因网络闪断等原因造成的数据丢失场景,当slave
再次连上master
后,如果条件允许,master
会补发丢失数据给slave
。因为补发的数据远远小于全量数据,可以有效避免全量复制的过高开销。部分复制流程图如下(复制缓存区溢出也会导致全量复制):psync[runid][offset]
命令三种返回值:
FULLRESYNC
:第一次连接,进行全量复制CONTINUE
:进行部分复制ERR
:不支持psync
命令,进行全量复制Redis
整体吞吐,并且读的性能可以通过对从节点进行线性扩容无限提升Redis
节点群能够存储的数据容量受到所有节点中内存最小的那台限制,比如一主两从架构:master=32GB、slave1=32GB、slave2=16GB
,那么整个Redis
节点群能够存储的最大容量为16GB
上图所示是目前企业中常用的Redis
架构,一主两从三哨兵架构,Redis Sentinel
(哨兵)主要功能包括主节点存活检测、主从运行情况检测、自动故障转移、主从切换。Redis Sentinel
最小配置是一主一从。Redis
的Sentinel
系统可以用来管理多个Redis
节点,该系统可以执行以下四个任务: - 监控:不断检查主服务器和从服务器是否正常运行 - 通知:当被监控的某个Redis
服务器出现问题,Sentinel
通过API脚本向管理员或者其他应用程序发出通知 - 自动故障转移:当主节点不能正常工作时,Sentinel
会开始一次自动的故障转移操作,它会将与失效主节点是主从关系的其中一个从节点升级为新的主节点,并且将其他的从节点指向新的主节点,这样就不需要人工干预进行主从切换 - 配置提供者:在Sentinel
模式下,客户端应用在初始化时连接的是Sentinel
节点集合,从中获取主节点的信息
info
命令获取最级联结构图,哨兵配置时只要配置对主节点的监控即可,通过向主节点发送info
,获取从节点的信息,并当有新的从节点加入时可以马上感知到Redis
数据节点的指定频道上发送该哨兵节点对于主节点的判断以及当前哨兵节点的信息,同时每个哨兵节点也会订阅该频道,来了解其它哨兵节点的信息及对主节点的判断,其实就是通过消息publish
和subscribe
来完成的Sentinel
会以每秒一次的频率向所有与其建立了命令连接的实例(master、salve
、其他Sentinel
)发ping
命令,通过判断ping
回复是有效回复还是无效回复来判断实例是否在线/存活(对该Sentinel
来说是“主观在线”),Sentinel
配置文件中的down-after-milliseconds
设置了判断主观下线的时间长度,如果实例在down-after-milliseconds
毫秒内,返回的都是无效回复,那么Sentinel
会认为该实例已(主观)下线,修改其flags
状态为SRI_S_DOWN
。如果多个Sentinel
监视一个服务,有可能存在多个Sentinel
的down-after-milliseconds
配置不同,这个在实际生产中要注意(主观下线:所谓主观下线,就是单个Sentinel
认为某个实例下线(有可能是接收不到订阅,之间的网络不通等等原因))sentinel is-masterdown-by-addr
寻求其它哨兵节点对主节点的判断,如果其他的哨兵也认为主节点主观下线了,则当认为主观下线的票数超过了quorum
(选举)个数,此时哨兵节点则认为该主节点确实有问题,这样就客观下线了,大部分哨兵节点都同意下线操作,也就说是客观下线,一般情况下,每个Sentinel
会以每10秒一次的频率向它已知的所有主服务器和从服务器发送INFO
命令,当一个主服务器被标记为客观下线时,Sentinel
向下线主服务器的所有从服务器发送INFO
命令的频率,会从10秒一次改为每秒一次Sentinel
和其他Sentinel
协商客观下线的主节点的状态,如果处于SDOWN
状态,则自动选出新的主节点,将剩余从节点指向新的主节点进行数据复制Sentinel
状态数据结构中保存了主服务的所有从服务信息,领头Sentinel
按照如下的规则从从服务列表中挑选出新的主服务:
slave-priority
最高的节点,如果有则返回没有就继续选择run_id
最小的节点slaveof no one
命令,让选出来的从节点成为主节点;并通过slaveof
命令让其他节点成为其从节点Sentinel
会向其发送slaveof
命令,让其成为新主的从lerder
选举流程:如果主节点被判定为客观下线之后,就要选取一个哨兵节点来完成后面的故障转移工作,选举出一个leader
的流程如下:
is-master-down-by-addr
命令,征求判断并要求将自己设置为领导者,由领导者处理故障转移num(sentinels)/2+1
时,将成为领导者,如果没有超过,继续重复选举…………Redis3.0
之前官方却并没有相对应的解决方案,不过在Redis3.0
之前却有很多其他的解决方案的提出以及落地,比如:
TwemProxy
:TwemProxy
是一种代理分片机制,由Twitter
开源。Twemproxy
作为代理, 可接受来自多个程序的访问,按照路由规则,转发给后台的各个Redis
服务器,再原路返回。这个方案顺理成章地解决了单个Redis
实例承载能力的问题。当然,Twemproxy
本身也是单点,需要用Keepalived
做高可用方案。这么些年来,Twemproxy
是应用范围最广、稳定性最高、 最久经考验的分布式中间件。只是,他还有诸多不方便之处。Twemproxy
最大的痛点在于,无法平滑地扩容/缩容。这样增加了运维难度:业务量突增,需增加Redis
服务器; 业务量菱缩,需要减少Redis
服务器。但对Twemproxy
而言,基本上都很难操作。或者说,Twemproxy
更加像服务器端静态sharding
,有时为了规避业务量突增导致的扩容需求,甚至被迫新开一个基于Twemproxy
的Redis
集群。Twemproxy
另一个痛点是,运维不友好,甚至没有控制面板。当然,由于使用了中间件代理,相比客户端直接连服务器方式,性能上有所损耗,实测结果降低20%左右。Codis
:Codis
由豌豆英于2014年11月开源,基于Go、C
开发,是近期涌现的、国人开发的优秀开源软件之一。现已广泛用于豌豆英的各种Redis
业务场景,从各种压力测试来看,稳定性符合高效运维的要求。性能更是改善很多,最初比Twemproxy
慢20%;现在比Twemproxy
快近100% (条件:多实例,-般Value
长度)。Codis
具有可视化运维管理界面。Codis
无疑是 为解决Twemproxy
缺点而出的新解决方案。因此综合方面会由于Twemproxy
很多。目前也越来越多公司选择Codis
,Codis
引入了Group
的概念,每个Group
包括1个Master
及至少1个Slave
,这是和Twemproxy
的区别之一。这样做的好处是,如果当前Master
有问题,则运维人员可通过Dashboard
“自助式”切换到Slave
,而不需要小心翼翼地修改程序配置文件。为支持数据热迁移(AutoRebalance
),出品方修改了RedisServer
源码,并称之为Codis Server
,Codis
采用预先分片(Pre-Sharding
)机制,事先规定好了,分成1024个slots
(也就是说,最多能支持后端1024个CodisServer
),这些路由信息保存在ZooKeeper
中。 不足之处有对Redis
源码进行了修改,以及代理实现本身会有的问题。Redis
,然后对Redis
进行读写操作,现在则是连接代理,读写操作全部交由代理来处理分发到具体的Redis
实例,而集群的组成就很好的打破了之前的一主多从架构,形成了多主多从的模式,每个节点由一个个主从来构建,每个节点存储不同的数据,每个节点都能够提供读写服务,从而做到真正意义上的高可用,具体结构如下:master
负责的一部分数据Redis
的很多命令不再完美支持,如set的交集、并集、差集等Redis3.x
之后的Redis-cluster
去中心化分片式集群,Redis-cluster
在Redis3.0
中推出,支持Redis
分布式集群部署模式。采用无中心分布式架构。所有的Redis
节点彼此互联(PING-PONG
机制),内部使用二进制协议优化传输速度和带宽节点的fail
是通过集群中超过半数的节点检测失效时才生效.客户端与Redis
节点直连,不需要中间proxy
层.客户端不需要连接集群所有节点连接集群中任何一个可用节点即可,减少了代理层,大大提高了性能。Redis-cluster
把所有的物理节点映射到[0-16383]slot
上,cluster负责维护node <-> slot <-> key
之间的关系。目前Jedis
已经支持Redis-cluster
。从计算架构或者性能方面无疑Redis-cluster
是最佳的选择方案。Redis-cluster
集群的原理吗?Redis Cluster
在设计中没有使用一致性哈希(ConsistencyHashing
),而是使用数据分片(Sharding
)引入哈希槽(hashSlot
)来实现;一个RedisCluster
包含16384(0~16383)
个哈希槽,存储在RedisCluster
中的所有键都会被映射到这些slot
中,集群中的每个键都属于这16384
个哈希槽中的一个,集群使用公式slot=CRC16(key)% 16384
来计算key
属于哪个槽,其中CRC16(key)
语句用于计算key
的CRC16
校验和。集群中的每个主节点(Master)都负责处理16384个哈希槽中的一部分,当集群处于稳定状态时,每个哈希槽都只由一个主节点进行处理,每个主节点可以有一个到N个从节点(Slave),当主节点出现宕机或网络断线等不可用时,从节点能自动提升为主节点进行处理。
假设我此时向Redis
发送一条命令:set name 竹子爱熊猫
,那么Redis
会使用CRC16
算法计算KEY值,CRC16(name)
,类似于一个HASH函数,完成后会得到一个数字,假设此时计算完name
后得到的结果是26384
,那么会拿着这个计算完成之后的结果%总槽数,26384%16384
得到结果为10000
,那么key=name
的这个值应该被放入负责10000
这个HashSlot
存储,如上图中,会被放入到第三个节点存储,当再次get
这个缓存时同理(Redis
底层的GossIP
原理由于本篇篇幅过长则不再阐述)。
Redis
版本新特性面试官: 既然你在前面提到过这么多版本之间都有不同的变化,那么我最后考考你Redis
不同的版本之间有什么区别吧
我: (心想:这不就是考我新特性吗,Redis
问这么久我都扛不住了,嗓子都冒烟了)好的好的,具体如下:
Redis3.x
:
Redis4.x
:
pync1
,4.x之后支持psync2
DEL/FLUSH
优化,新的UNLINK
与DEL
作用相同,FLUSHALL/FLUSHDB
中添加了ASYNC
选项,Redis
现在可以在不同的线程中删除后台的key而不会阻塞服务器RDB + AOF
格式MEMORY
:能够执行不同类型的内存分析:内存问题的故障排除(使用MEMORY DOCTOR
,类似于LATENCYDOCTOR
),报告单个键使用的内存量,更深入地报告Redis
内存使用情况SWAPDB
:能够完全立即(无延迟)替换同实例下的两个Redis
数据库(目前我们业务没啥用)Redis
现在使用更少的内存来存储相同数量的数据Redis
现在可以对使用的内存进行碎片整理,并逐渐回收空间Redis5.x
:
Stream data type
)Redis
模块API:定时器、集群和字典APIRDB
可存储LFU
和LRU
信息Redis-cli
中的集群管理器从Ruby (redis-trib.rb)
移植到了C
语言代码。执行redis-cli --cluster help
命令以了解更多信息sorted set
)命令:ZPOPMIN/MAX
和阻塞变体(blocking variants
)Active defragmentation
至v2
版本HyperLogLog
的实现HELP
子命令Jemalloc
至5.1
版本CLIENT UNBLOCK
和CLIENT ID
LOLWUT
命令Lua
相关的改进:
Lua
脚本更好地传播到replicas / AOF
Lua
脚本现在可以超时并在副本中进入-BUSY
状态HZ(Dynamic HZ)
以平衡空闲CPU
使用率和响应性Redis
核心代码进行了重构并在许多方面进行了改进,许多错误修复和其他方面的改进Redis6.x
:
ACL
:在Redis 5
版本之前,Redis
安全规则只有密码控制还有通过rename
来调整高危命令比如flushdb/KEYS*/shutdown
等。Redis6
则提供ACL
的功能对用户进行更细粒度的权限控制:
KEY
Redis
通信协议:RESP3
Client side caching
客户端缓存:基于RESP3
协议实现的客户端缓存功能。为了进一步提升缓存的性能,将客户端经常访问的数据cache
到客户端。减少TCP
网络交互,提升RTCluster
集群:Redis6.0
版本后redis/src
目录下提供的大部分工具开始支持Cluster
集群Modules API
:Redis 6
中模块API开发进展非常大,因为Redis Labs
为了开发复杂的功能,从一开始就用上Redis
模块。Redis
可以变成一个框架,利用Modules
来构建不同系统,而不需要从头开始写然后还要BSD
许可。Redis
一开始就是一个向编写各种系统开放的平台Disque
:Disque
作为一个RedisModule
使用足以展示Redis
的模块系统的强大。集群消息总线API、屏蔽和回复客户端、计时器、模块数据的AOF和RDB等等面试官: 嗯嗯,小伙子你前途无量呀,今天晚上方便入职吗?
我: ..........
伙伴们有兴趣想了解内容和更多相关学习资料的请点赞收藏+评论转发+关注我,后面会有很多干货。我有一些面试题、架构、设计类资料可以说是程序员面试必备!所有资料都整理到网盘了,需要的话欢迎下载!私信我回复【999】即可免费获取
作者:竹子爱熊猫
链接:https://juejin.cn/post/7097521572885299214
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。