一、前言
近乎所有与Java相关的面试都会问到缓存的问题,基础一点的会问到什么是“二八定律”、什么是“热数据和冷数据”,复杂一点的会问到缓存雪崩、缓存穿透、缓存预热、缓存更新、缓存降级等问题,这些看似不常见的概念,都与我们的缓存服务器相关,一般常用的缓存服务器有Redis、Memcached等,而笔者目前最常用的也只有Redis这一种。
如果你在以前面试的时候还没有遇到过面试官问你《为什么说Redis是单线程的以及Redis为什么这么快!》,那么你看到这篇文章的时候,你应该觉得是一件很幸运的事情!如果你刚好是一位高逼格的面试官,你也可以拿这道题去面试对面“望穿秋水”般的小伙伴,测试一下他的掌握程度。
好啦!步入正题!我们先探讨一下Redis是什么,Redis为什么这么快、然后在探讨一下为什么Redis是单线程的?
二、Redis简介
Redis是一个开源的内存中的数据结构存储系统,它可以用作:数据库、缓存和消息中间件。
它支持多种类型的数据结构,如字符串(Strings),散列(Hash),列表(List),集合(Set),有序集合(Sorted Set或者是ZSet)与范围查询,Bitmaps,Hyperloglogs 和地理空间(Geospatial)索引半径查询。其中常见的数据结构类型有:String、List、Set、Hash、ZSet这5种。
Redis 内置了复制(Replication),LUA脚本(Lua scripting), LRU驱动事件(LRU eviction),事务(Transactions) 和不同级别的磁盘持久化(Persistence),并通过 Redis哨兵(Sentinel)和自动分区(Cluster)提供高可用性(High Availability)。
Redis也提供了持久化的选项,这些选项可以让用户将自己的数据保存到磁盘上面进行存储。根据实际情况,可以每隔一定时间将数据集导出到磁盘(快照),或者追加到命令日志中(AOF只追加文件),他会在执行写命令时,将被执行的写命令复制到硬盘里面。您也可以关闭持久化功能,将Redis作为一个高效的网络的缓存数据功能使用。
Redis不使用表,他的数据库不会预定义或者强制去要求用户对Redis存储的不同数据进行关联。
数据库的工作模式按存储方式可分为:硬盘数据库和内存数据库。Redis 将数据储存在内存里面,读写数据的时候都不会受到硬盘 I/O 速度的限制,所以速度极快。
(1)硬盘数据库的工作模式:
2)内存数据库的工作模式:
看完上述的描述,对于一些常见的Redis相关的面试题,是否有所认识了,例如:什么是Redis、Redis常见的数据结构类型有哪些、Redis是如何进行持久化的等。
三、Redis到底有多快
Redis采用的是基于内存的采用的是单进程单线程模型的 KV 数据库,由C语言编写,官方提供的数据是可以达到100000+的QPS(每秒内查询次数)。这个数据不比采用单进程多线程的同样基于内存的 KV 数据库 Memcached 差!有兴趣的可以参考官方的基准程序测试《How fast is Redis?》(https://redis.io/topics/benchmarks)
横轴是连接数,纵轴是QPS。此时,这张图反映了一个数量级,希望大家在面试的时候可以正确的描述出来,不要问你的时候,你回答的数量级相差甚远!
四、Redis为什么这么快
1、完全基于内存,绝大部分请求是纯粹的内存操作,非常快速。数据存在内存中,类似于HashMap,HashMap的优势就是查找和操作的时间复杂度都是O(1);
2、数据结构简单,对数据操作也简单,Redis中的数据结构是专门进行设计的;
3、采用单线程,避免了不必要的上下文切换和竞争条件,也不存在多进程或者多线程导致的切换而消耗 CPU,不用去考虑各种锁的问题,不存在加锁释放锁操作,没有因为可能出现死锁而导致的性能消耗;
4、使用多路I/O复用模型,非阻塞IO;
5、使用底层模型不同,它们之间底层实现方式以及与客户端之间通信的应用协议不一样,Redis直接自己构建了VM 机制 ,因为一般的系统调用系统函数的话,会浪费一定的时间去移动和请求;
以上几点都比较好理解,下边我们针对多路 I/O 复用模型进行简单的探讨:
(1)多路 I/O 复用模型
多路I/O复用模型是利用 select、poll、epoll 可以同时监察多个流的 I/O 事件的能力,在空闲的时候,会把当前线程阻塞掉,当有一个或多个流有 I/O 事件时,就从阻塞态中唤醒,于是程序就会轮询一遍所有的流(epoll 是只轮询那些真正发出了事件的流),并且只依次顺序的处理就绪的流,这种做法就避免了大量的无用操作。
这里“多路”指的是多个网络连接,“复用”指的是复用同一个线程。采用多路 I/O 复用技术可以让单个线程高效的处理多个连接请求(尽量减少网络 IO 的时间消耗),且 Redis 在内存中操作数据的速度非常快,也就是说内存内的操作不会成为影响Redis性能的瓶颈,主要由以上几点造就了 Redis 具有很高的吞吐量。
五、那么为什么Redis是单线程的
我们首先要明白,上边的种种分析,都是为了营造一个Redis很快的氛围!官方FAQ表示,因为Redis是基于内存的操作,CPU不是Redis的瓶颈,Redis的瓶颈最有可能是机器内存的大小或者网络带宽。既然单线程容易实现,而且CPU不会成为瓶颈,那就顺理成章地采用单线程的方案了(毕竟采用多线程会有很多麻烦!)。
可以参考:https://redis.io/topics/faq
看到这里,你可能会气哭!本以为会有什么重大的技术要点才使得Redis使用单线程就可以这么快,没想到就是一句官方看似糊弄我们的回答!但是,我们已经可以很清楚的解释了为什么Redis这么快,并且正是由于在单线程模式的情况下已经很快了,就没有必要在使用多线程了!
但是,我们使用单线程的方式是无法发挥多核CPU 性能,不过我们可以通过在单机开多个Redis 实例来完善!
警告1:这里我们一直在强调的单线程,只是在处理我们的网络请求的时候只有一个线程来处理,一个正式的Redis Server运行的时候肯定是不止一个线程的,这里需要大家明确的注意一下!例如Redis进行持久化的时候会以子进程或者子线程的方式执行(具体是子线程还是子进程待读者深入研究);例如我在测试服务器上查看Redis进程,然后找到该进程下的线程:
ps命令的“-T”参数表示显示线程(Show threads, possibly with SPID column.)“SID”栏表示线程ID,而“CMD”栏则显示了线程名称。
警告2:在上图中FAQ中的最后一段,表述了从Redis 4.0版本开始会支持多线程的方式,但是,只是在某一些操作上进行多线程的操作!所以该篇文章在以后的版本中是否还是单线程的方式需要读者考证!
六、注意点
1、我们知道Redis是用”单线程-多路复用IO模型”来实现高性能的内存数据服务的,这种机制避免了使用锁,但是同时这种机制在进行sunion之类的比较耗时的命令时会使redis的并发下降。因为是单一线程,所以同一时刻只有一个操作在进行,所以,耗时的命令会导致并发的下降,不只是读并发,写并发也会下降。而单一线程也只能用到一个CPU核心,所以可以在同一个多核的服务器中,可以启动多个实例,组成master-master或者master-slave的形式,耗时的读命令可以完全在slave进行。
需要改的redis.conf项:
pidfile /var/run/redis/redis_6377.pid #pidfile要加上端口号port 6377 #这个是必须改的logfile /var/log/redis/redis_6377.log #logfile的名称也加上端口号dbfilename dump_6377.rdb #rdbfile也加上端口号
1
2
3
4
2、“我们不能任由操作系统负载均衡,因为我们自己更了解自己的程序,所以,我们可以手动地为其分配CPU核,而不会过多地占用CPU,或是让我们关键进程和一堆别的进程挤在一起。”。 CPU 是一个重要的影响因素,由于是单线程模型,Redis 更喜欢大缓存快速 CPU, 而不是多核
在多核 CPU 服务器上面,Redis 的性能还依赖NUMA 配置和处理器绑定位置。最明显的影响是 redis-benchmark 会随机使用CPU内核。为了获得精准的结果,需要使用固定处理器工具(在 Linux 上可以使用 taskset)。最有效的办法是将客户端和服务端分离到两个不同的 CPU 来高校使用三级缓存。
七、扩展
以下也是你应该知道的几种模型,祝你的面试一臂之力!
1、单进程多线程模型:MySQL、Memcached、Oracle(Windows版本);
2、多进程模型:Oracle(Linux版本);
3、Nginx有两类进程,一类称为Master进程(相当于管理进程),另一类称为Worker进程(实际工作进程)。启动方式有两种:
(1)单进程启动:此时系统中仅有一个进程,该进程既充当Master进程的角色,也充当Worker进程的角色。
(2)多进程启动:此时系统有且仅有一个Master进程,至少有一个Worker进程工作。
(3)Master进程主要进行一些全局性的初始化工作和管理Worker的工作;事件处理是在Worker中进行的。
1、Redis的的用途和场景
高性能与高并发 各种业务场景如何利用Redis秀操作,可以参考《Redis数据说明》
2、使用不当的后果
缓存与数据库的数据一致性
缓存雪崩
缓存穿透
缓存并发竞争
3、redis为啥高性能
绝大部分请求是纯粹的内存操作(非常快速),数据存在内存中,类似于HashMap,HashMap的优势就是查找和操作的时间复杂度都是O(1);
数据结构简单,对数据操作也简单
采用单线程,避免了不必要的上下文切换和竞争条件,也不存在多进程或者多线程导致的切换而消耗 CPU,不用去考虑各种锁的问题,不存在加锁释放锁操作,没有因为可能出现死锁而导致的性能消耗;
非阻塞IO - IO多路复用,“多路”指的是多个网络连接,“复用”指的是复用同一个线程。
4、redis的数据类型
string(字符串),hash(哈希),list(列表),set(集合)及zset(sorted set:有序集合)。
5、redis的过期策略
两种策略组合,保证过期的key一定能删掉。
定期删除,每隔100ms随机抽取设置过期时间的key,检查是否过期。(避免过多数据的轮询耗时)
惰性删除,key查询时再判断是否过期,过期了再进行确认,过期则删除。
LRU内存淘汰策略,进行过期key(最近最少使用)的内存释放。可以用LinkedHashMap自己手写一个.
6.redis如何支持高并发
单机redis的并发在几万(高性能的主机可以支撑到10w+),一般会采用主从架构+读写分离,一般是一主多从,主负责写并将数据同步复制到其他的slave节点上去,从节点负责读,所有的读请求都要走从节点,这样可以支撑起一个水平扩展的读高并发架构。
7、redis replication主从复制的核心原理
当启动一个salve节点时,它首先会发送一个psync的命令给master节点,如果是第一次连接,就会触发全量同步,master会异步的生成一份rdb快照文件(无磁盘化复制,内存中创建不会落盘),同时还会将这段时间从客户端收到的所有写命令缓存在内存中。rdb生辰后,发送给slave节点,slave写入磁盘再加载到内存中,然后master会讲内存中缓存的写命令也同步给slave,slave也会去同步这部分增量数据。
master和slave都会维护一个offset,不仅仅用于全量复制,让master和slave都知道相互之间一个数据一致性情况。
8、断点续传
全量主从复制支持断点续传,master节点在内存中维护了backlog,记录了一个同步的偏移量,如果断线重连以后,会从backlog中记录的偏移量位置开始进行同步,如果没有找到offset就会触发全量同步。
9、redis的高可用机制
redis的高可用是通过哨兵机制来保障的。类似zk,可以实现集群监控、消息通知、故障转移和配置中心的作用。哨兵本身也是分布式的:
故障转移,判断一个master挂了,需要多数哨兵同意才行,需要重新选举。
哨兵本身也是集群模式,部分节点挂了,还是能工作,同时为了方便选举哨兵的节点数最好设置成奇数个。
a.社区版本推出的原生高可用解决方案
Redis哨兵架构
当故障转移的工作流程就是,当Master宕机的时候,Sentinel会选举出新的Master,并根据Sentinel中client-reconfig-script脚本配置的内容,去动态修改VIP(虚拟IP),将VIP(虚拟IP)指向新的Master。我们的客户端就连向指定的VIP即可!
缺陷: (1)主从切换的过程中会丢数据 (2)Redis只能单点写,不能水平扩容
b.开源Proxy+Replication+Sentinel
前端使用Twemproxy+KeepAlived做代理,将其后端的多台Redis实例分片进行统一管理与分配
HA proxy+KeepLive
每一个分片节点的Slave都是Master的副本且只读;Sentinel持续不断的监控每个分片节点的Master,当Master出现故障且不可用状态时,Sentinel会通知/启动自动故障转移等动作;Sentinel 可以在发生故障转移动作后触发相应脚本(通过 client-reconfig-script 参数配置 ),脚本获取到最新的Master来修改Twemproxy配置。
缺陷: (1)部署结构超级复杂 (2)可扩展性差,进行扩缩容需要手动干预 (3)运维不方便
10、redis哨兵的选举算法
判断master失败的两种状态:
sdown,主观宕机,一个哨兵ping不同一个master,超过一定时刻就主观认为sdown了。
odown,客观宕机,如果有quorum数量的哨兵都认为master宕机了,就客观认为master宕机了。
如果一个master被认为odown,且majority哨兵都允许了准备切换,选举一个slave为一个master。
slave priority越低,选举为master的优先级越高
slave priority相同,replica offset越靠后,优先级越高(从master同步数据越多)
上面都相同,就选择一个run id较小的slave。
source: 史上最全 Redis 高可用解决方案总结
11、redis挂掉以后的数据恢复(持久化机制)
redis持久化主要功能就是为了保证灾难恢复,数据恢复。可以归类于高可用。关于持久化的AOF和RDB,二者区别介绍如下:
RDB持久化是通过将某个时间点Redis服务器存储的数据保存到RDB文件中来实现持久化的。AOF持久化是通过将Redis服务器执行的所有写命令保存到AOF文件中来实现持久化的。
RDB持久化记录的是结果,AOF持久化记录的是过程,所以AOF持久化生成的AOF文件会有体积越来越大的问题,Redis提供了AOF重写功能来减小AOF文件体积。
AOF持久化的安全性要比RDB持久化的安全性高,即如果发生机器故障,AOF持久化要比RDB持久化丢失的数据要少。
如果Redis服务器开启了AOF持久化功能(默认是关闭的),Redis服务器在启动时会使用AOF文件来还原数据,如果Redis服务器没有开启AOF持久化功能,Redis服务器在启动时会使用RDB文件来还原数据,所以AOF文件的优先级比RDB文件的优先级高。
12、RDB中save和Bgsave的区别?
Redis提供了2个命令来创建RDB文件,一个是SAVE,另一个是BGSAVE。
SAVE命令会阻塞Redis服务器进程,直到RDB文件创建完毕为止,在服务器进程阻塞期间,服务器不能处理任何命令请求。
BGSAVE命令会派生出一个子进程,然后由子进程负责创建RDB文件,服务器进程(父进程)继续处理命令请求
BGSAVE命令可以在不阻塞服务器进程的情况下执行,所以推荐使用BGSAVE命令。
13、RDB和AOF如何选择?
可以综合使用,用AOF来保证数据不丢失,作为恢复数据的第一选择;用RDB来做冷备,在AOF或者磁盘损坏不可用的情况下,还有RDB作为快速恢复的手段。
source: 阿里二面:熟悉Redis?讲讲你理解的Redis的持久化机制(RDB、AOF)
14、Redis的架构选择
如果数据量很少,主要是承载高并发高性能的场景,比如你的缓存一般就几个G,单机足够了
主从+replication,数据量一般,满足高并发,一个mater,多个slave,要几个slave跟你的要求的读吞吐量有关系,然后自己搭建一个sentinal集群,去保证redis主从架构的高可用性,就可以了
redis cluster,主要是针对海量数据+高并发+高可用的场景,海量数据,如果你的数据量很大,那么建议就用redis cluster
15、Redis Cluster
客户端与Redis节点直连,不需要中间Proxy层,直接连接任意一个Master节点;根据公式HASH_SLOT=CRC16(key) mod 16384,计算出映射到哪个分片上,然后Redis会去相应的节点进行操作
Redis cluster架构
具有如下优点: (1)无需Sentinel哨兵监控,如果Master挂了,Redis Cluster内部自动将Slave切换Master (2)可以进行水平扩容 (3)支持自动化迁移,当出现某个Slave宕机了,那么就只有Master了,这时候的高可用性就无法很好的保证了,万一Master也宕机了,咋办呢? 针对这种情况,如果说其他Master有多余的Slave ,集群自动把多余的Slave迁移到没有Slave的Master 中。
缺点: (1)批量操作是个坑(mget) (2)资源隔离性较差,容易出现相互影响的情况。
source: 那些年用过的Redis集群架构(含面试解析)
16、Redis cluster的高可用性
整个流程跟哨兵相比,非常类似,所以说,redis cluster功能强大,直接集成了replication和sentinal的功能。不过在通信协议上整个采用了gossip协议,跟集中式的哨兵协议不同,不是将集群元数据(节点信息,故障,等等)集中存储在某个节点上,而是互相之间不断通信,保持整个集群所有节点的数据是完整的。其他在判断节点是否当季、节点过滤和选举上的思路都是少数服从多少的思路,基本一致。
集中式:好处在于,元数据的更新和读取,时效性非常好,一旦元数据出现了变更,立即就更新到集中式的存储中,其他节点读取的时候立即就可以感知到; 不好在于,所有的元数据的跟新压力全部集中在一个地方,可能会导致元数据的节点有压力。
gossip:好处在于,元数据的更新比较分散,不是集中在一个地方,更新请求会陆陆续续,打到所有节点上去更新,有一定的延时,降低了压力; 缺点,元数据更新有延时,可能导致集群的一些操作会有一些滞后。
17、数据分区的选择
a.哈希取余分区
哈希取余分区思路非常简单:计算 key 的 hash 值,然后对节点数量进行取余,从而决定数据映射到哪个节点上。 该方案最大的问题是,当新增或删减节点时,节点数量发生变化,系统中所有的数据都需要重新计算映射关系,引发大规模数据迁移。
b.一致性哈希分区
一致性哈希算法将整个哈希值空间组织成一个虚拟的圆环,如下图所示,范围为 0-2^32-1。对于每个数据,根据 key 计算 hash 值,确定数据在环上的位置,然后从此位置沿环顺时针行走,找到的第一台服务器就是其应该映射到的服务器。
与哈希取余分区相比,一致性哈希分区将增减节点的影响限制在相邻节点。一致性哈希分区的主要问题在于,当节点数量较少时,增加或删减节点,对单个节点的影响可能很大,造成数据的严重不平衡;同时也需要保证节点能够均匀分布在哈希环,使得缓存数据能够均匀分布。
c.带虚拟节点的一致性哈希分区(哈希槽)
该方案在一致性哈希分区的基础上,引入了虚拟节点的概念。Redis 集群使用的便是该方案,其中的虚拟节点称为槽(slot)。槽是介于数据和实际节点之间的虚拟概念;每个实际节点包含一定数量的槽,每个槽包含哈希值在一定范围内的数据。引入槽以后,数据的映射关系由数据 hash->实际节点,变成了数据 hash->槽->实际节点。在使用了槽的一致性哈希分区中,槽是数据管理和迁移的基本单位。槽解耦了数据和实际节点之间的关系,增加或删除节点对系统的影响很小。槽的数量一般远小于 2^32,远大于实际节点的数量;在 Redis 集群中,槽的数量为 16384。
数据映射
Redis 集群将数据映射到实际节点的过程:
Redis 对数据的特征值(一般是key)计算哈希值,使用的算法是 CRC16。
根据哈希值,计算数据属于哪个槽。
根据槽与节点的映射关系,计算数据属于哪个节点。
source: 深入学习 Redis 集群搭建方案及实现原理
18、缓存雪崩
故障原因:redis挂了 事前:redis高可用,主从+哨兵,redis cluster,避免全盘崩溃 事中:本地cache缓存 + hystrix限流&降级,避免MySQL被打死 事后:redis持久化,快速恢复缓存数据
故障原因2:缓存时采用了相同的过期时间,导致缓存在某一时刻同时失效 将缓存失效时间分散开,比如我们可以在原有的失效时间基础上增加一个随机值,比如1-5分钟随机,这样每一个缓存的过期时间的重复率就会降低,就很难引发集体失效的事件。
19、缓存穿透
缓存穿透是指缓存和数据库中都没有的数据,而用户不断发起请求,如发起为id为“-1”的数据或id为特别大不存在的数据。这时的用户很可能是攻击者,攻击会导致数据库压力过大。解决办法有两个:
查询数据库不存在,则在redis中存一份Null值,避免访问Mysql
采用布隆过滤器,将List数据装载入布隆过滤器中,访问经过布隆过滤器,存在才可以往db中查询。于是在内存中就可以拦截恶意请求。
20、数据库与缓存一致性
Cache Aside Pattern,基本都采用如下的缓存模式:
读的时候,先读缓存,缓存没有的话,那么就读数据库,然后取出数据后放入缓存,同时返回响应
更新的时候,先删除缓存,然后再更新数据库
更新操作中为啥是直接删除而不是更新? 更新缓存的代价是很高的,删除缓存,而不是更新缓存,就是一个lazy计算的思想,不要每次都重新做复杂的计算,不管它会不会用到,而是让它到需要被使用的时候再重新计算。
21、高并发情况下如何保证一致性
还是应用上面的缓存模式来看:数据发生了变更,先删除了缓存,然后要去修改数据库,此时还没修改 一个请求过来,去读缓存,发现缓存空了,去查询数据库,查到了修改前的旧数据,放到了缓存中,数据变更的程序完成了数据库的修改,此时就会出现缓存和数据库中数据不一致的情况!
数据库与缓存更新与读取操作进行异步串行化 更新数据的时候,根据数据的唯一标识,将操作路由之后,发送到一个jvm内部的队列中,读取数据的时候,如果发现数据不在缓存中,那么将重新读取数据+更新缓存的操作,根据唯一标识路由之后,也发送同一个jvm内部的队列中,一个队列对应一个工作线程,每个工作线程串行拿到对应的操作,然后一条一条的执行
这样的话,一个数据变更的操作,先执行,删除缓存,然后再去更新数据库,但是还没完成更新。此时如果一个读请求过来,读到了空的缓存,那么可以先将缓存更新的请求发送到队列中,此时会在队列中积压,然后同步等待缓存更新完成。
22、生产环境中的redis是怎么部署的?
redis-cluster模式,企业数据架构组维护,10台机器,5主+5从,每个主实例挂了一个从实例,5个节点对外提供读写服务,单节点qps峰值可能可以达到每秒2万,5台机器并发峰值为10万读写请求/s。
机器是什么配置?(4c8g或者8c16g),但是分配给redis进程的是内存,一般线上生产环境,redis的内存尽量不要超过10g,超过10g可能会有问题。给分配了一半4g。
高可用策略:因为每个主实例都挂了一个从实例,所以是高可用的,任何一个主实例宕机,都会自动故障迁移,redis从实例会自动变成主实例继续提供读写服务.
往内存里写的是什么数据?每条数据的大小是多少?用户登陆,每条数据是2k。1000个用户2m,50万条数据是1g。仅仅不到总内存的1/4。
目前高峰期每秒就是3000左右的请求量。