一、RDB 和 AOF 机制
RDB:Redis DataBase
将某一个时刻的内存快照(
Snapshot
),以二进制的方式写入磁盘。
实际操作过程是fork一个子进程,先将数据集写入临时文件,写入成功后,再替换之前的文件,用二进制压缩存储。
手动触发
- save命令,使 Redis 处于阻塞状态,直到 RDB 持久化完成,才会响应其他客户端发来的命令,所以在生产环境一定慎用。
- bgsave命令,fork出一个子进程执行持久化,主进程只在fork过程中有短暂的阻塞,子进程创建之后,主进程就可以响应客户端请求了。
自动触发
- save m n配置 :在 m 秒内,如果有 n 个键发生改变,则自动触发持久化,通过bgsave执行,如果设置多个、只要满足其一就会触发,配置文件有默认配置(可以注释掉)
- flushall:用于清空redis所有的数据库,flushdb清空当前redis所在库数据(默认是0号数据库),会清空RDB文件,同时也会生成dump.rdb、内容为空。
- 主从同步:全量同步时会自动触发bgsave命令,生成rdb发送给从节点。
优点:
1
、整个
Redis
数据库将只包含一个文件
dump.rdb
,方便持久化。
2
、容灾性好,方便备份。
3
、性能最大化,
fork
子进程来完成写操作,让主进程继续处理命令,所以是
IO 最大化。使用单独子进程来进行持久化,主进程不会进行任何
IO
操作,保证了
redis
的高性能。
4.
相对于数据集大时,比
AOF
的启动效率更高。
缺点:
1
、数据安全性低。
RDB
是间隔一段时间进行持久化,如果持久化之间
redis 发生故障,会发生数据丢失。所以这种方式更适合数据要求不严谨的时候
)
2
、由于
RDB
是通过
fork子进程来协助完成数据持久化工作的,因此,如果当数据集较大时,可能会导致整个服务器停止服务几百毫秒,甚至是
1
秒钟。
AOF:Append Only File
以日志的形式记录服务器所处理的每一个写、删除操作,查询操作不会记录,以文本的方式记录,可以打开文件看到详细的操作记录,调操作系统命令进程刷盘。
默认关闭,在配置文件中开启。
- 所有的写命令会追加到 AOF 缓冲中。
- AOF 缓冲区根据对应的策略向硬盘进行同步操作。
- 随着 AOF 文件越来越大,需要定期对 AOF 文件进行重写,达到压缩的目的。
- 当 Redis 重启时,可以加载 AOF 文件进行数据恢复。
同步策略
每秒同步:异步完成,效率非常高,一旦系统出现宕机现象,那么这一秒钟之内修改的数据将会丢失。
每修改同步:同步持久化,每次发生的数据变化都会被立即记录到磁盘中,最多丢一条。
不同步:由操作系统控制,可能丢失较多数据。
优点:
1、数据安全,Redis中提供了3中同步策略,即每秒同步、每修改同步和不同步。事实上,每秒同步也是异步完成的,其效率也是非常高的,所差的是一旦系统出现宕机现象,那么这一秒钟之内修改的数据 将会丢失。而每修改同步,我们可以将其视为同步持久化,即每次发生的数据变化都会被立即记录到磁盘中。
2
、通过
append
模式写文件,即使中途服务器宕机也不会破坏已经存在的内容,可以通过 redis-check-aof
工具解决数据一致性问题。
3
、
AOF
机制的
rewrite
模式。定期对
AOF
文件进行重写,以达到压缩的目的。
缺点:
1
、
AOF
文件比
RDB
文件大,且恢复速度慢。
2
、数据集大的时候,比
rdb
启动效率低。
3
、运行效率没有
RDB
高。
AOF 文件比 RDB 更新频率高,优先使用 AOF 还原数据。AOF 比 RDB 更安全也更大。RDB 性能比 AOF 好。如果两个都配了优先加载 AOF。
二、Redis的过期键的删除策略
Redis
是
key-value
数据库,我们可以设置
Redis
中缓存的
key
的过期时间。
Redis的过期策略就是指当Redis
中缓存的
key
过期了,
Redis
如何处理。
- 惰性过期:只有当访问一个key时,才会判断该key是否已过期,过期则清除。该策略可以最大化地节省CPU资源,却对内存非常不友好。极端情况可能出现大量的过期key没有再次被访问,从而不会被清除,占用大量内存。
- 定期过期:每隔一定的时间,会扫描一定数量的数据库的expires字典中一定数量的key,并清除其中已过期的key。该策略是一个折中方案。通过调整定时扫描的时间间隔和每次扫描的限定耗时,可以在不同情况下使得CPU和内存资源达到最优的平衡效果。
expires
字典会保存所有设置了过期时间的
key
的过期时间数据,其中,
key是指向键空间中的某个键的 指针,
value
是该键的毫秒精度的
UNIX
时间戳表示的过期时间。键空间是指该
Redis集群中保存的所有键。
Redis中同时使用了惰性过期和定期过期两种过期策略。
定期扫描所有的数据库(16个),随机检查其中的key是否过期。
三、Redis线程模型、单线程快的原因
Redis
基于
Reactor
模式开发了网络事件处理器,这个处理器叫做文件事件处理器
file event handler。 这个文件事件处理器,它是单线程的,所以
Redis
才叫做单线程的模型,它采用
IO多路复用机制来同时监听多个
Socket
,根据
Socket上的事件类型来选择对应的事件处理器来处理这个事件。可以实现高性能的网络通信模型,又可以跟内部其他单线程的模块进行对接,保证了
Redis
内部的线程模型的简单性。
文件事件处理器的结构包含
4
个部分:多个
Socket
、
IO多路复用程序、文件事件分派器以及事件处理器 (命令请求处理器、命令回复处理器、连接应答处理器等)。
多个
Socket
可能并发的产生不同的操作,每个操作对应不同的文件事件,但是
IO多路复用程序会监听多个
Socket
,会将
Socket
放入一个队列中排队,每次从队列中取出一个
Socket 给事件分派器,事件分派器把
Socket 给对应的事件处理器。 然后一个
Socket
的事件处理完之后,
IO
多路复用程序才会将队列中的下一个
Socket 给事件分派器。文件事件分派器会根据每个
Socket
当前产生的事件,来选择对应的事件处理器来处理。
单线程快的原因:
1
)纯内存操作
2
)核心是基于非阻塞的
IO
多路复用机制
3
)单线程反而避免了多线程的频繁上下文切换带来的性能问题
四、简述Redis事务实现
1
、事务开始
MULTI
命令的执行,标识着一个事务的开始。
MULTI
命令会将客户端状态的
flags 属性中打开REDIS_MULTI
标识来完成的。
2
、命令入队
当一个客户端切换到事务状态之后,服务器会根据这个客户端发送来的命令来执行不同的操作。如果客户端发送的命令为
MULTI
、
EXEC
、
WATCH
、
DISCARD中的一个,立即执行这个命令,否则将命令放入一个事务队列里面,然后向客户端返回
QUEUED
回复。
- 如果客户端发送的命令为 EXEC、DISCARD、WATCH、MULTI 四个命令的其中一个,那么服务器立即执行这个命令。
- 如果客户端发送的是四个命令以外的其他命令,那么服务器并不立即执行这个命令。首先检查此命令的格式是否正确,如果不正确,服务器会在客户端状态(redisClient)的 flags 属性关闭 REDIS_MULTI 标识,并且返回错误信息给客户端。如果正确,将这个命令放入一个事务队列里面,然后向客户端返回 QUEUED 回复。
事务队列是按照
FIFO
的方式保存入队的命令。
3
、事务执行
- 客户端发送 EXEC 命令,服务器执行 EXEC 命令逻辑。
- 如果客户端状态的 flags 属性不包含 REDIS_MULTI 标识,或者包含 REDIS_DIRTY_CAS 或者 REDIS_DIRTY_EXEC 标识,那么就直接取消事务的执行。
- 否则客户端处于事务状态(flags 有 REDIS_MULTI 标识),服务器会遍历客户端的事务队列,然后执行事务队列中的所有命令,最后将返回结果全部返回给客户端;
redis
不支持事务回滚机制,但是它会检查每一个事务中的命令是否错误。
Redis
事务不支持检查那些程序员自己逻辑错误。仅支持语法上的错误。例如对
String
类型的数据库键执行对
HashMap 类型的操作!
- WATCH 命令是一个乐观锁,可以为 Redis 事务提供 check-and-set (CAS)行为。可以监控一个或多个键,一旦其中有一个键被修改(或删除),之后的事务就不会执行,监控一直持续到EXEC 命令。
- MULTI命令用于开启一个事务,它总是返回OK。MULTI执行之后,客户端可以继续向服务器发送任意多条命令,这些命令不会立即被执行,而是被放到一个队列中,当EXEC命令被调用时,所有队列中的命令才会被执行。
- EXEC:执行所有事务块内的命令。返回事务块内所有命令的返回值,按命令执行的先后顺序排列。当操作被打断时,返回空值 nil 。
- 通过调用DISCARD,客户端可以清空事务队列,并放弃执行事务, 并且客户端会从事务状态中退出。
- UNWATCH命令可以取消watch对所有key的监控。
五、redis 主从复制的核心原理
通过执行
slaveof命令或设置slaveof选项,让一个服务器去复制另一个服务器的数据。主数据库可以进行读写操作,当写操作导致数据变化时会自动将数据同步给从数据库。而从数据库一般是只读的,并接受主数据库同步过来的数据。
一个主数据库可以拥有多个从数据库,而一个从数据库只能拥有一个主数据库。
- 从节点执行slaveof masterIp port,保存主节点信息。
- 从节点中的定时任务发现主节点信息,建立和主节点的socket连接。
- 从节点发送信号,主节点返回,两边能互相通信。
- 连接建立后,主节点将所有数据发送给从节点(数据同步)
- 主节点把当前的数据同步给从节点后,便完成了复制过程。接下来,主节点就会持续的把写命令发送给从节点,保证主从数据一致性。
runId
:
每个
redis
节点启动都会生成唯一的
uuid
,每次
redis重启后,runId
都会发生变化。
offset
:
主从节点各自维护自己的复制偏移量
offset
,当主节点有写入命令时,
offset=offset+命令的字节长度。从节点在收到主节点发送的命令后,也会增加自己的
offset
,并把自己的
offset发送给主节点。主节点同时保存自己的
offset
和从节点的
offset
,
通过对比offset来判断主从节点数据是否一致
。
repl_backlog_size
:
保存在主节点上的一个固定长度的先进先出队列,默认大小是
1MB
。
全量复制:
- 从节点发送psync命令,psync runid offset(由于是第一次,runid为?,offset为-1)
- 主节点返回FULLRESYNC runId offset,runId是主节点的runId,offset是主节点目前的offset。 从节点保存信息
- 主节点启动bgsave命令fork子进程进行RDB持久化。
- 主节点将RDB文件发送给从节点,到从节点加载数据完成之前,写命令写入缓冲区。
- 从节点清理本地数据并加载RDB,如果开启了AOF会重写AOF。
(
1
)主节点通过
bgsave
命令
fork
子进程进行
RDB
持久化,该过程是非常消耗
CPU
、内存
(
页表复制
)、硬盘
IO。
(
2
)主节点通过网络将
RDB
文件发送给从节点,对主从节点的带宽都会带来很大的消耗。
(
3
)从节点清空老数据、载入新
RDB文件的过程是阻塞的,无法响应客户端的命令;如果从节点执行 bgrewriteaof,也会带来额外的消耗。
部分复制:
1.
复制偏移量:执行复制的双方,
主从节点,分别会维护一个复制偏移量offset
。
2.
复制积压缓冲区:
主节点内部维护了一个固定长度的、先进先出(FIFO)队列作为复制积压缓冲区
,当主从节点
offset
的差距过大超过缓冲区长度时,将无法执行部分复制,只能执行全量复制。
3.
服务器运行
ID(runid)
:每个
Redis
节点,都有其运行
ID
,运行
ID由节点在启动时自动生成,
主节点会将自己的运行ID发送给从节点,从节点会将主节点的运行ID存起来
。
从节点Redis断开重连的时候,就是根据运行ID来判断同步的进度
:
- 如果从节点保存的runid与主节点现在的runid相同,说明主从节点之前同步过,主节点会继续尝试使用部分复制(到底能不能部分复制还要看offset和复制积压缓冲区的情况);
- 如果从节点保存的runid与主节点现在的runid不同,说明从节点在断线前同步的Redis节点并不是当前的主节点,只能进行全量复制。
过程原理:
六、缓存穿透、缓存击穿、缓存雪崩
- 缓存穿透
缓存穿透是指缓存和数据库中都没有的数据
,导致所有的请求都落到数据库上,造成数据库短时间内承受大量请求而崩掉。
解决方案:
- 接口层增加校验,如用户鉴权校验,id做基础校验,id<=0的直接拦截;
- 缓存空对象。从缓存取不到的数据,在数据库中也没有取到,这时也可以将key-value对写为key-null,缓存有效时间可以设置短点,如30秒(设置太长会导致正常情况也没法使用)。这样可以防止攻击用户反复用同一个id暴力攻击。
- 采用布隆过滤器,将所有可能存在的数据哈希到一个足够大的 bitmap 中,一个一定不存在的数据会被这个 bitmap 拦截掉,从而避免了对底层存储系统的查询压力。
- 缓存击穿
缓存击穿是指缓存中没有但数据库中有的数据(一般是缓存时间到期的热点数据),这时由于并发用户特别多,同时读缓存没读到数据,又同时去数据库去取数据,引起数据库压力瞬间增大,造成过大压力。和缓存雪崩不同的是,缓存击穿指并发查同一条数据,缓存雪崩是不同数据都过期了,很多数据都查不到从而查数据库。
解决方案
- 设置热点数据永远不过期。
- 加互斥锁。
- 缓存雪崩
缓存雪崩是指缓存同一时间大面积的失效,所以,后面的请求都会落到数据库上,造成数据库短时间内承受大量请求而崩掉。
解决方案:
- 缓存数据的过期时间设置随机,防止同一时间大量数据过期现象发生。
- 给每一个缓存数据增加相应的缓存标记,记录缓存是否失效,如果缓存标记失效,则更新数据缓存。
- 缓存预热。
- 互斥锁。
七、如何保证数据库与缓存的一致性?
由于缓存和数据库是分开的,无法做到原子性的同时进行数据修改,可能出现缓存更新失败,或者数据库更新失败的情况,这时候会出现数据不一致,影响前端业务。
问题
- 先更新数据库,再更新缓存。缓存可能更新失败,读到老数据。
- 先删缓存,再更新数据库。并发时,读操作可能还是会将旧数据读回缓存。
解决方案
1》先操作缓存,但是不删除缓存,将缓存修改为一个特殊值(-999)。客户端读缓存时,发现是默认值,就休眠一小会,再去查一次Redis。 -》 特殊值对业务有侵入。 休眠时间,可能会多次重复,对性能有影响。
2》延时双删。 先删除缓存,然后再写数据库,休眠一小会,再次删除缓存。-》 如果数据写操作很频繁,同样还是会有脏数据的问题。
- 先更新数据库,再删缓存。也存在缓存删除失败的可能。
解决方案
1》给缓存设置一个过期时间 问题:过期时间内,缓存数据不会更新。
2》引入MQ,保证原子操作。
将热点数据缓存设置为永不过期,但是在value当中写入一个逻辑上的过期时间,另外起一个后台线程,扫描这些key,对于已逻辑上过期的缓存,进行删除。
删除比更新操作更加轻量,是延迟加载的一种实现,更新可能涉及多个表、比较耗时。
终极方案:
将访问操作串行化
- 先删缓存,将更新数据库的操作放进有序队列中。
- 从缓存查不到的查询操作,都进入有序队列。
会面临的问题:
- 读请求积压,大量超时,导致数据库的压力:限流、熔断。
- 如何避免大量请求积压:将队列水平拆分,提高并行度。
- 保证相同请求路由正确。
八、布隆过滤器原理,优缺点
位图:int[10]
,每个
int
类型的整数是
4*8=32
个
bit
,则
int[10]
一共有
320 bit
,每个
bit
非
0
即
1,初始化时都是
0
添加数据时,将数据进行hash得到hash值,对应到bit
位,将该
bit
改为
1
,
hash函数可以定义多个,则一个数据添加会将多个(
hash
函数个数)
bit
改为
1
,多个
hash
函数的目的是减少
hash碰撞的概率
查询数据:hash函数计算得到hash
值,对应到
bit
中,如果有一个为
0
,则说明数据不在
bit中,如果都为
1
,则该数据可能在
bit
中。
优点:
- 占用内存小
- 增加和查询元素的时间复杂度为:O(K), (K为哈希函数的个数,一般比较小),与数据量大小无关
- 哈希函数相互之间没有关系,方便硬件并行运算
- 布隆过滤器不需要存储元素本身,在某些对保密要求比较严格的场合有很大优势
- 数据量很大时,布隆过滤器可以表示全集
- 使用同一组散列函数的布隆过滤器可以进行交、并、差运算
缺点:
- 误判率,即存在假阳性(False Position),不能准确判断元素是否在集合中
- 不能获取元素本身
- 一般情况下不能从布隆过滤器中删除元素