目录
一、Redis集群模型
1.1、主从模式
1.1.1 主从模式优缺点
1.2、哨兵模式
1.2.1 哨兵模式的作用:
1.2.2 哨兵实现原理
1.2.3 主观下线和客观下线
1.2.4 哨兵模式优缺点
1.3、各大厂的Redis集群方案
1.3.1 客户端分片
1.3.2 代理分片
Twemproxy的优点:
Twemproxy的不足:
1.3.3 Codis
1.4、Redis Cluster
二.RDB持久化
2.1 RDB全量写入
2.1.1 rdb原理
2.1.2rdb模式
2.1.3rdb触发情况
2.1.4rdb优势和劣势
2.1.5rdb文件配置
2.1.6rdb命令配置
2.1.7rdb数据恢复
2.2 AOF持久化:增量写入
2.2.1aof原理
2.2.2 aof触发情况
2.2.3 aof优势和劣势
2.2.4 aof文件配置
2.2.5 aof命令配置
2.2.6 aof数据恢复
2.3.RDB&AOF对比
三、Redis高阶
3.1 Redis高阶之缓存击穿
3.1.1 缓存击穿定义
3.1.2缓存击穿产生原因
3.1.3缓存击穿解决方案
3.1.4缓存击穿解决方案之互斥锁详解
3.2 Redis高阶之缓存穿透
3.2.1 缓存穿透定义
3.2.2缓存穿透产生的原因
3.2.3缓存穿透的三种解决方案
3.2.4缓存穿透解决方案之布隆过滤器详解
3.3 Redis高阶之缓存雪崩
3.3.1缓存雪奔定义
3.3.2产生雪崩的原因
3.3.3 缓存雪崩的四种解决方案
3.3.4 缓存雪崩解决方案之加锁限流详解
Redis常见面试问题
Redis简介
Redis,英文全称是Remote Dictionary Server(远程字典服务),是一个开源的使用ANSI C语言编写、支持网络、可基于内存亦可持久化的日志型、Key-Value数据库,并提供多种语言的API。
与memcached一样,为了保证效率,数据都是缓存在内存中。区别的是redis会周期性的把更新的数据写入磁盘或者把修改操作写入追加的记录文件,并且在此基础上实现了master-slave(主从)同步。
与MySQL数据库不同的是,Redis的数据是存在内存中的。它的读写速度非常快,每秒可以处理超过10万次读写操作。因此redis被广泛应用于缓存,另外,Redis也经常用来做分布式锁。除此之外,Redis支持事务、持久化、LUA 脚本、LRU 驱动事件、多种集群方案。
Redis 是一个高性能的key-value数据库。 redis的出现,很大程度补偿了memcached这类key/value存储的不足,在部 分场合可以对关系数据库起到很好的补充作用。
Redis支持主从同步。数据可以从主服务器向任意数量的从服务器上同步,从服务器可以是关联其他从服务器的主服务器。
一、Redis集群模型
在服务开发中,单机都会存在单点故障的问题,及服务部署在一场台服务器上,一旦服务器宕机服务就不可用,所以为了让服务高可用,分布式服务就出现了,将同一服务部署到多台机器上,即使其中几台服务器宕机,只要有一台服务器可用服务就可用。
redis也是一样,为了解决单机故障引入了主从模式,但主从模式存在一个问题:master节点故障后服务,需要人为的手动将slave节点切换成为maser节点后服务才恢复。redis为解决这一问题又引入了哨兵模式,哨兵模式能在master节点故障后能自动将salve节点提升成master节点,不需要人工干预操作就能恢复服务可用。
但是主从模式、哨兵模式都没有达到真正的数据sharding存储,每个redis实例中存储的都是全量数据,所以redis cluster就诞生了,实现了真正的数据分片存储。但是由于redis cluster发布得比较晚(2015年才发布正式版 ),各大厂等不及了,陆陆续续开发了自己的redis数据分片集群模式,比如:Twemproxy、Codis等。
1.1、主从模式
redis单节点虽然有通过RDB和AOF持久化机制能将数据持久化到硬盘上,但数据是存储在一台服务器上的,如果服务器出现硬盘故障等问题,会导致数据不可用,而且读写无法分离,读写都在同一台服务器上,请求量大时会出现I/O瓶颈。
为了避免单点故障 和 读写不分离,Redis 提供了复制(replication)功能实现master数据库中的数据更新后,会自动将更新的数据同步到其他slave数据库上。
如上redis主从结构特点:一个master可以有多个salve节点;salve节点可以有slave节点,从节点是级联结构。
1.1.1 主从模式优缺点
优点: 主从结构具有读写分离,提高效率、数据备份,提供多个副本等优点。
不足: 最大的不足就是主从模式不具备自动容错和恢复功能,主节点故障,集群则无法进行工作,可用性比较低,从节点升主节点需要人工手动干预。
普通的主从模式,当主数据库崩溃时,需要手动切换从数据库成为主数据库:
在从数据库中使用
SLAVE NO ONE
命令将从数据库提升成主数据继续服务。启动之前崩溃的主数据库,然后使用SLAVEOF命令将其设置成新的主数据库的从数据库,即可同步数据。
1.2、哨兵模式
第一种主从同步/复制的模式,当主服务器宕机后,需要手动把一台从服务器切换为主服务器,这就需要人工干预,费事费力,还会造成一段时间内服务不可用,这时候就需要哨兵模式登场了。
哨兵模式是从Redis的2.6版本开始提供的,但是当时这个版本的模式是不稳定的,直到Redis的2.8版本以后,这个哨兵模式才稳定下来。
哨兵模式核心还是主从复制,只不过在相对于主从模式在主节点宕机导致不可写的情况下,多了一个竞选机制:从所有的从节点竞选出新的主节点。竞选机制的实现,是依赖于在系统中启动一个sentinel进程。
如上图,哨兵本身也有单点故障的问题,所以在一个一主多从的Redis系统中,可以使用多个哨兵进行监控,哨兵不仅会监控主数据库和从数据库,哨兵之间也会相互监控。每一个哨兵都是一个独立的进程,作为进程,它会独立运行。
1.2.1 哨兵模式的作用:
监控所有服务器是否正常运行:通过发送命令返回监控服务器的运行状态,处理监控主服务器、从服务器外,哨兵之间也相互监控。
故障切换:当哨兵监测到master宕机,会自动将slave切换成master,然后通过发布订阅模式通知其他的从服务器,修改配置文件,让它们切换master。同时那台有问题的旧主也会变为新主的从,也就是说当旧的主即使恢复时,并不会恢复原来的主身份,而是作为新主的一个从。
1.2.2 哨兵实现原理
哨兵在启动进程时,会读取配置文件的内容,通过如下的配置找出需要监控的主数据库:
sentinelmonitormaster-nameipportquorum#master-name是主数据库的名字#ip和port是当前主数据库地址和端口号#quorum表示在执行故障切换操作前,需要多少哨兵节点同意。
这里之所以只需要连接主节点,是因为通过主节点的info命令,获取从节点信息,从而和从节点也建立连接,同时也能通过主节点的info信息知道新增从节点的信息。
一个哨兵节点可以监控多个主节点,但是并不提倡这么做,因为当哨兵节点崩溃时,同时有多个集群切换会发生故障。哨兵启动后,会与主数据库建立两条连接。
订阅主数据库
_sentinel_:hello
频道以获取同样监控该数据库的哨兵节点信息定期向主数据库发送info命令,获取主数据库本身的信息。
跟主数据库建立连接后会定时执行以下三个操作:
(1)每隔10s向master和 slave发送info命令。作用是获取当前数据库信息,比如发现新增从节点时,会建立连接,并加入到监控列表中,当主从数据库的角色发生变化进行信息更新。
(2)每隔2s向主数据里和从数据库的_sentinel_:hello
频道发送自己的信息。作用是将自己的监控数据和哨兵分享。每个哨兵会订阅数据库的_sentinel:hello
频道,当其他哨兵收到消息后,会判断该哨兵是不是新的哨兵,如果是则将其加入哨兵列表,并建立连接。
(3)每隔1s向所有主从节点和所有哨兵节点发送ping命令,作用是监控节点是否存活。
1.2.3 主观下线和客观下线
哨兵节点发送ping命令时,当超过一定时间(down-after-millisecond
)后,如果节点未回复,则哨兵认为主观下线。主观下线表示当前哨兵认为该节点已经下面,如果该节点为主数据库,哨兵会进一步判断是够需要对其进行故障切换,这时候就要发送命令(SENTINEL is-master-down-by-addr
)询问其他哨兵节点是否认为该主节点是主观下线,当达到指定数量(quorum)时,哨兵就会认为是客观下线。
当主节点客观下线时就需要进行主从切换,主从切换的步骤为:
选出领头哨兵。
领头哨兵所有的slave选出优先级最高的从数据库。优先级可以通过
slave-priority
选项设置。如果优先级相同,则从复制的命令偏移量越大(即复制同步数据越多,数据越新),越优先。
如果以上条件都一样,则选择run ID较小的从数据库。
选出一个从数据库后,哨兵发送slave no one
命令升级为主数据库,并发送slaveof命令将其他从节点的主数据库设置为新的主数据库。
1.2.4 哨兵模式优缺点
1.优点
哨兵模式是基于主从模式的,解决可主从模式中master故障不可以自动切换故障的问题。
2.不足-问题
是一种中心化的集群实现方案:始终只有一个Redis主机来接收和处理写请求,写操作受单机瓶颈影响。
集群里所有节点保存的都是全量数据,浪费内存空间,没有真正实现分布式存储。数据量过大时,主从同步严重影响master的性能。
Redis主机宕机后,哨兵模式正在投票选举的情况之外,因为投票选举结束之前,谁也不知道主机和从机是谁,此时Redis也会开启保护机制,禁止写操作,直到选举出了新的Redis主机。
主从模式或哨兵模式每个节点存储的数据都是全量的数据,数据量过大时,就需要对存储的数据进行分片后存储到多个redis实例上。此时就要用到Redis Sharding技术。
1.3、各大厂的Redis集群方案
Redis在3.0版本前只支持单实例模式,虽然Redis的开发者Antirez早在博客上就提出在Redis 3.0版本中加入集群的功能,但3.0版本等到2015年才发布正式版。各大企业等不急了,在3.0版本还没发布前为了解决Redis的存储瓶颈,纷纷推出了各自的Redis集群方案。这些方案的核心思想是把数据分片(sharding)存储在多个Redis实例中,每一片就是一个Redis实例。
1.3.1 客户端分片
客户端分片是把分片的逻辑放在Redis客户端实现,(比如:jedis已支持Redis Sharding功能,即ShardedJedis),通过Redis客户端预先定义好的路由规则(使用一致性哈希),把对Key的访问转发到不同的Redis实例中,查询数据时把返回结果汇集。这种方案的模式如图所示。
客户端分片的优缺点:
优点:客户端sharding技术使用hash一致性算法分片的好处是所有的逻辑都是可控的,不依赖于第三方分布式中间件。服务端的Redis实例彼此独立,相互无关联,每个Redis实例像单服务器一样运行,非常容易线性扩展,系统的灵活性很强。开发人员清楚怎么实现分片、路由的规则,不用担心踩坑。
1.一致性哈希算法:
是分布式系统中常用的算法。比如,一个分布式的存储系统,要将数据存储到具体的节点上,如果采用普通的hash方法,将数据映射到具体的节点上,如mod(key,d),key是数据的key,d是机器节点数,如果有一个机器加入或退出这个集群,则所有的数据映射都无效了。
一致性哈希算法解决了普通余数Hash算法伸缩性差的问题,可以保证在上线、下线服务器的情况下尽量有多的请求命中原来路由到的服务器。
2.实现方式:一致性hash算法,比如MURMUR_HASH散列算法、ketamahash算法
比如Jedis的Redis Sharding实现,采用一致性哈希算法(consistent hashing),将key和节点name同时hashing,然后进行映射匹配,采用的算法是MURMUR_HASH。
采用一致性哈希而不是采用简单类似哈希求模映射的主要原因是当增加或减少节点时,不会产生由于重新匹配造成的rehashing。一致性哈希只影响相邻节点key分配,影响量小。
不足:
这是一种静态的分片方案,需要增加或者减少Redis实例的数量,需要手工调整分片的程序。
运维成本比较高,集群的数据出了任何问题都需要运维人员和开发人员一起合作,减缓了解决问题的速度,增加了跨部门沟通的成本。
在不同的客户端程序中,维护相同的路由分片逻辑成本巨大。比如:java项目、PHP项目里共用一套Redis集群,路由分片逻辑分别需要写两套一样的逻辑,以后维护也是两套。
客户端分片有一个最大的问题就是,服务端Redis实例群拓扑结构有变化时,每个客户端都需要更新调整。如果能把客户端分片模块单独拎出来,形成一个单独的模块(中间件),作为客户端 和 服务端连接的桥梁就能解决这个问题了,此时代理分片就出现了。
1.3.2 代理分片
redis代理分片用得最多的就是Twemproxy,由Twitter开源的Redis代理,其基本原理是:通过中间件的形式,Redis客户端把请求发送到Twemproxy,Twemproxy根据路由规则发送到正确的Redis实例,最后Twemproxy把结果汇集返回给客户端。
Twemproxy通过引入一个代理层,将多个Redis实例进行统一管理,使Redis客户端只需要在Twemproxy上进行操作,而不需要关心后面有多少个Redis实例,从而实现了Redis集群。
Twemproxy的优点:
客户端像连接Redis实例一样连接Twemproxy,不需要改任何的代码逻辑。
支持无效Redis实例的自动删除。
Twemproxy与Redis实例保持连接,减少了客户端与Redis实例的连接数。
Twemproxy的不足:
由于Redis客户端的每个请求都经过Twemproxy代理才能到达Redis服务器,这个过程中会产生性能损失。
没有友好的监控管理后台界面,不利于运维监控。
Twemproxy最大的痛点在于,无法平滑地扩容/缩容。对于运维人员来说,当因为业务需要增加Redis实例时工作量非常大。
Twemproxy作为最被广泛使用、最久经考验、稳定性最高的Redis代理,在业界被广泛使用。
1.3.3 Codis
Twemproxy不能平滑增加Redis实例的问题带来了很大的不便,于是豌豆荚自主研发了Codis,一个支持平滑增加Redis实例的Redis代理软件,其基于Go和C语言开发,并于2014年11月在GitHub上开源。
在Codis的架构图中,Codis引入了Redis Server Group
,其通过指定一个主CodisRedis和一个或多个从CodisRedis,实现了Redis集群的高可用。当一个主CodisRedis挂掉时,Codis不会自动把一个从CodisRedis提升为主CodisRedis,这涉及数据的一致性问题(Redis本身的数据同步是采用主从异步复制,当数据在主CodisRedis写入成功时,从CodisRedis是否已读入这个数据是没法保证的),需要管理员在管理界面上手动把从CodisRedis提升为主CodisRedis。
如果手动处理觉得麻烦,豌豆荚也提供了一个工具Codis-ha
,这个工具会在检测到主CodisRedis挂掉的时候将其下线并提升一个从CodisRedis为主CodisRedis。
Codis中采用预分片的形式,启动的时候就创建了1024个slot,1个slot相当于1个箱子,每个箱子有固定的编号,范围是1~1024。slot这个箱子用作存放Key,至于Key存放到哪个箱子,可以通过算法“crc32(key)%1024
”获得一个数字,这个数字的范围一定是1~1024之间,Key就放到这个数字对应的slot。
例如,如果某个Key通过算法“crc32(key)%1024
”得到的数字是5,就放到编码为5的slot(箱子)。1个slot只能放1个Redis Server Group
,不能把1个slot放到多个Redis Server Group
中。1个Redis Server Group
最少可以存放1个slot,最大可以存放1024个slot。因此,Codis中最多可以指定1024个Redis Server Group
。
Codis最大的优势在于支持平滑增加(减少)Redis Server Group
(Redis实例),能安全、透明地迁移数据,这也是Codis 有别于Twemproxy等静态分布式 Redis 解决方案的地方。Codis增加了Redis Server Group
后,就牵涉到slot的迁移问题。
例如,系统有两个Redis Server Group
,Redis Server Group
和slot的对应关系如下。
当增加了一个Redis Server Group,slot就要重新分配了。Codis分配slot有两种方法:
第一种:通过Codis管理工具Codisconfig手动重新分配,指定每个Redis Server Group所对应的slot的范围,例如:可以指定Redis Server Group和slot的新的对应关系如下。
第二种:通过Codis管理工具Codisconfig的rebalance功能,会自动根据每个Redis Server Group的内存对slot进行迁移,以实现数据的均衡。
1.4、Redis Cluster
Redis 的哨兵模式虽然已经可以实现高可用,读写分离 ,但是存在几个方面的不足:
哨兵模式下每台 Redis 服务器都存储相同的数据,很浪费内存空间;数据量太大,主从同步时严重影响了master性能。
哨兵模式是中心化的集群实现方案,每个从机和主机的耦合度很高,master宕机到salve选举master恢复期间服务不可用。
哨兵模式始终只有一个Redis主机来接收和处理写请求,写操作还是受单机瓶颈影响,没有实现真正的分布式架构。
redis在3.0上加入了 Cluster 集群模式,实现了 Redis 的分布式存储,也就是说每台 Redis 节点上存储不同的数据。cluster模式为了解决单机Redis容量有限的问题,将数据按一定的规则分配到多台机器,内存/QPS不受限于单机,可受益于分布式集群高扩展性。
Redis Cluster是一种服务器Sharding技术(分片和路由都是在服务端实现),采用多主多从,每一个分区都是由一个Redis主机和多个从机组成,片区和片区之间是相互平行的。Redis Cluster集群采用了P2P的模式,完全去中心化。
如上图,官方推荐,集群部署至少要 3 台以上的master节点,最好使用 3 主 3 从六个节点的模式。Redis Cluster集群具有如下几个特点:
集群完全去中心化,采用多主多从;所有的redis节点彼此互联(PING-PONG机制),内部使用二进制协议优化传输速度和带宽。
客户端与 Redis 节点直连,不需要中间代理层。客户端不需要连接集群所有节点,连接集群中任何一个可用节点即可。
每一个分区都是由一个Redis主机和多个从机组成,分片和分片之间是相互平行的。
每一个master节点负责维护一部分槽,以及槽所映射的键值数据;集群中每个节点都有全量的槽信息,通过槽每个node都知道具体数据存储到哪个node上。
redis cluster主要是针对海量数据+高并发+高可用的场景,海量数据,如果你的数据量很大,那么建议就用redis cluster,数据量不是很大时,使用sentinel就够了。redis cluster的性能和高可用性均优于哨兵模式。
Redis Cluster采用虚拟哈希槽分区而非一致性hash算法,预先分配一些卡槽,所有的键根据哈希函数映射到这些槽内,每一个分区内的master节点负责维护一部分槽以及槽所映射的键值数据。
二.RDB持久化
2.1 RDB全量写入
2.1.1 rdb原理
RDB
持久化是把当前进程数据生成快照保存到硬盘的过程,触发RDB
持久化过程分为手动触发和自动触发
过程:
- 1)执行bgsave命令,Redis父进程判断当前是否存在正在执行的子进程,如RDB/AOF子进程,如果存在bgsave命令直接返回。
- 2)父进程执行fork操作创建子进程,fork操作过程中父进程会阻塞,通过info stats命令查看latest_fork_usec选项,可以获取最近一个fork操作的耗时,单位为微秒。
- 3)父进程fork完成后,bgsave命令返回“Background saving started”信息并不再阻塞父进程,可以继续响应其他命令。
- 4)子进程创建RDB文件,根据父进程内存生成临时快照文件,完成后对原有文件进行原子替换 执行lastsave命令可以获取最后一次生成RDB的时间,对应info统计的rdb_last_save_time选项。
- 5)进程发送信号给父进程表示完成,父进程更新统计信息,具体见info Persistence下的rdb_*相关选项。
2.1.2rdb模式
SAVE 阻塞式的RDB持久化,当执行这个命令时间时rdis的主进程把内存里的数据库状态写入到rdb文件中,直到该文件创建完毕的这段时间内redis讲不能处理任何命令请求
BGSAVE 非阻塞式的持久化,它会创建一个子进程,专门去把内存中的数据库状态写入RDB文件,同时主进程还可以处理来自客户端的请求命令,但子进程基本是复制父进程,这等于两个相同大小的redis进程在系统上运行,会造成内存使用率的大幅增加。
2.1.3rdb触发情况
1.手动执行bgsave或save命令 2.根据配置文件的save选项自动触发 3.主从结构时,从节点执行全量复制操作,主节点自动执行,将生成的RDB文件发送给从 4.执行debug reload命令重新加载Redis时 5.默认情况下执行shutdown命令关闭redis时,如果没有开启AOF持久化功能则自动执行
2.1.4rdb优势和劣势
优势:
- 一旦采用该方式,那么你的整个Redis数据库将只包含一个文件,这对可以定时每天可以备份出一个整个的数据文件。
- 对于灾难恢复而言,RDB是非常不错的选择。因为我们可以非常轻松的将一个单独的文件压缩后再转移到其它存储介质上。
- 相比于AOF机制,如果数据集很大,RDB的启动效率会更高。
劣势:
- 由于RDB是通过fork子进程来协助完成数据持久化工作的,因此,如果当数据集较大时,可能会导致整个服务器停止服务几百毫秒,甚至是1秒钟。
- RDB文件使用特定二进制格式保存,Redis版本演进过程中有多个格式的RDB版本,存在老版本Redis服务无法兼容新版RDB格式的问题。
2.1.5rdb文件配置
redis.conf文件
#注释所有save行则停止rdb持久化#900秒(15分钟)内至少1个key值改变(则进行数据库保存--持久化)save 900 1#300秒(5分钟)内至少10个key值改变(则进行数据库保存--持久化)save 300 10#60秒(1分钟)内至少10000个key值改变(则进行数据库保存--持久化)save 60 10000#当RDB持久化出现错误后,再写入数据会报错,用于提示用户出问题了。#yes是开启,no是关闭,默认开启stop-writes-on-bgsave-error yes#是否压缩rdb文件,rdb文件压缩使用LZF压缩算法,压缩会消耗一些cpu,不压缩文件会很大#yes开启,no关闭,默认开启rdbcompression yes#使用CRC64算法来进行数据校验,防止RDB是错误的,但是这样做会增加大约10%的性能消耗#yes开启,no关闭,默认开启rdbchecksum yes
复制
2.1.6rdb命令配置
阻塞当前Redis服务器 直到RDB过程完成为止,对于内存比较大的实例会造成长时间阻塞,线上环境不建议使用。 save
Redis进程执行fork操作创建子进程,RDB持久化过程由子进程负责,完成后自动结束。阻塞只发生在fork阶段,一般时间很短。 bgsave
查看latest_fork_usec选项,可以获取最近一个fork操作的耗时,单位为微秒。 info stats
2.1.7rdb数据恢复
1.将RDB备份放到配置文件指定的数据目录下,启动redis将会自动恢复。加载期间将会阻塞,无法进行其它操作。
2.上述方法不行,或者恢复的集群,可以使用redis-migrate-tool工具进行恢复。
2.2 AOF持久化:增量写入
2.2.1aof原理
以独立日志的方式记录每次写命令,重启时再重新执行AOF文件中的命令达到恢复数据的目的。AOF的主要作用是解决了数据持久化的实时性,目前已经是Redis持久化的主流方式。
写入的数据具有可读性,同步时先写入缓冲区,再放入硬盘。如果直接写入硬盘,性能将取决于磁盘负载,并且放到缓冲区,可以提供各种同步策略。
过程:
- 1)所有的写入命令会追加到aof_buf(缓冲区)中。
- 2)AOF缓冲区根据对应的策略向硬盘做同步操作。
- 3)随着AOF文件越来越大,需要定期对AOF文件进行重写,达到压缩的目的。
- 4)当Redis服务器重启时,可以加载AOF文件进行数据恢复。
2.2.2 aof触发情况
1.根据配置文件自动触发
2.2.3 aof优势和劣势
优势:
- 该机制可以带来更高的数据安全性,即数据持久性。根据策略不同,从而对数据安全性不同,可以在性能和安全区选择一个。
- 由于该机制对日志文件的写入操作采用的是append模式,因此在写入过程中即使出现宕机现象,也不会破坏日志文件中已经存在的内容。
- 如果日志过大,将自动启用rewrite机制。以append模式不断的将修改数据写入到老的磁盘文件中,同时还会创建一个新的文件用于记录此期间有哪些修改命令被执行,保证安全性。
- AOF包含一个格式清晰、易于理解的日志文件用于记录所有的修改操作。事实上,我们也可以通过该文件完成数据的重建。
劣势:
- 对于相同数量的数据集而言,AOF文件通常要大于RDB文件。RDB 在恢复大数据集时的速度比 AOF 的恢复速度要快。
- 根据同步策略的不同,AOF在运行效率上往往会慢于RDB。总之,每秒同步策略的效率是比较高的,同步禁用策略的效率和RDB一样高效。
2.2.4 aof文件配置
在Redis的配置文件中存在三种同步方式,它们分别是:
#是否开启aof持久化。默认no,要打开appendonly yes#位置appendfilename "appendonly.aof"#每次有数据修改发生时都会写入AOF文件#命令写入aof_buf后调用系统fsync操作同步AOF文件,fsync完成后线程返回appendfsync always#每秒钟同步一次,该策略为AOF的缺省策略#命令写入aof_buf后调用系统write操作,write完成后线程返回。fsync同步文件操作由专门线程每秒调用一次#这个模式兼顾了效率的同时也保证了数据的完整性,即使在服务器宕机也只会丢失一秒内对redis数据库做的修改appendfsync everysec#不加入缓冲区,直接写到硬盘,速度最快,不安全#命令写入aof_buf后调用系统write操作,不对aof文件做fsync同步,同步硬盘操作由操作系统负责,通常同步周期最长30秒#这种模式下效率是最快的,但对数据来说也是最不安全的,如果redis里的数据都是从后台数据库如mysql中取出来的,属于随时可以找回或者不重要的数据,那么可以考虑设置成这种模式。appendfsync no
复制
2.2.5 aof命令配置
aof文件重写手动触发 bgrewriteaof
aof文件重写自动触发,配置文件
#新的aof文件大小是上次的aof文件的大小2倍(100)时,进行重写auto-aof-rewrite-percentage 100#表示运行AOF重写时文件最小体积, 默认为64MBauto-aof-rewrite-min-size 64mb
复制
2.2.6 aof数据恢复
- 将AOF备份放到配置文件指定的数据目录下,启动redis将会自动恢复。加载期间将会阻塞,无法进行其它操作。
- 上述方法不行,或者恢复的集群,可以使用redis-migrate-tool工具进行恢复。
- 可以使用pipline方式批量硬写入,但效率会低
2.3.RDB&AOF对比
二者选择的标准,就是看系统是愿意牺牲一些性能,换取更高的缓存一致性(aof),还是愿意写操作频繁的时候,不启用备份来换取更高的性能,待手动运行save的时候,再做备份(rdb)。
启动加载流程:
- AOF持久化开启且存在AOF文件时, 优先加载AOF文件
- AOF关闭或者AOF文件不存在时, 加载RDB文件
- 加载AOF/RDB文件成功后, Redis启动成功
- AOF/RDB文件存在错误时, Redis启动失败并打印错误信息
三、Redis高阶
3.1 Redis高阶之缓存击穿
3.1.1 缓存击穿定义
缓存击穿
,是指缓存中没有但数据库中有的数据
,并且某一个key非常热点
,在不停的扛着大并发,大并发集中对这一个点进行访问,当这个key在失效的瞬间(一般是缓存时间到期),持续的大并发就穿破缓存,直接请求数据库,就像在一个屏障上凿开了一个洞。
缓存击穿看着有点像,其实它两区别是,缓存雪奔是指数据库压力过大甚至down机,缓存击穿只是大量并发请求到了DB数据库层面。可以认为击穿是缓存雪奔的一个子集吧。有些文章认为它俩区别,是区别在于击穿针对某一热点key缓存,雪奔则是很多key。
3.1.2缓存击穿产生原因
可以看成缓存雪崩的一个特殊子集。
比如xxx塌房哩、xxx商品活动,这时候大量用户都在访问该热点事件,但是可能优于某种原因,redis的这个热点key过期了,那么这时候大量高并发对于该key的请求就得不到redis的响应,那么就会将请求直接打在DB服务器上,导致整个DB瘫痪。
3.1.3缓存击穿解决方案
- 使用互斥锁方案。缓存失效时,不是立即去加载db数据,而是先使用某些带成功返回的原子操作命令,如(Redis的setnx)去操作,成功的时候,再去加载db数据库数据和设置缓存。否则就去重试获取缓存。
- “永不过期”,是指没有设置过期时间,但是热点数据快要过期时,异步线程去更新和设置过期时间。
3.1.4缓存击穿解决方案之互斥锁详解
业界比较常用的做法,是使用mutex。简单地来说,就是在缓存失效的时候(判断拿出来的值为空),不是立即去load db,而是先使用缓存工具的某些带成功操作返回值的操作(比如Redis的SETNX或者Memcache的ADD)去set一个mutex key,当操作返回成功时,再进行load db的操作并回设缓存;否则,就重试整个get缓存的方法。
SETNX,是「SET if Not eXists」的缩写,也就是只有不存在的时候才设置,可以利用它来实现锁的效果。
public String get(key) {String value = redis.get(key);if (value == null) { //代表缓存值过期//设置3min的超时,防止del操作失败的时候,下次缓存过期一直不能load dbif (redis.setnx(key_mutex, 1, 3 * 60) == 1) {//代表设置成功 value = db.get(key); redis.set(key, value, expire_secs); redis.del(key_mutex);} else {//这个时候代表同时候的其他线程已经load db并回设到缓存了,这时候重试获取缓存值即可 sleep(50); get(key);//重试}} else {return value; } }
3.2 Redis高阶之缓存穿透
3.2.1 缓存穿透定义
缓存穿透
是指查询一个数据库一定不存在的数据
。正常的使用缓存流程大致是,数据查询先进行缓存查询,如果key不存在或者key已经过期,再对数据库进行查询,并把查询到的对象,放进缓存。如果数据库查询对象为空,则不放进缓存。
这里需要注意缓存击穿
的区别,缓存击穿,缓存击穿是指缓存中没有但数据库中有的数据
,并且某一个key非常热点
,在不停的扛着大并发,大并发集中对这一个点进行访问,当这个key在失效的瞬间(一般是缓存时间到期),持续的大并发就穿破缓存,直接请求数据库,就像在一个屏障上凿开了一个洞。
通俗点说,读请求访问时,缓存和数据库都没有某个值,这样就会导致每次对这个值的查询请求都会穿透到数据库,这就是缓存穿透。
3.2.2缓存穿透产生的原因
- 业务不合理的设计,比如大多数用户都没开守护,但是你的每个请求都去缓存,查询某个userid查询有没有守护。
- 业务/运维/开发失误的操作,比如缓存和数据库的数据都被误删除了。
- 黑客非法请求攻击,比如黑客故意捏造大量非法请求,以读取不存在的业务数据。
3.2.3缓存穿透的三种解决方案
- 如果是非法请求,我们在API入口,对参数进行校验,过滤非法值。
- 如果查询数据库为空,我们可以给缓存设置个空值,或者默认值。但是如有有写请求进来的话,需要更新缓存哈,以保证缓存一致性,同时,最后给缓存设置适当的过期时间。(业务上比较常用,简单有效)
- 使用布隆过滤器快速判断数据是否存在。即一个查询请求过来时,先通过布隆过滤器判断值是否存在,存在才继续往下查。
布隆过滤器原理:它由初始值为0的位图数组和N个哈希函数组成。一个对一个key进行N个hash算法获取N个值,在比特数组中将这N个值散列后设定为1,然后查的时候如果特定的这几个位置都为1,那么布隆过滤器判断该key存在。
3.2.4缓存穿透解决方案之布隆过滤器详解
布隆过滤器
是一个bit向量或者bit,如果我们要映射一个值到布隆过滤器中,我们使用多个不同的哈希函数生成多个哈希值,并将每个生成的哈希值指向的bit位设置为1,如下baidu一词设置了三个位置为1。
原理:对一个key进行k个hash算法获取k个值,在比特数组中将这k个值散列后设定为1,然后查的时候如果特定的这几个位置都为1,那么布隆过滤器判断该key存在。
“tencent”一词,对应的情况
可以看到,不同的词对应的bit位置可能相同,当词很多的情况时,可能大部分bit位置都是1,这时查询taobao可能对应的位置都为1,只能说明taobao一词可能存在,不是一定存在的,这时1就被覆盖了,这就是布隆过滤器的误判。如果它说不存在那肯定不存在,如果它说存在,那数据有可能实际不存在。
Redis的bitmap只支持2^32大小,对应到内存也就是512MB,误判率万分之一,可以放下2亿左右的数据,性能高,空间占用率及小,省去了大量无效的数据库连接。
因此我们可以通过布隆过滤器,将Redis缓存穿透控制在一个可容范围内。
使用布隆过滤器:
导入依赖
com.google.guava guava 19.0
代码:
public class Test {private static int size = 1000000;//预计要插入多少数据private static double fpp = 0.01;//期望的误判率private static BloomFilter bloomFilter = BloomFilter.create(Funnels.integerFunnel(), size, fpp);public static void main(String[] args) {//插入数据for (int i = 0; i < 1000000; i++) {bloomFilter.put(i);}int count = 0;for (int i = 1000000; i < 2000000; i++) {if (bloomFilter.mightContain(i)) {count++;System.out.println(i + "误判了");}}System.out.println("总共的误判数:" + count);}}
应用:
@Cacheable(value="key1")public String get(String key) {String value = redis.get(key);// redis中不存在该缓存if (value== null) {//布隆过滤器也没有,直接返回if(!bloomfilter.mightContain(key)){return null; }else{//布隆过滤器中能查到,不代表一定有,查出来放入redis,同样也可以避免缓存穿透value = db.get(key);redis.set(key, value); }}return value;}
(2)、缓存空对象
当存储层不命中后,即使返回的空对象也将其缓存起来,同时会设置一个过期时间,之后再访问这个数据将会从缓存中获取,保护了后端数据源。
但是这种方法会存在两个问题:
● 如果空值能够被缓存起来,这就意味着缓存需要更多的空间存储更多的键
,因为这当中可能会有很多的空值的键;
● 即使对空值设置了过期时间,还是会存在缓存层和存储层的数据会有一段时间窗口的不一致,这对于需要保持一致性的业务会有影响
3.3 Redis高阶之缓存雪崩
3.3.1缓存雪奔定义
指缓存中数据大批量到过期时间,而查询数据量巨大,请求都直接访问数据库,引起数据库压力过大甚至down机。
- 缓存雪奔一般是由于大量数据同时过期造成的,对于这个原因,可通过均匀设置过期时间解决,即让过期时间相对离散一点。如采用一个较大固定值+一个较小的随机值,5小时+0到1800秒酱紫。
- Redis 故障宕机也可能引起缓存雪奔。这就需要构造Redis高可用集群啦。
3.3.2产生雪崩的原因
假如马上就要到双十一零点,很快就会迎来一波抢购,这波商品时间比较集中的放入了缓存,假设缓存一个小时。那么到了凌晨一点钟的时候,这批商品的缓存就都过期了。而对这批商品的访问查询,都落到了数据库上,对于数据库而言,就会产生周期性的压力波峰。
3.3.3 缓存雪崩的四种解决方案
- 采取不同分类商品,缓存不同周期。在同一分类中的商品,加上一个随机因子。这样能尽可能分散缓存过期时间,而且,热门类目的商品缓存时间长一些,冷门类目的商品缓存时间短一些,也能节省缓存服务的资源。
- 如果缓存数据库是分布式部署,将 热点数据均匀分布在不同的缓存数据库中。
- 设置热点数据永远不过期。
- 使用加锁限流的方式。
3.3.4 缓存雪崩解决方案之加锁限流详解
Redis常见面试问题
Redis有哪些特性?
- 性能高, 读的速度是100000次/s,写的速度是80000次/s;
- 数据持久化,支持RDB 、AOF;
- 支持事务。通过MULTI和EXEC指令包起来;
- 多种数据结构类型;
- 主从复制;
- 其他特性:发布/订阅、通知、key过期等;
Redis为什么这么快?
- 完全基于内存,没有磁盘IO上的开销,异步持久化除外
- 单线程,避免多个线程切换的性能损耗
- 非阻塞的IO多路复用机制
- 底层的数据存储结构优化,使用原生的数据结构提升性能。
Redis底层的基础数据结构有哪些?
- 字符串。没有采用C语言的传统字符串,而是自己实现的一个简单动态字符串SDS的抽象类型,并保存了⻓度
- 信息。
- 链表(linkedlist)。双向无环链表结构,每个链表的节点由一个listNode结构来表示,每个节点都有前置和
- 后置节点的指针
- 字典(hashtable)。保存键值对的抽象数据结构,底层使用hash表,每个字典带有两个hash表,供平时使
- 用和rehash时使用。
- 跳跃表(skiplist)。跳跃表是有序集合的底层实现之一。redis跳跃表由zskiplist和zskiplistNode组成,
- zskiplist用于保存跳跃表 信息(表头、表尾节点、⻓度等),zskiplistNode用于表示表跳跃节点,每个跳跃表的
- 层高都是1- 32的随机数,在同一个跳跃表中,多个节点可以包含相同的分值,但是每个节点的成员对象必须
- 是唯一的,节点按照分值大小排序,如果分值相同,则按照成员对象的大小排序。
- 整数集合(intset)。用于保存整数值的集合抽象数据结构,不会出现重复元素,底层实现为数组。
- 压缩列表(ziplist)。为节约内存而开发的顺序性数据结构,可以包含多个节点,每个节点可以保存一个字节
- 数组或者整数值。
Redis支持哪些数据类型?
- 五种常用数据类型:String、Hash、Set、List、SortedSet。
- 三种特殊的数据类型:Bitmap、HyperLogLog、Geospatial,其中Bitmap 、HyperLogLog的底层都是 String 数据类型,Geospatial 底层是 Sorted Set 数据类型;
- 字符串对象string:int整数、embstr编码的简单动态字符串、raw简单动态字符串
- 列表对象list:ziplist、linkedlist
- 哈希对象hash:ziplist、hashtable
- 集合对象set:intset、hashtable
- 有序集合对象zset:ziplist、skiplist
Redis常用的5种数据结构和应用场景?
- String:缓存、计数器、分布式锁等
- List:链表、队列、微博关注人时间轴列表等
- Hash:用户信息、Hash 表等
- Set:去重、赞、踩、共同好友等
- Zset:访问量排行榜、点击量排行榜等
Redis为什么采用单线程?
官方回复,CPU不会成为Redis的制约瓶颈,Redis主要受内存、网络限制。
例如,在一个普通的 Linux 系统上,使用pipelining 可以每秒传递 100 万个请求,所以如果您的应用程序主要使用 O(N) 或 O(log(N)) 命令,则几乎不会使用太多 CPU,属于IO密集型系统。
Redis6.0之后又改用多线程呢
Redis的多线程主要是处理数据的读写、协议解析。执行命令还是采用单线程顺序执行。
主要是因为redis的性能瓶颈在于网络IO而非CPU,使用多线程进行一些周边预处理,提升了IO的读写效率,从而提高了整体的吞吐量。antirez 在 RedisConf 2019 分享时提到,Redis 6 引入的多线程 IO 对性能提升至少一倍以上。
Redis过期键Key 的删除策略有哪些?
有3种过期删除策略。惰性删除、定期删除、定时删除
- 惰性删除:使用key时才进行检查,如果已经过期,则删除,缺点:过期的key如果没有被访问到,一直无法删除,一直占用内存,造成空间浪费。
- 定期删除:每隔一段时间做一次检查,删除过期的key,每次只是随机取一些key去检查。
- 定时删除:为每个key设置过期时间,同时创建一个定时器。一旦到期,立即执行删除。缺点:如果过期键比较多时,占用CPU较多,对服务的性能有很大影响。
如果Redis的内存空间不足,淘汰机制?
- volatile-lru:从已设置过期时间的key中,移出最近最少使用的key进行淘汰
- allkeys-lru:当内存不足以容纳新写入数据时,在键空间中,移除最近最少使用的key(这个是最常用的)
- volatile-ttl:从已设置过期时间的key中,移出将要过期的key
- volatile-random:从已设置过期时间的key中,随机选择key淘汰
- allkeys-random:从key中随机选择key进行淘汰
- no-eviction:禁止淘汰数据。当内存达到阈值的时候,新写入操作报错
- volatile-lfu:从已设置过期时间的数据集(server.db[i].expires)中挑选最不经常使用的数据淘汰(LFU(Least
- Frequently Used)算法,也就是最频繁被访问的数据将来最有可能被访问到)
- allkeys-lfu:当内存不足以容纳新写入数据时,在键空间中,移除最不经常使用的key。
Redis突然挂了怎么解决?
- 从系统可用性⻆度思考,Redis Cluster引入主备机制,当主节点挂了后,自动切换到备用节点,继续提供服务。
- Client端引入本地缓存,通过开关切换,避免Redis突然挂掉,高并发流量把数据库打挂。
Redis持久化有哪些方式?
- 快照RDB。将某个时间点上的数据库状态保存到 RDB文件 中,RDB文件是一个压缩的二进制文件,保存在磁盘上。当Redis崩溃时,可用于恢复数据。通过 SAVE 或 BGSAVE 来生成RDB文件。
- SAVE:会阻塞redis进程,直到RDB文件创建完毕,在进程阻塞期间,redis不能处理任何命令请求。
- BGSAVE:会fork出一个子进程,然后由子进程去负责生成RDB文件,父进程还可以继续处理命令请求,不会阻塞进程。
- 只追加文件AOF。以日志的形式记录每个写操作(非读操作)。当不同节点同步数据时,读取日志文件的内容将写指令从前到后执行一次,即可完成数据恢复。
Redis常用场景
- 缓存,有句话说的好,「性能不够,缓存来凑」
- 分布式锁,利用Redis 的 setnx
- 分布式session
- 计数器,通过incr命令
- 排行榜,Redis 的 有序集合
- 其他
Redis 缓存要注意的七大经典问题?
列举了亿级系统,高访问量情况下Redis缓存可能会遇到哪些问题?以及对应的解决方案。
- 缓存集中失效
- 缓存穿透
- 缓存雪崩
- 缓存热点
- 缓存大Key
- 缓存数据的一致性
- 数据并发竞争预热
Redis集群方案有哪几种?
- 主从复制模式
- Sentinel(哨兵)模式
- Redis Cluster 模式
Redis主从数据同步(主从复制)的过程?
- slave启动后,向master发送sync命令
- master收到sync之后,执行bgsave保存快照,生成RDB全量文件
- master把slave的写命令记录到缓存
- bgsave执行完毕之后,发送RDB文件到slave,slave执行
- master发送缓冲区的写命令给slave,slave接收命令并执行,完成复制初始化。
- 此后,master每次执行一个写命令都会同步发送给slave,保持master与slave之间数据的一致性
Redis主从复制的优缺点?
- 1、优点:master能自动将数据同步到slave,可以进行读写分离,分担master的读压力master、slave之间的同步是以非阻塞的方式进行的,同步期间,客户端仍然可以提交查询或更新请求
- 缺点:不具备自动容错与恢复功能,master 节点宕机后,需要手动指定新的 master,master宕机,如果宕机前数据没有同步完,则切换IP后会存在数据不一致的问题,难以支持在线扩容,Redis的容量受限于单机配置
Redis Sentinel(哨兵)模式的优缺点?
哨兵模式基于主从复制模式,增加了哨兵来监控与自动处理故障。
- 优点:哨兵模式基于主从复制模式,所以主从复制模式有的优点,哨兵模式也有master 挂掉可以自动进行切换,系统可用性更高
- 缺点:Redis的容量受限于单机配置需要额外的资源来启动sentinel进程
Redis Cluster 模式的优缺点?
实现了Redis的分布式存储,即每台节点存储不同的内容,来解决在线扩容的问题。
- 优点:
- 无中心架构,数据按照slot分布在多个节点
- 集群中的每个节点都是平等的,每个节点都保存各自的数据和整个集群的状态。
- 每个节点都和其他所有节点连接,而且这些连接保持活跃,这样就保证了我们只需要连接集群中的任意一个节点,就可以获取到其他节点的数据。
- 可线性扩展到1000多个节点,节点可动态添加或删除。
- 能够实现自动故障转移,节点之间通过gossip协议交换状态信息,用投票机制完成slave到master的⻆色转换。
- 缺点:
- 数据通过异步复制,不保证数据的强一致性。
- slave充当 “冷备”,不对外提供读、写服务,只作为故障转移使用。
- 批量操作限制,目前只支持具有相同slot值的key执行批量操作,对mset、mget、sunion等操作支持不友好
- key事务操作支持有限,只支持多key在同一节点的事务操作,多key分布在不同节点时无法使用事务功能
- 不支持多数据库空间,一台redis可以支持16个db,集群模式下只能使用一个,即db0 。
- Redis Cluster模式不建议使用pipeline和multi-keys操作,减少max redirect产生的场景。
Redis 如何做扩容?
为了避免数据迁移失效,通常使用一致性哈希 实现动态扩容缩容,有效减少需要迁移的Key数量。
但是Cluster 模式,采用固定Slot槽位方式(16384个),对每个key计算CRC16值,然后对16384取模,然后根据slot值找到目标机器,扩容时,我们只需要迁移一部分的slot到新节点即可。
Redis的集群原理?
一个redis集群由多个节点node组成,而多个node之间通过 cluster meet命令来进行连接,组成一个集群。
数据存储通过分片的形式,整个集群分成了16384个slot,每个节点负责一部分槽位。整个槽位的信息会同步到所有节点中。
key与slot的映射关系:健值对 key,进行 CRC16 计算,计算出一个16bit的值,将16bit的值对16384取模,得到0~16383的数表示 key 对应的哈希槽
Redis 如何做到高可用?
哨兵机制。具有自动故障转移、集群监控、消息通知等功能。
哨兵可以同时监视所有的主、从服务器,当某个master下线时,自动提升对应的slave为master,然后由新master对外提供服务。
什么是Redis 事务?
Redis事务是一组命令的集合,将多个命令打包,然后把这些命令按顺序添加到队列中,并且按顺序执行这些命令。
Redis事务中没有像Mysql关系型数据库事务隔离级别的概念,不能保证原子性操作,也没有像Mysql那样执行事务失败会进行回滚操作
Redis事务执行流程?
通过MULTI 、EXEC、WATCH等命令来实现事务机制,事务执行过程将一系列多个命令按照顺序一次性执行,在执行期间,事务不会被中断,也不会去执行客户端的其他请求,直到所有命令执行完毕。
具体过程:服务端收到客户端请求,事务以MULTI 开始如果正处于事务状态时,则会把后续命令放入队列同时返回给客户端 QUEUED ,反之则直接执行这个命令
当收到客户端的EXEC命令时,才会将队列里的命令取出、顺序执行,执行完将当前状态从事务状态改为非事务状态
如果收到DISCARD命令,放弃执行队列中的命令,可以理解为Mysql的回滚操作,并且将当前的状态从事务状态改为非事务状态
WATCH 监视某个key,该命令只能在MULTI命令之前执行。如果监视的key被其他客户端修改,EXEC将会放弃执行队列中的所有命令。 UNWATCH 取消监视之前通过WATCH 命令监视的key。通过执行EXEC、DISCARD 两个命令之前监视的key也会被取消监视。
Redis与Guava、Caffeine 有什么区别?
缓存分为本地缓存和分布式缓存。
- Caffeine、Guava,属于本地缓存,特点:
- 直接访问内存,速度快,受内存限制,无法进行大数据存储。
- 无网络通讯开销,性能更高。
- 只支持本地应用进程访问,同步更新所有节点的本地缓存数据成本较高。
- 应用进程重启,数据会丢失。
- 所以,本地缓存适合存储一些不易改变或者低频改变的高热点数据。
- Redis属于分布式缓存,特点:
- 集群模式,支持大数据量存储
- 数据集中存储,保证数据的一致性
- 数据跨网络传输,性能低于本地缓存。但同一个机房,两台服务器之间请求跑一个来回也就需要500微秒,比
- 起其优势,这点损耗完全可以忽略,这也是分布式缓存受欢迎的原因。
- 支持副本机制,有效的保证了高可用性。
如何实现一个分布式锁?
- 数据库表,性能比较差
- 使用Lua脚本 (包含 SETNX + EXPIRE 两条指令)
- SET的扩展命令(SET key value [EX][PX] [NX|XX])
- Redlock 框架
- Zookeeper Curator 框架提供了现成的分布式