Redis面试直通车
关注公众号获取更多资料
Redis持久化机制
Redis是一个支持持久化的内存数据库,通过持久化机制,把内存中的数据同步到硬盘文件来保证数据库数据持久化,当Redis重启后通过硬盘文件重新加载到内存,就能达到恢复数据的目的。
实现:单独创建fork()的一个子进程,当前父进程的数据库数据复制到子进程的内存中,然后由子进程写入到临时文件,持久化的过程结束了,再用这个临时文件替换上次的快照文件,然后子进程退出,内存释放。
RDB:Redis默认的持久化方式,按照一定的事件周期策略吧内存的数据已快照的形式保存到硬盘的二进制文件,对应的产生的数据文件为dump.rdb,通过配置文件中的save参数来定义快照的周期。快照可以是数据的一个副本,也可以是数据的一个复制品。
AOF:Redis会将每一个收到的写命令通过write函数追加到文件最后,类似于MySQL的binlog,当Redis重启时会通过重新执行文件中保存的写命令来咋内存中重建整个数据库的内容。
当RDB和AOF同时开启时,Redis恢复数据会优先选择AOF。
缓存雪崩,缓存穿透,缓存预热,缓存更新,缓存降级
缓存雪崩
可以简单的理解为由于原有的缓存失效,新的缓存没有到内存的期间。
比如,Redis都设置了相同的过期时间,在同一时间,所有的缓存全部失效,导致原来应该访问缓存的数据全部请求数据库,导致数据库的CPU和内存压力在一瞬间暴增,从而造成宕机等一系列连锁反应,造成整个系统的崩溃。
解决方法:
- 考虑加锁或者队列的方式保证在同一时刻不会有大量的线程对数据库访问
- 将缓存失效的事件分散开
缓存穿透
用户在查询数据时,因为在数据库没有,所以缓存中也不会有,从而每次都要去数据库在查询一次,相当于进行了两次无用查询,这种绕过缓存直接查询数据库的情况称为缓存穿透。也是缓存命中率的问题。
解决办法:
- 将查询返回的数据为空也进行缓存,这个方法简单粗暴,但是需要将缓存的更新时间设置的很短,最长不能超过五分钟,这样在下次有数据更新的时候才能及时从数据库中获取到。
- 布隆过滤器(推荐)
布隆过滤器类似于一个hash set,用来判断某个元素(key)是否在某个集合中。和一般的hash set不同的是,这个算法无需存储key的值,对于每个key,只需要k个比特位,每个存储一个标志,用来判断key是否在集合中。
关注公众号回复“布隆过滤器”,获取相关介绍
缓存预热
在系统上线后,将相关的缓存数据直接加载到缓存系统中,边在用户请求时先查询数据库,然后再将数据缓存的问题。
预热方式:
- 直接写缓存刷新页面,上线后手动操作
- 如果数据量不大,可以在系统启动后自行加载
- 定时刷新缓存
缓存更新
Redis自带六中策略可以选择:
- volatile-lru:从已设置过期时间的数据中挑选最近最少使用的数据淘汰
- volatile-ttl:从已设置过期时间的数据中挑选将要过期的数据淘汰
- volatile-random:从已设置过期时间的数据中任意选择数据淘汰
- allkeys-lru:从数据集中挑选最近最少使用的数据淘汰
- allkeys-random:从数据集中任意选择数据淘汰
- no-enviction(驱逐):禁止驱逐数据
除此之外,我们还可以根据实际的业务需要进行自定义的更新策略,常见的有如下两种:
- 定时清理过期的缓存。
- 当有请求过来时,先判断当前缓存是否过期,如果过期再去数据库查询
以上两种自定义的模式各有优劣,第一种的缺点是维护大量的缓存key的时候比较麻烦,第二种的缺点是每次用户请求过来时都需要先判断缓存是否失效,逻辑比较复杂。
缓存降级
当访问量剧增,服务出现问题(如响应时间慢或不响应)或非核心服务影响到核心流程的性能时,仍然需要保证服务还是可用的,即使是有损服务。系统可以根据一些关键数据进行自动降级,也可以配置开关实现人工降级。降级的最终目的是保证核心服务可用,即使是有损的。而且有些服务是无法降级的(如加入购物车、结算)。
以参考日志级别设置预案:
-
一般:比如有些服务偶尔因为网络抖动或者服务正在上线而超时,可以自动降级;
-
警告:有些服务在一段时间内成功率有波动(如在95~100%之间),可以自动降级或人工降级, 并发送告警;
-
错误:比如可用率低于90%,或者数据库连接池被打爆了,或者访问量突然猛增到系统能承受的 最大阀值,此时可以根据情况自动降级或者人工降级
-
严重错误:比如因为特殊原因数据错误了,此时需要紧急人工降级
服务降级的目的,是为了防止Redis服务故障,导致数据库跟着一起发生雪崩问题。因此,对于不重要 的缓存数据,可以采取服务降级策略,例如一个比较常见的做法就是,Redis出现问题,不去数据库查 询,而是直接返回默认值给用户。
热点数据和冷数据
热点数据是指被频繁访问的数据。热点数据缓存才有价值。
对于冷数据而言,大部分数据可能还没有再次访问到就已经被挤出内存或者过期,这种情况下的数据不仅占用内存而且价值不大。对于经常修改的数据,看情况是否使用缓存。
对于冷数据,数据更新前至少被读取两次,缓存才有意义。这也是缓存的基本策略。
那么存不存在修改频率很高,但是又不得不考虑缓存的数据呢?是有的。比如读取数据的时候对数据库压力很大,但是又是热点数据,这个时候需要考虑缓存,减少数据库的压力,但是可以设置数据过期时间或者执行数据定期更新机制。比如某阅读软件的点赞数,收藏数,转发数等等,这些既是热点数据,又是频繁更新的数据,可以考虑缓存。
Memcache和Redis的区别
- 储存方式:Memcache把数据全部存放在内存中,断电或者重启服务器后数据会丢失,而且数据不能超过内存大小。Redis有部分数据可以存放到硬盘上持久化数据。
- 数据类型:Memcache只支持字符串的数据类型,Redis支持list,set,zset,hash,bitmap等不同的数据结构。
- 底层模型:底层实现方式以及与客户端之间的通信应用协议不同。Redis直接构件了自己的VM机制,避免调用系统函数,浪费时间移动数据和请求。
- value值大小不同:Memcache只有1MB,而Redis最大可以达到1GB。
- Redis的速度比Memcache快很多
- Redis支持数据备份,即master-slave模式的数据备份
Redis为什么快
- 纯内存操作
- 单线程操作,避免了频繁的上下文切换
- 采用非阻塞I/O多路复用机制
Redis数据类型
- String
常规get/set操作,value可以是String也可以是数字。
- hash
value可以存放结构化的对象比较方便操作其中的某个字段。
- list
使用list数据结构可以做简单的消息队列功能,还可以使用lrange命令,做基于Redis的分页功能,性能很好。
- set
存放不重复的数据集合,可以用来做全局去重的功能。
注意:为什么不使用Java自带的去重,因为一般情况下都是集群部署,不能很好的进行全局去重。
- zset
与set类似,也是去重的,但是不同的是zset是一个有序的集合,即去重又有序。
Redis的过期策略和内存淘汰机制
Redis采用的是定期删除+惰性删除策略。
为什么不使用定时删除策略呢?定时删除需要一个定时器来负责监视key,过期后自动删除,虽然内存即使释放了,但是消耗CPU资源,在并发量较大的情况下,CPU需要处理请求,而不是去消耗资源删除key。
定期删除+惰性删除
定期删除默认每100ms进行一次检查,检查是否有key过期,如果过期则删除,但是并不是每100ms将所有的key都检查一遍,而是随机抽取,这样就会导致很多key到期也没有删除。
惰性删除是指在获取某个key的时候,Redis会检查一下,如果这个key设置了过期时间,如果设置了并且过期则会删除。
即使使用了定期删除+惰性删除的机制,如果某一个key长时间没有访问并且定期删除没有扫描到,那么时间久了Redis内存就会越来越高。那么此时就应该采用内存淘汰机制。
在redis.conf中有一行配置:
maxmemory-policy volatile-lru
- volatile-lru:从已设置过期时间的数据集中挑选最近最少使用的数据淘汰
- volatile-ttl:从已设置过期时间的数据集中挑选将要过期的数据淘汰
- volatile-random:从设置过期时间的数据集中任意选择数据淘汰
- allkeys-lru:从数据集中挑选最近最少使用的数据淘汰
- allkeys-random:从数据集中选择任意数据淘汰
- no-enviction:禁止驱逐数据,新写入操作会报错
Redis为什么是单线程的
官方表示,Redis是基于内存操作的,CPU不会是Redis性能的瓶颈,Redis性能瓶颈可能是及其内存大小或者网络带宽。而且既然CPU不会成为瓶颈,而且单线程容易实现,就顺理成章的使用单线程了。Redis利用队列技术将并发访问转变为串行访问。
-
使用内存操作,速度快,没必要多线程
-
避免上下文切换和竞争时间片
-
采用非阻塞I/O
采用非阻塞I/O优点:
- 速度快
- 支持丰富的数据类型
- 支持事务
当多个系统同时去set一个key的时候,可以使用一个分布式锁或者使用队列转为串行执行,保证读写的一致性。
提升Redis性能
- Master节点不做任何持久化工作
- 比较重要的数据可以在Slave中开启AOF数据备份,策略设置为每秒一次
- Master和Slave最好在同一个局域网内
- 避免在压力很大的主库上增加从库
- 主从复制不要使用图状结构,使用单向链表更为稳定
为何Redis操作是原子性的,怎么保证原子性的
Redis之所以操作是原子性的,是因为Redis是单线程的。
Redis本身提供的所有API都是源自操作,Redis中的事务就是保证批量操作的原子性的。
Redis事务
Redis事务功能是通过MULTI、EXEC、DISCARD和WATCH 四个原语实现的。
Redis会将一个事务中的所有命令序列化,然后按顺序执行。
- Redis 不支持回滚“Redis 在事务失败时不进行回滚,而是继续执行余下的命令”, 所以 Redis 的> 内部可 以保持简单且快速。
- 如果在一个事务中的命令出现错误,那么所有的命令都不会执行。
- 如果在一个事务中出现运行错误,那么正确的命令会被执行。
- MULTI命令用于开启一个事务,它总是返回OK。 MULTI执行之后,客户端可以继续向服务器发送任 意多条命令,这些命令不会立即被执行,而是被放到一个队列中,当EXEC命令被调用时,所有队列中 的命令才会被执行。
- EXEC:执行所有事务块内的命令。返回事务块内所有命令的返回值,按命令执行的先后顺序排列。 当操作被打断时,返回空值 nil 。
- 通过调用DISCARD,客户端可以清空事务队列,并放弃执行事务, 并且客户端会从事务状态中退出。
- WATCH 命令可以为 Redis 事务提供 check-and-set (CAS)行为。 可以监控一个或多个键,一旦其 中有一个键被修改(或删除),之后的事务就不会执行,监控一直持续到EXEC命令。
哨兵机制
在Redis的master/slave模式中,slave在整个体系中起到了数据冗余备份和读写分离的作用。当master遇到异常终止后,需要从slave中选举一个新的master继续对外提供服务,所以Redis需要一种机制去对slave进行选择,在Redis中使用的是哨兵(sentinel)机制。
哨兵的作用就是监视Redis集群的运行状况,他的功能如下
- 监控master和slave是否正常运行
- master出现故障时自动将slave选举为master
哨兵是一个独立的进程,他的架构图如下:
这种哨兵机制解决了master的选举问题,但是又引出了另一个问题,哨兵的高可用性,如果当前哨兵出现异常,那么整个选举过程就会失效。
在一个一主多从的Redis体系中可以使用多个哨兵,这些哨兵不仅仅会监控master和slave,还会相互监控,这种方式称为哨兵集群,哨兵集群需要解决故障发现和master决策协商的问题,毕竟如果每个哨兵选举的master不同时到底该听谁的呢。
哨兵节点之间的互相感知
哨兵节点之间会因为共同监视一个master而产生关联,一个新加入的哨兵节点需要和其他监视相同master的哨兵节点互相感知。需要做到如下几点:
- 需要互相感知的哨兵都向他们共同监视的master节点订阅channel:sentinel:hello
- 新加入的哨兵节点想这个channel发布一条消息,包含自身的信息,这样订阅了这个channel的其他哨兵们就会发现这个新家入的哨兵节点
- 新加入的哨兵和其他哨兵建立长连接
简单的说分三步:订阅-广播-建立连接。
master的故障发现
哨兵节点会定期向master节点发送心跳包来判断他的存活状态,如果maser节点没有正确响应,哨兵节点就会把这个master设置为主观不可用状态,然后他会把主观不可用状态发送给其他所有哨兵借点去确认,当确认的哨兵节点数大于设置的quorum时,则会认为master是客观不可用,接着就可以开始进入选举新的master的流程。
这里会遇到一个问题,哨兵本身就是一个集群,如果多个哨兵节点同时发现master节点达到客观不可用状态,那么由谁来决策那个slave节点被选举为master呢?这时候需要在哨兵集群中选择一个leader来做决策。这里用到了Raft算法,和Paxos算法类似,都是分布式一致性算法,但是它比Paxos算法更容易理解,他们都是基于投票算法,只要保证半数节点通过提议即可。
注意:哨兵节点需要访问所有的主从Redis节点,所以确保所有的redis.config中bind变成0.0.0.0,即允许其他机器访问
关注公众号回复“Raft算法”、“Paxos算法”、“Redis配置”获取相关资料
点击文末阅读原文查看相关动画演示http://thesecretlivesofdata.com/raft/
配置实现哨兵
在其中任意一台服务器上创建一个sentinel.conf文件,文件内容为:
sentinel monitor name ip port quorum
-- 5000ms内master没有相应就认为是sdown主观不可用
sentinel down-after-milliseconds name 5000
-- 15000ms后master还没有活过来,启动failover,从生下的slave中选择一个升级为master
sentinel failover-timeout mymaster 15000
- name代表要监控的master名字,可以自定义
- ip代表master的IP地址
- port代表master的端口号
- quorum代表最低通过票数,需要超过这个数才能被选举为master
比如:
sentinel monitor mymaster 192.168.25.129 6379 2
启动方式
./bin/redis-sentinel sentinel.conf
# 或者
redis-server /path/to/sentinel.conf --sentinel
哨兵在监控系统时,只需要配置监控master即可,哨兵会自动发现所有的slave。在重新选举master节点后sentinel.conf配置文件会发生相应的变化。
Redis集群/数据分片
使用了哨兵模式,保证了Redis的master节点高可用,但是在master-slave结构种每台机器依然存储了所有的数据,导致总缓存数据量受限于储存内存最小的节点,形成了木桶效应,因为Redis是基于内存储存的,所以这个问题尤为突出。
Redis3.0之后的版本支持集群Redis-cluster,集群的特点在于拥有和单一实例一样的性能,同事在网络分区以后能够提供一定的可访问型和对主数据故障恢复的支持。
哨兵和集群是两个独立的功能,当不需要对数据进行集群分片使用哨兵就够了, 如果需要进行水平扩容,集群是一个比较好的方式。
拓扑结构
一个Redis集群(Redis-cluster)是由多个Redis节点构成,不同的节点组(包含master-slave结构)的数据之间没有交集,即将数据分片储存,每个接电阻对应数据的一个分片。
在一个节点组内部,对应的master-slave两种节点,这两种同组的节点数据实时同步保持一致,通过异步话的主备复制机制来保证。
在一个节点组中有且只有一个master,可以拥有0个或多个slave节点,在这个节点组中只有master对用户提供写服务,读服务可以由master和slave共同提供。
Redis-cluster是基于gossip协议实现的无中心化节点的集群,因为去中心化的架构不存在统一配置中心,各个节点对于整个集群的状态认知来自于节点之间的信息交互,在Redis-cluster中,这个信息交互式通过Redis Cluster Bus来完成的。
关注公众号回复“Gossip协议”,“Redis集群搭建”获取相关推荐文章
Redis Cluster各节点之间通过Redis Cluster Bus交互信息。Redis Cluster每个节点都会记录集群的配置信息:如信息版本号Epoch、集群状态State等数据。
Redis Cluster的每个节点都保存着Node视角的集群结构。它描述了数据的分片方式,节点主备关系,并通过Epoch作为版本号实现集群结构信息的一致性,同时控制着数据迁移和故障转移的过程。
数据分区
分布式数据库首要解决把整个数据集按照分区规则映射到多个节点的问题,即把数据集划分到多个节点上,每个节点负责整个数据的一个子集, Redis Cluster采用哈希分区规则,采用虚拟槽分区。
虚拟槽分区巧妙地使用了哈希空间,使用分散度良好的哈希函数把所有的数据映射到一个固定范围内的整数集合,整数定义为槽(slot)。比如Redis Cluster槽的范围是0 ~ 16383。
槽是集群内数据管理和迁移的基本单位。采用大范围的槽的主要目的是为了方便数据的拆分和集群的扩展,每个节点负责一定数量的槽。
计算公式:slot = CRC16(key)%16383。每一个节点负责维护一部分槽以及槽所映射的键值数据。
HashTag
通过分片手段,可以将数据合理的划分到不同的节点上,这本来是一件好事。但是有的时候,我们希望对相关联的业务以原子方式进行操作。举个简单的例子
我们在单节点上执行MSET , 它是一个原子性的操作,所有给定的key会在同一时间内被设置,不可能出现某些指定的key被更新另一些指定的key没有改变的情况。
但是在集群环境下,我们仍然可以执行MSET命令,但它的操作不在是原子操作,会存在某些指定的key被更新,而另外一些指定的key没有改变,原因是多个key可能会被分配到不同的机器上。
所以,这里就会存在一个矛盾点,及要求key尽可能的分散在不同机器,又要求某些相关联的key分配到相同机器。
从前面的分析中我们了解到,分片其实就是一个hash的过程,对key做hash取模然后划分到不同的机器上。所以为了解决这个问题,我们需要考虑如何让相关联的key得到的hash值都相同呢?
如果key全部相同是不现实的,所以怎么解决呢?
在redis中引入了HashTag的概念,可以使得数据分布算法可以根据key的某一个部分进行计算,然后让相关的key落到同一个数据分片
举个简单的例子,加入对于用户的信息进行存储, user:user1:id、user:user1:name/ 那么通过hashtag的方式,user:{user1}:id、user:{user1}.name; 表示当一个key包含 {} 的时候,就不对整个key做hash,而仅对 {} 包括的字符串做hash。
重定向客户端
Redis Cluster并不会代理查询,那么如果客户端访问了一个key并不存在的节点,这个节点是怎么处理的呢?比如我想获取key为msg的值,msg计算出来的槽编号为254,
当前节点正好不负责编号为254的槽,那么就会返回客户端下面信息:
-MOVED 254 127.0.0.1:6381
表示客户端想要的254槽由运行在IP为127.0.0.1,端口为6381的Master实例服务。如果根据key计算得出的槽恰好由当前节点负责,则当期节点会立即返回结果。
分片迁移
在一个稳定的Redis cluster下,每一个slot对应的节点是确定的,但是在某些情况下,节点和分片对应的关系会发生变更
- 新加入master节点
- 某个节点宕机
也就是说当动态添加或减少node节点时,需要将16384个槽做个再分配,槽中的键值也要迁移。这一过程,在目前实现中,还处于半自动状态,需要人工介入。
新增一个主节点
新增一个节点D,redis cluster的这种做法是从各个节点的前面各拿取一部分slot到D上。 大致就会变成这样:
- 节点A覆盖1365-5460
- 节点B覆盖6827-10922
- 节点C覆盖12288-16383
- 节点D覆盖0-1364,5461-6826,10923-12287
删除一个主节点
先将节点的数据移动到其他节点上,然后才能执行删除。
槽迁移的过程
槽迁移的过程中有一个不稳定状态,这个不稳定状态会有一些规则,这些规则定义客户端的行为,从而使得RedisCluster不必宕机的情况下可以执行槽的迁移。
下面这张图描述了我们迁移编号为1、2、3的槽的过程中,他们在MasterA节点和MasterB节点中的状态。