如何保证缓存和数据库双写一致性?
先更新数据库,更新缓存
先更新缓存,再更新数据库
缺点:写多的场景下,每次更新操作都要更新下缓存,性能损耗非常大,一般不用
先更新数据库,再删除缓存
Q1: 更新数据操作成功,缓存却删除失败,其他请求每次查询到缓存都是错误的数据,怎么解决数据不一致?
A1: 通过消息队列MQ进行删除补偿,这种方案会大量侵入业务代码,可通过订阅BinLog日志优化,会增加系统复杂度
先删除缓存,再更新数据库
Q1: 并发下,读请求A在写请求B更新操作期间同时查询缓存为空,再查询数据库为旧值(事务还没提交或还没更新成功),导致数据不一致?
A1: 延时双删
Q2: 延时双删在数据库主从架构下,主从同步延迟导致数据不一致?
A2: 强制读主库
Redis内存淘汰策略有哪些?
当内存使用超过配置时的内存淘汰策略:不处理/LRU/LFU/随机/过期时间
noeviction
: 不会驱逐任何key
allkeys-lru
: 对所有key使用LRU算法进行删除
volatile-lru
: 对所有设置了过期时间的key使用LRU算法进行删除
allkeys-random:
对所有key随机删除
volatile-random
: 对所有设置了过期时间的key随机删除
volatile-ttl
: 删除马上要过期的key
allkeys-lfu
: 对所有key使用LFU算法进行删除
volatile-lfu
: 对所有设置了过期时间的key使用LFU算法进行删除
LRU内存淘汰策略?
最近最少使用对象淘汰算法(基于时间)
核心:如果数据最近被访问过,那么将来被访问的几率也越高
实现步骤
1、底层结构使用链表,插入删除效率高O(1),查询效率低O(n)
2、将缓存命中的Key先从链表遍历删除,再将Key存储到链表头部
3、当内存达到设定的阈值时,先删除链表尾部的Key
Q1: 怎么解决链表遍历查询效率低?
A1: 使用HashMap保存Key和链表的索引值(空间转时间)
A2: 使用JDK的实现LinkedHashMap
LFU内存淘汰策略?
最近最不常用对象淘汰算法(基于访问频次)
核心:如果一个数据在最近一段时间内使用次数很少,那么在将来一段时间内被使用的可能性也很小。
实现步骤
1、底层结构使用HashMap<String,Integer>对应key信息和访问次数,
2、插入/删除/查询指定Key的时间复杂度O(1),遍历最少访问次数的Key时间复杂度O(n)
Q1: 怎么解决查询次数最少的key耗时问题?
A1: 使用双Hash解决,另一个HashMap把访问频次作为Key,对应的频次的数据组成链表作为Value
缓存雪崩、缓存穿透、缓存击穿?
缓存雪崩:同一时间大面积Key失效,导致所有请求都达到数据库,严重会引发大面积服务挂掉
A1: 设置Key的时候采用失效时间+随机值避免过期时间一致导致同一时间集体失效
A2:设置热点数据永不过期
A3: 集群部署缓存服务,解决大量Key同时失效导致Redis缓存服务挂掉
缓存穿透:请求访问不存在的Key,同时数据库也不存在该数据,导致每次请求都穿透缓存打到数据库
A1: 布隆过滤器(BloomFilter),可判断某条数据是否存在数据库中
A2: 根据具体业务给对应Key设置不同的标识符value=null/数据不存在/0,设置过期时间为30s
A3: Nginx控制同一IP多次请求自动加入黑名单
缓存击穿:某个热点Key被并发访问,待Key过期失效因没重新设值,请求直接击穿缓存打到数据库上
A1: 设置热点数据永不过期
A2: 使用分布式锁控制并发请求,只让其中一个请求成功后,其他请求即可访问缓存拿到数据
A3: 请求接口限流熔断降级
Redis如何保证高可用?
哨兵集群Sentinel:必须用三个以上实例去保证自己的健壮性
集群部署:主从+哨兵保证集群的高可用
数据持久化:集群不能保证数据不丢失,RDB做冷备,AOF做热备
Redis主从原理?
Redis单机模式下能支持的QPS是有上限的,读10W+,写8W1,而且实际应用中既要读又要写。
因此可以使用主从架构模式让master节点负责写任务,slave节点负责读任务,让缓存服务负担减轻,而且能轻松实现水平扩容
主从数据复制原理:
1、当启动slave节点时,会发送psync命令给master
2、如果是第一次连接到master,他会先触发一次全量复制
3、master会fork出一个子进程生成RDB快照,同时会把新的写请求都缓存在内存
4、RDB文件生成后,master会将其发送给slave
5、slave拿到之后先写入本地的磁盘,再加载到内存
6、然后master再把内存里面缓存的新命令发给slave
Redis哨兵原理?
背景:当主节点master出现异常时不能实现主从复制节点自动切换、故障处理转移等操作。而哨兵机制正是基于Redis的主从复制进行的节点监控管理机制,是Redis高可用的一个重要实现。
核心机制:三个定时监控任务完成对各个节点的发现和监控
定时任务 | 触发间隔 | 功能 |
---|---|---|
定时info | 10s | Sentinel节点获取最新redis节点信息和拓扑关系 |
定时pub/sub | 2s | Sentinel节点通过订阅master频道进行彼此信息通信 |
定时ping | 1s | Sentinel节点检查与所有redis节点、其他哨兵节点网络 |
故障转移
- 当
sentinel node
节点监听到master
节点出现故障,slave
从节点无法对master
进行数据复制 sentinel node
发现master
节点异常,会在sentinel集群节点
中内部进行投票选举出leader
来进行master
、slave
业务数据节点故障进行转移处理,并通知client
客户端- 当新的
master
产生后,slave
节点会复制新的master
,但是还会继续监听旧的master
节点 - 当旧的
master
节点故障恢复后,由于sentinel集群
一直监听,会重新将其作为新的slave
节点纳入集群管理中,开始复制新的master
节点,实现节点故障后的重复利用
哨兵leader选举(Raft算法)
- 当一个sentinel节点确认redis集群的主节点下线后
- 请求其他sentinel节点要求将自己选举为leader。被请求的sentinel节点如果没有同意过其他节点的选举请求,则默认同意,选举票数+1,否则不同意
- 当一个sentinel节点获得选举票数达到最低票数(sentinel节点数/2+1)时,选举该节点为leader
下线判断(通过ping/pong消息实现节点间通信用来交互节点间的状态信息)
- sentinel集群中每个节点会定时对redis集群中所有节点进行心跳检测
- 如果一个节点在down-after-milliseconds时间内没回复sentinel节点的心跳包,则该sentinel节点认为其主观下线
- 当节点被一个
sentinel节点
记为主观下线时,并不意味着该节点肯定故障了,还需要sentinel集群
的其他sentinel节点
共同判断(半数以上)为主观下线才行。
master节点选举
- 过滤故障节点
- 选择优先级slave-priority最大的slave作为master
- 选择复制偏移量offset最大的slave作为master
- 选择runId最小的slave作为master,随机方案兜底
Redis集群原理?
数据分区路由规则:哈希取模算法、一致性哈希、虚拟槽
Gossip通讯协议:节点彼此不断通信交换信息,一段时间后所有的节点都会知道集群完整的信息
- meet: 新节点加入触发,通知新节点加入
- ping: 每秒触发,检测节点在线、交互状态信息
- pong: 回复、广播触发,封装自身状态信息
- fail: 节点异常触发,广播节点异常信息
优点:能够保持对集群节点信息的更新采集,较快响应异常
缺点:由于是点对点通讯,集群节点间通讯成本较高,且随集群规模越大,成本呈指数级增长
集群搭建流程
- 准备节点:做好配置
- 节点握手:cluster meet {ip} {port} 建立节点间连接,开始Gossip协议消息通讯,这时候节点集群状态还是fail的,还需要进行槽位分配
- 分配槽:16384个槽位全部分配给节点后,建立集群节点与槽范围的映射关系,集群才进入在线状态
Redis Cluster中计算槽位hash值采用CRC16算法,16bit可以产生2^16=65536个值,为什么不mod(65536),而是mod(16384) ??
- 正常的心跳包会携带着节点的完整配置,通过使用与旧的配置信息幂等的配置来更新旧配置。 这意味着它们需要包含原始形式的节点的插槽配置信息,使用16384个slots的话将会占用2k的空间,但是如果使用65536个slots的话将会占用8k空间。
- 同时,由于其他设计权衡,RedisCluster不太可能扩展到超过1000个Master节点。
集群限制
- mget/mset只支持相同槽值的key执行批量操作,对于映射为不同slot值的key可能存在于多个节点因此不被支持
- 可通过pipeline方式实现,将mget、mset命令进行批量的get、set命令转换,执行后聚合数据返回
- 当试图通过
lua
脚本来原子地执行一系列命令时,对Key的读写也必须遵循这个要求
Redis冷备、热备、数据恢复?
两张数据持久化方式:RDB、AOF
RDB: 全量持久化,执行bgsave命令后先fork出一个子进程生成RDB快照文件,保存fork前的全量数据
优点:
1、适合做冷备,可定时备份数据到远程服务器,可将数据回滚到指定时间点
2、对Redis的性能影响非常小,在同步数据时只是fork出一个子进程去做持久化
3、恢复数据速度比AOF快
缺点:
1、默认5分钟或者更久的时间才会生成一次,会丢失5分钟的数据
2、生成快照时如果文件很大,客户端可能会暂停几毫秒甚至几秒
AOF: 增量持久化,执行fsync命令后后台开启一个线程将aof buffer缓存区的新命令以append-only的方式追加到AOF日志文件
优点:
1、每秒一次通过后台线程fsync操作,最多丢一秒的数据
2、对日志文件的操作以append-only的方式去写,少了磁盘寻址的开销,写入性能惊人,文件不容易破损
3、通过非常可读的方式记录日志,适合做灾难性数据误删除的紧急恢复
缺点:
1、AOF文件比RDB的大,恢复速度慢
2、开启AOF后Redis支持写的QPS会比RDB支持写的要低
Redis分布式锁?
Redis数据类型?
基础数据类型
- String
- Hash
- List
- Set
- SortedSet
高级数据类型
- HyperLogLog: 供不精确的去重计数功能
- Geo:保存地理位置,作位置距离计算、根据半径计算位置
- Pub/Sub: 订阅发布功能,简单的消息队列
- BitMap:实现BloomFilter
- pipeline: 批量执行一组指令,一次性返回全部结果
- lua: 提交lua脚本来执行一系列原子性的操作
- 事务: 只保证串行执行指令,保证全部执行,但是执行指令失败是并不会回滚,而是继续执行下去
Module
- BloomFilter
- RedisSearch
- Redis-ML
Redis为什么那么快?
1、基于内存实现并且KV存储,查找和操作的时间复杂度都是O(1)
2、数据结构简单,对数据操作也简单
3、单线程模型避免了不必要的上下文切换和竞争条件,也不存在多进程或多线程导致的切换而消耗CPU,不用去考虑各种锁的问题。不存在加锁释放锁死锁导致的性能消耗问题
4、IO多路复用模型:select/epoll机制,一个线程处理多个IO流
LUA脚本?
Redis单个命令是原子性的,有时候我们也需要能够组合多个Redis命令且保证原子性的执行,在2.6版本引入了Lua.
Redis怎么实现延时队列?
使用SortedSet数据类型实现,将时间戳当做score,lpush生产消息,rbpop拉取阻塞消息,根据score消费消息
Pipeline有什么用?
Redis6.0为啥改成多线程模型?单线程不是已经很快了吗?
redis6.0之前网络IO请求和数据读写都是单线程的,IO多路复用虽然能提升IO的利用率,但是在处理IO请求时,调用select的过程是阻塞的,也就是说这个过程是阻塞线程,如果并发量很高,此处可能会成为瓶颈。
redis6.0在网络IO请求的处理用了多线程,解决之前大量cpu时间片耗费在网络io的同步处理上,充分发挥多核的优势,减少由于网络io等待造成的影响。