缓存穿透
实际上就是一直查询根本不存在的数据,导致每次查询都请求到数据库,使数据库压力过大
解决方法
1、缓存空数据,如果查询根本不存在的数据,就造一个空数据写入缓存供其查询,但这样会消耗内存,并且如果后期这个不存在的数据被插入到数据库中,则会出现redis和数据库数据不一致的情况
2、使用布隆过滤器,布隆过滤器就是在数据预热(缓存热点数据)的同时将这些数据库数据同时写入布隆过滤器(布隆过滤器就是用来拦截根本不存在的数据的),并且布隆过滤器是基于位图的,所以写入方法是:写入数据时先经过多次不同的hash运算,将位图中对应位置的值置为1,那么在查询数据时会针对要查询的数据同样做hash运算,判断位图中对应位置的值是否为1,如果为1,说明数据存在。
但这样存在误判的情况,即
既可以扩大位图(但会消耗内存),也可以手动设置误判率(0.05为可接受的误判率,不至于压垮数据库)
缓存击穿
缓存击穿就是某个key突然过期,而刚好有针对该key的大量请求(即可称为热点key),导致查询缓存时出现查询不到大部分数据的情况,请求都打到数据库上,使数据库压力过大
解决方法
互斥锁就是在大量线程查询缓存时只允许一个线程在查询不到缓存的情况下通过数据库数据重建缓存,而其他线程因为获取不到锁而等待一段时间后又重试,这里的重试指的是从查询缓存开始,直到缓存被获取到锁的线程重建成功后,其他线程就可以获取到缓存了。这种方式最终获取到的缓存数据具有强一致性(也就是一定是正确的),但由于其他线程需要等待,所以性能差
逻辑过期就是一旦发现缓存数据过期,同样会加锁重建缓存,但是通过新开一个线程进行重建的,父线程会直接返回过期的数据,不必等待,同时其他线程无法获取到锁重建缓存,说明此刻已经有其他线程在重建缓存了,因此这个线程只需要返回过期数据即可,也不必等待。这种方式最终是高可用,性能优的
缓存雪崩
缓存雪崩就是大量的key过期或者redis宕机导致大量请求打到数据库
解决方法
如图,针对于大量key过期,可以给TTL添加随机值,针对于redis宕机,可以使用哨兵、集群,还可以限流、添加多级缓存
双写一致性
对于写操作,如何保证双写一致性:使用延迟双删
先删除缓存还是先删除数据库
先给答案:两种方式都有问题
在线程1写数据时,首先删除缓存,突然有线程2来查缓存,发现缓存不存在,所以根据数据库重建缓存,并查询到数据v=10,之后线程1继续执行,写入新数据v=20,这样就出现线程2读取到老数据的情况,并且数据库和缓存也不一致
缓存过期,线程1查询不到缓存于是去查询数据库,此时突然有线程2来写数据库,由于此刻还未重建缓存,于是线程2删除缓存是没有用处的,之后线程1根据重建缓存,但重建的缓存是旧数据,于是又出现了数据库和缓存不一致的情况
为什么要双删
防止数据库和缓存数据不一致(即防止脏数据)
为什么要延时
因为实际场景使用数据库主从模式,需要延迟一定时间让主库的数据同步到从库,但延迟的时间无法精确确定,因此只能控制一部分的风险
实现双写一致
强一致性要求的业务:
如果使用分布式锁,即将删除缓存和删除数据库锁成一个原子性操作,则线程会互斥等待,性能低。所以可以从缓存的特点出发,缓存的数据一般都是读多写少的,写多的数据一般不会存在缓存中,所以可以使用共享锁和排他锁,共享锁是指读操作不互斥,但是写操作互斥,排他锁是指所有操作都互斥,因此与分布式锁相比,读写锁有一部分锁是不互斥的,也就是线程无需等待,从而提高了一部分性能,保证了数据强一致,但总体上性能还是低的,因此读写锁适用于强一致性要求的业务
读写锁的实现如上
允许延迟一致的业务:
使用mq(消息队列)进行异步通知,具体原理可参考redis学习记录中的收银员--厨师场景
使用阿里中间价Canal进行异步通知
持久化
RDB
RDB的执行原理
这里的原理和数据库篇中的原理一致,所以不再赘述,或者可以自行查看视频
需要补充的点是,主进程写数据的时候拷贝数据副本,之后的主进程的读操作也映射到数据副本,此时就出现了内存数据与磁盘数据不一致的情况,但不必担心,因为每次fork子进程写rdb文件后,子进程会被回收,一段时间后再次fork子进程写rdb文件就会将数据副本的数据写入rdb文件,这个过程是一个不断重复的过程,所以不必担心数据不一致的情况
AOF
对比
数据过期策略
惰性删除,cpu不需要经常性地检查key是否过期,提高了一部分性能,但是由于不检查过期,也就不会删除数据,会占用空间
定期删除,为了保证对cpu保持一定的友好性,限制了删除操作的频率和耗时,从而使cpu可以空出一部分时间去干别的事,同时由于定期删除不像惰性删除那样不检查过期就不删除数据,而是间隔一定时间删除数据,从而也保证了空间的释放,不会占用很大的空间。然而删除操作的频率和耗时是难以确定的
Redis的缓存失效会不会立即删除
数据淘汰策略
假如缓存过多,内存是有限的,内存被占满了怎么办:使用数据淘汰策略
注意区分数据淘汰策略和数据删除策略,数据删除策略指的是数据过期后如何处理,而数据淘汰策略是指内存空间不够后数据如何处理
个人认为比较重要的策略:
- 不淘汰任何key,但不允许写新数据
- 根据ttl的剩余值进行淘汰,越小越淘汰
- 对所有key(包括没有设置过期时间的)随机淘汰
- 对设置过期时间的key随机淘汰
- 使用lru算法淘汰
- 使用lfu算法淘汰
- 其他的策略就是上面的策略进行组合得到的
使用建议
熟悉问法
分布式锁
使用场景
集群模式下的定时任务、抢单、幂等性等场景
示例:
加互斥锁可以解决,但性能低,然而如果是在集群模式下,每个jvm各自维护自己的互斥锁,这样就导致仍然可能存在来自不同服务器的线程并发修改数据的情况,因此引进分布式锁(锁住所有的jvm线程)
实现原理
基于setnx实现分布式锁
为了避免由于服务器宕机等原因导致锁没人释放,所以需要设置一个超时时间,但设置的超时时间太短,则可能业务还没执行完所就被释放了,设置的超时时间太长,则业务执行完后锁还要保留一段时间才释放,其他线程依然处于阻塞状态
如何合理的设置超时时间: 1、 预估时间(还不是很合理) 2、 在业务执行的过程中开一个线程来监视业务的执行时间,如果锁到达超时时间还未执行完业务,就给锁的超时时间延长(较为合理,但手动实现还是很麻烦,因此引入redisson分布式锁)
基于redisson实现分布式锁
redisson分布式锁底层使用的是setnx和lua脚本保证原子性
redisson实现的分布式锁提供了一个看门狗机制(相当于另外一个线程)用于给锁续期,redisson分布式锁的默认超时过期时间为30s,那么看门狗会每隔10s重置超时时间(相当于续期),所以在这种情况下,只有业务执行完成后才能手动的释放锁,但是这里的手动释放锁与之前并不冲突,如果服务器宕机了,看门狗机制会失效,也就是不再延迟超时时间,当达到锁的超时时间,同样会被自动释放掉
重试机制:另外一个线程获取不到锁,会不断重试,但重试是有阈值的,达到一定重试次数后,线程会直接结束
如果在使用tryLock时传入了第二个参数,则redisson分布式锁默认你可以自己控制超时时间,此时看门狗机制就不再生效
redisson分布式锁的可重入特性
获取锁时会判断当前线程的id是否在缓存中存在记录,如果存在则重入次数加一,释放锁时则重入次数减一
redisson分布式锁的主从一致性
对于redis搭建的集群,如果主节点突然宕机了,从节点会选举产生新的从节点,但是之前获取锁的线程还未把信息同步到从节点,所以新的主节点依然可以获取到锁,从而出现锁不能实现互斥的情况,可能出现脏数据,因此引入了红锁
其实就是为了保证数据一致(信息同步),会在多个redis节点创建锁,避免数据的丢失,但红锁的实现复杂、性能差、运维繁,实际项目中很少使用
redis集群方案
主从复制、哨兵模式、分片集群
主从复制、主从同步流程
针对的是高并发
主从复制
搭建redis集群,可以采取主从复制的方案,其中master节点主要用于写操作,slave节点主要用于读操作,实现读写分离,同时master节点执行写操作后需要将数据同步到从节点,从而提高redis并发能力
主从同步
全量同步
首先从节点执行replicaof建立连接,并携带replid和offset向主节点请求数据同步,其中主从节点的replid是一致的,如果主节点判断接收到的请求中replid是一致的,说明发送请求的节点是自己的从节点,和自己的存储的数据集是相同的,于是可以将自己的版本信息发送给从节点,从节点保存版本信息
接着主节点执行bgsave命令生成rdb文件发送给从节点,从节点清空本地数据并加载rdb文件,然而由于rdb文件是主节点的父进程fork一个子进程出来写rdb文件的,父进程仍然可以接收请求修改数据,因此为了保证数据一致,从节点加载完rdb文件后,还需要接收到主节点发送来的repl_baklog并执行,这个日志文件保存的就是从节点在加载rdb文件时主节点修改的数据命令,从而进一步保证数据一致性
之后每次同步都不必再加载rdb文件,只需要再执行repl_baklog文件命令即可,那么如何后面每次同步如何知道要执行repl_baklog中的哪部分命令呢,这就需要用到偏移量offset,当从节点再次发起同步请求时会携带offset,主节点对比一下repl_baklog中的offset,从而知道哪部分的数据命令是还未执行过的,将这部分命令发送给从节点执行即可
增量同步
增量同步就是从节点重启等情况下,发送同步请求,被主节点判断为不是第一次同步(因为重启前已经同步过了),所以直接根据offset发送未执行的命令给从节点即可
哨兵模式、集群脑裂
针对的是高可用
哨兵模式
哨兵就是用来监控、选出新的主节点、通知redis客户端(java代码中使用redistemplate等客户端,当主节点发送变化,无需修改java代码,客户端会自动使用新的主节点)。哨兵本身也是一个节点
哨兵集群监控redis集群,间隔地向节点发送ping命令,如果响应了pong,则说明节点正常,不响应则还不能认为节点失效了,只能说针对于该哨兵来说这个节点失效了,如果超过一半的哨兵都没收到响应,则说明这个节点真的失效了,需要选取新的主节点(注意术语:主观下线、客观下线)
sentinel集群选取leader:从sentinel集群中选出一个sentinel节点来进行选择redis集群主节点:
选取redis集群主节点方法:
- 根据断开时间的长度,断开时间越长则缺失的数据越多,越不应该成为主节点
- 根据从节点优先级slave-priority,越小优先级越高,越可能成为主节点
- 如果salve-priority都相同,则根据offset值,offset越大,说明从节点同步的数据越多,越可能成为主节点
- 如果offset还是相同,则判断从节点的id大小,越小越可能成为主节点
集群脑裂
由于网络的原因,导致主节点和从节点位于不同的网络分区,且此时哨兵集群无法监测到主节点的状态,则会去从节点的分区中选出新的主节点,这样redis集群就有两个主节点了,看起来就像是分裂了一样,称之为集群脑裂,而此时redis客户端连接的还是老的主节点,写数据还是写在老的主节点中
当网络恢复后,老的主节点就会被强制降为从节点,并作为从节点去同步主节点中的数据,那么之前在老节点新写的数据就被丢失了
解决方案如下:增加redis配置,对于一个主节点必须至少存在一个从节点,也可以设置主从同步的间隔不能超过5秒,如果违反了以上规则,说明出现了集群脑裂
分片集群、数据读写规则
针对的是海量数据存储和高并发写
分片集群
多个master节点,各自拥有自己的从节点,并且分片集群不需要哨兵了,因为不同的master节点就相当于哨兵了,相互之间可以监测,同时也不需要哨兵的通知机制,即不需要哨兵通知java代码的redis客户端使用哪个master节点,因为分片集群具有自动路由功能,可以将客户端的请求路由到正确的master节点上
每个master节点具有各自数量的hash槽,在写缓存的时候会对key进行hash、取模之后放入对应的hash槽,如果要读则根据同样的hash、取模操作得知数据存放在哪个master节点,于是将redis客户端的请求路由到这个正确的master节点
redis主从和集群可以保证数据一致性吗
redis是单线程的,但是为什么还是那么快
讲一下Redis底层的数据结构
ZSet用过吗
Zset 底层是怎么实现的
跳表是怎么实现的
跳表是怎么设置层高的
同上
Redis为什么使用跳表而不是用B+树
压缩列表是怎么实现的
介绍一下 Redis 中的 listpack
哈希表是怎么扩容的
String 是使用什么存储的?为什么不用 c 语言中的字符串
Redis哪些地方使用了多线程
TODO
Redis怎么实现的io多路复用
TODO
Redis的网络模型是怎样的
TODO
如何实现redis 原子性
除了lua有没有什么也能保证redis的原子性
为什么使用redis
Redis 具备高性能
Redis 具备高并发
本地缓存和Redis缓存的区别
redis应用场景是什么
Redis除了缓存,还有哪些应用
Redis支持并发操作吗
Redis分布式锁的实现原理?什么场景下用到分布式锁
Redis的大Key问题是什么
大Key问题的缺点
Redis大key如何解决
什么是热key
如何解决热key问题
如何保证 redis 和 mysql 数据缓存一致性问题
延迟双删(建议先修改缓存、再修改数据库)、分布式锁、读写锁