目录
1 Redis的持久化
由于 redis 是一个内存数据库,所有的数据都是保存在内存当中的,内存当中的数据极易丢失,所以 redis 的数据持久化就显得尤为重要,在redis当中,提供了两种数据持久化的方式,分别为RDB以及AOF,且Redis默认开启的数据持久化方式为RDB方式。
1.1 RDB持久化方案
RDB方案是Redis默认的持久化方案。
- 按照一定的时间内,如果Redis内存中的数据产生了一定次数的更新,就将整个Redis内存中的所有数据拍摄一个全量快照文件存储在硬盘上。
- 新的快照会覆盖老的快照文件,快照是全量快照,包含了内存中所有的内容,基本与内存一致。
- 如果Redis故障重启,从硬盘的快照文件进行恢复。
RDB快照存储于Redis目录下datas文件夹下
dump.rdb
触发条件
- 手动触发:当执行某些命令时,会自动拍摄快照【一般不用】
save
:手动触发拍摄RDB快照的,将内存的所有数据拍摄最新的快照- 前端运行
- 阻塞所有的客户端请求,等待快照拍摄完成后,再继续处理客户端请求
- 特点:快照与内存是一致的,数据不会丢失,用户的请求会被阻塞
bgsave
:手动触发拍摄RDB快照的,将内存的所有数据拍摄最新的快照- 后台运行
- 主进程会fork一个子进程负责拍摄快照,客户端可以正常请求,不会被阻塞
- 特点:用户请求继续执行,用户的新增的更新数据不在快照中
shutdown
:执行关闭服务端命令flushall
:清空,没有意义
- 自动触发:按照一定的时间内发生的更新的次数,拍摄快照
- 配置文件中有对应的配置,决定什么时候做快照
#Redis可以设置多组rdb条件,默认设置了三组,这三组共同交叉作用,满足任何一个都会拍摄快照
save 900 1
save 300 10
save 60 10000
为什么默认设置3组?如果只有一组策略,面向不同的写的场景,会导致数据丢失。针对不同读写速度,设置不同策略,进行交叉保存快照,满足各种情况下数据的保存策略。
总结:
- 优点
- rdb方式实现的是全量快照,快照文件中的数据与内存中的数据是一致的
- 快照是二进制文件,生成快照加载快照都比较快,体积更小
- Fork进程实现,性能更好
- 总结:更快、更小、性能更好
- 缺点
- 存在一定概率导致部分数据丢失
- 应用:希望有一个高性能的读写,不影响业务,允许一部分的数据存在一定概率的丢失【做缓存】,大规模的数据备份和恢复。
1.2 AOF持久化方案
RDB存在一定概率的数据丢失,如何解决?
AOF方案
- 按照一定的规则,将内存数据的操作日志追加写入一个文件中;
- 当Redis发生故障,重启,从文件中进行读取所有的操作日志,恢复内存中的数据;
- 重新对Redis进行执行,用于恢复内存中的数据。
追加的规则
- appendfsync always
- 每更新一条数据就同步将这个更新操作追加到文件中
- 优点:数据会相对安全,几乎不会出现数据丢失的情况
- 缺点:频繁的进行数据的追加,增大磁盘的IO,导致性能较差
- appendfsync everysec(常用)
- 每秒将一秒内Redis内存中数据的操作异步追加写入文件
- 优点:在安全性和性能之间做了权衡,性能要比always高
- 缺点:有数据丢失风险 ,但最多丢失1秒
- appendfsync no
- 交给操作系统来做,不由Redis控制
- 肯定不用的
总结
- 优点:安全性和性能做了折中方案,提供了灵活的机制,如果性能要求不高,安全性可以达到最高
- 缺点
- 这个文件是普通文本文件,相比于二进制文件来说,每次追加和加载比较慢
- 数据的变化以追加的方式写入AOF文件
- 问题:文件会不断变大,文件中会包含不必要的操作【过期的数据】
- 解决:模拟类似于RDB做全量的方式,定期生成一次全量的AOF文件
- 应用:数据持久化安全方案,理论上绝对性保证数据的安全
持久化方案:两种方案怎么选?
- 两种方案都可以用:默认不配置AOF,使用的RDB
- 问题:两种都用,重启Redis加载的是谁的数据?
- 加载AOF
2 Redis架构
- Redis的服务只有单台机器
- 问题1:单点故障问题,如果Redis服务故障,整个Redis服务将不可用
- 单点故障问题
- 问题2:单台机器的内存比较小,数据存储的容量不足,会导致redis无法满足需求
- 单机资源不足问题
- 解决方案:分布式
2.1 主从复制架构
分布式主从架构
- Master:主节点
- 负责对外提供数据的读写
- Slave:从节点
- 负责对外提供读的请求
- 负责与主节点同步数据
特点:主从节点上的数据都是一致的,连接任何一个节点实现读,默认写操作只能连接主节点
优缺点:
- 优点:实现了读写分离,分摊了读写的压力负载,如果一台Redis的Slave故障,其他的Redis服务节点照常对外提供服务
- 解决了Redis单点故障问题
- 缺点:如果Master故障,整个集群不能对外提供写的操作,Master没有HA机制
- 带来了Master单点故障问题
2.2 哨兵集群设计
主从复制集群的Master存在单点故障问题,怎么解决?
- 类似于ZK的设计
- 每台节点存储的数据都是一样的
- 如果Leader故障,允许Follower选举成为Leader
哨兵设计
思想:基于主从复制模式之上封装了哨兵模式,如果Master出现故障,让Slave选举成为新的Master
实现:哨兵进程 实现
- 必须能发现Master的故障
- 必须负责重新选举新的Master
Redis主从架构
- 哨兵进程
- 每个哨兵负责监听所有Redis节点和其他哨兵
- 为什么要监听所有Redis的节点:发现所有节点是否会出现故障
- 如果Master出现故障,会进行投票选择一个Slave来成为新的Master
- 为什么要监听别的哨兵:如果哨兵故障,不能让这个哨兵参与投票选举等
哨兵功能
- 集群监控:监控节点状态
- 消息通知:汇报节点状态
- 故障转移:实现Master重新选举
- 配置中心:实现配置同步
流程
- step1:如果Master突然故障,有一个哨兵会发现这个问题,这个哨兵会立即通告给所有哨兵
- 主观性故障【sdown】
- step2:当有一定的个数的哨兵都通告Master故障了,整体认为Master故障了
- 客观性故障【odown】
- step3:所有哨兵根据每台Slave通信的健康状况以及Slave权重选举一个新的Master
- step4:将其他所有的Slave的配置文件中的Master切换为当前最新的Master
2.3 哨兵集群设计
Redis哨兵集群中的存储容量只有单台机器,如何解决大量数据使用Redis存储问题?
分片集群模式:解决了单点故障和单机资源不足的问题。
- 将多个Redis小集群从逻辑上合并为一个大集群,每个小集群分摊一部分槽位,对每一条Redis的数据进行槽位计算,这条数据属于哪个槽位,就存储对应槽位的小集群中
分片的规则:根据Key进行槽位运算:CRC16【K】 & 16383 = 0 ~ 16383
3 Redis事务机制
事务定义:事务是数据库操作的最小工作单元,包含原子性、一致性、隔离性、持久性
Redis事务:Redis一般不用事务
Redis本身是单线程的,所以本身没有事务等概念
Redis 支持事务的本质是一组命令的集合,事务支持一次执行多个命令,串行执行每个命令。将一组需要一起执行的命令放在
multi
和exec之间
。一旦Redis开启了事务,将所有命令放入一个队列中,提交事务时,对整个队列中的命令进行执行
redis事务就是一次性、顺序性、排他性的执行一个队列中的一系列命令
没有隔离性:批量的事务命令执行前在缓存队列中,没有事务交叉,不存在脏读幻读等问题
不能保证原子性:单条命令是原子性执行的,但事务不保证原子性,且没有回滚机制,事务中任意命令执行失败,其余的命令仍会被执行。
过程
- 开启事务
- 提交命令
- 执行事务
命令
multi
:开启事务exec
:执行事务- discard:取消事务
测试
正常使用
set user1 hadoop1 set user2 hadoop2 get user1 get user2 multi set user1 hadoop3 set user2 hadoop4 exec get user1 get user2
都执行
语法【编译】错误
flushdb set user1 hadoop1 set user2 hadoop2 get user1 get user2 multi set user1 hadoop3 sets user2 hadoop4 exec get user1 get user2
都不执行
类型【运行】错误
flushdb set user1 hadoop1 set user2 hadoop2 get user1 get user2 multi set user1 hadoop3 lpush user2 hadoop4 # 运行错误 exec get user1 get user2
user1执行,user2不执行
取消事务
flushdb set user1 hadoop1 set user2 hadoop2 get user1 get user2 multi set user1 hadoop3 set user2 hadoop4 discard get user1 get user2
都不执行
总结:
- Redis 的事务机制很弱,在事务回滚机制上,只能对基本的语法错误进行判断。
- 事务是Redis实现在服务端的行为,用户执行multi命令时,服务器会将对应这个用户的客户端对象设置为一个特殊的状态,在这个状态下后续用户执行的命令不会被真的执行,而是被服务器缓存起来,直到用户执行exec命令为止,服务器会将这个用户对应的客户端对象中缓存的命令按照提交的顺序依次执行。
4 Redis过期策略与内存淘汰机制
Redis使用的是内存,内存如果满了,怎么解决?
4.1 过期策略
设计思想:避免内存满,指定Key的存活时间,到达存活时间以后自动删除。命令:expire/setex
- 定时过期:指定Key的存活时间,一直监听这个存活时间,一旦达到存活时间,自动删除
- 需要CPU一直做监听,如果Key比较多,CPU的消耗比较严重
- 惰性过期:指定Key的存活时间,当使用这个Key的时候,判断是否过期,如果过期就删除
- 如果某个Key设置了过期时间,但是一直没有使用,不会被发现过期了,就会导致资源浪费
- 定期过期:每隔一段时间就检查数据是否过期,如果过期就进行删除
- 中和的策略机制
Redis中使用了惰性过期和定期过期两种策略共同作用
4.2 内存淘汰机制
设计思想:内存满了,怎么淘汰
Redis的内存淘汰策略是指在Redis的用于缓存的内存不足时,怎么处理需要新写入且需要申请额外空间的数据, Redis 源码中的默认配置
实际项目中设置内存淘汰策略:maxmemory-policy allkeys-lru,移除最近最少使用的key。
- 缓存使用:allkeys-lru
- 数据库使用:volatile-lru / noeviction
5 Redis高频面试题
在应用程序和MySQL数据库中建立一个中间层:Redis缓存,通过Redis缓存可以有效减少查询数据库的时间消耗,但是引入redis又有可能出现缓存穿透、缓存击穿、缓存雪崩等问题。
4.1 缓存穿透
缓存穿透:key对应的数据在数据源并不存在,每次针对此key的请求从缓存获取不到,请求都会到数据源,从而可能压垮数据源。
总的来说,查询Key,缓存和数据源都没有,频繁查询数据源。比如用一个不存在的用户 id 获取用户信息,无论论缓存还是数据库都没有,若黑客利用此漏洞进行攻击可能压垮数据库。
解决缓存穿透的方案主要有两种:
- 当查询不存在时,也将结果保存在缓存中。但是这可能会存在一种问题:大量没有查询结果的请求保存在缓存中,这时我们就可以将这些请求的key设置得更短一些;
- 提前过滤掉不合法的请求,可以使用Redis中布隆过滤器;
布隆过滤器可能存在误判,意思就是某个数据可能不存在,但是可能误判为存在。
4.2 缓存击穿
缓存击穿:key对应的数据库存在,但在redis中过期,此时若有大量并发请求过来,这些请求发现缓存过期一般都会从后端DB加载数据并回设到缓存,这个时候大并发的请求可能会瞬间把后端DB压垮。
总的来说,查询Key,缓存过期,大量并发,频繁查询数据源。
业界比较常用的做法:使用互斥锁。简单地来说,就是在缓存失效的时候(判断拿出来的值为空),不是立即去load db(查询数据库),而是先使用缓存工具的某些带成功操作返回值的操作(比如Redis的SETNX或者Memcache的ADD)去set一个mutex key,就是只让一个线程构建缓存,其他线程等待构建缓存的线程执行完,重新从缓存获取数据。
在请求数据库这一步进行上锁,这个时候只有一个线程可以抢到这个锁,这个线程查询到数据库再重新将数据写入缓存,其他线程再去缓存中查询。
4.3 缓存雪崩
缓存雪崩:当缓存服务器重启或者大量缓存集中在某一个时间段失效,这样在失效的时候,也会给后端系统(比如DB)带来很大压力。
总的来说,缓存不可用(服务器重启或缓存失效),频繁查询数据源。
与缓存击穿的区别在于这里针对很多key缓存,前者则是某一个key。缓存正常从Redis中获取,示意图如下:
缓存失效瞬间示意图如下:
缓存失效时的雪崩效应对底层系统的冲击非常可怕!大多数系统设计者考虑用加锁或者队列的方式保证来保证不会有大量的线程对数据库一次性进行读写,从而避免失效时大量的并发请求落到底层存储系统上。还有一个简单方案就时将缓存失效时间分散开,比如可以在原有的失效时间基础上增加一个随机值,比如1-5分钟随机,这样每一个缓存的过期时间的重复率就会降低,就很难引发集体失效的事件。