缓存穿透问题
- 什么是缓存穿透?
例如当我们根据id查询一个数据的时候,但是这个数据本身不存在或者已经被删除之后,缓存中不存在,就会去查询数据库,但是不存在的数据不会缓存到数据库中,那么一旦大量的这个请求出现的时候,就会频繁的穿透Redis去访问数据库,因此就出现了缓存穿透的问题。
缓存穿透:查询一个不存在的数据,mysql查询不到数据也不会写入缓存,就会导致每次请求都会去查询数据库。
解决方案一:缓存空数据,查询数据为空的时候,仍然把空结果返回。
优点:简单
缺点:消耗内存,可能会发生数据不一致问题(加入数据库中存入数据,但是Redis中缓存依然是null)
解决方案二:使用布隆过滤器,查询布隆过滤器,存在查Redis。不存在直接返回。
那么什么是布隆过滤器?
(百度百科)它实际上是一个很长的二进制向量和一系列随机映射函数。布隆过滤器可以用于检索一个元素是否在一个集合中。它的优点是空间效率和查询时间都比一般的算法要好的多,缺点是有一定的误识别率和删除困难。
布隆过滤器通俗的来讲就是查看一个元素是否存在于集合中,这个原理和HashMap存储的原理有那么一点点类似
大概示例如下图:
但是他会存在一个误判率的问题,如果id为3经过三次hash之后得到值都为1但是实际上id为3的数据其实并不存在,但是他也会默认id为3的数据是存在的,具体可以看下面的图,图片更为直观一点:
误判率:误判率也是有一定的规律的,一般数组越小误判率越大,数据越大误判率越小,但是这个在项目开发的过程中是可以控制的,一般是控制在0.05
布隆过滤器优点:内存占用较少,没有多余的key
布隆过滤器缺点:实现复杂,存在误判。
缓存击穿
缓存击穿:给某一个热点的key设置了过期时间,当key过期的时候,恰好这个时间点这个key有大量的请求过来,这些并发请求可能会瞬间将DB压垮。
通俗的解释就是为了提高数据库的性能,我们通常在一些情况下回去使用Redis缓存,比如一个热门的新闻文章等等数据,那么通常会给这个热点数据设置一个过期的时间,那么刚好过期时间到的时候,有大量并发请求过来,就会直接穿过Redis进去DB中,如果超过了DB的最大访问压力是完全可以将DB直接压垮的。
解决缓存击穿的两种方式:
互斥锁:(强一致性的效果,但是性能比较差)
原理:当两个线程同时去查询数据的时候,当线程1查询缓存未命中(缓存过期),获取互斥锁,查询数据库重建缓存数据,如果这个时候线程2查询缓存未命中(缓存获取),那么就会去获取互斥锁,如果获取互斥锁失败,那么就进入休眠,休眠一会儿再进行重试,等待线程1写入缓存,释放锁,线程2获取缓存。
遇到和钱挂钩的业务这里一般采用互斥锁的方式
逻辑过期:(高可用,性能优,但是不能保证数据的一致性)
原理:同时有两个线程,线程1查询缓存时发现逻辑时间过期,获取互斥锁并且开启线程2,随后线程1直接返回已经过期的数据,线程2查询数据库之后重建缓存,写入缓存重置逻辑,过期时间;在线程2执行的期间,如果线程3查询缓存发现缓存时间过期,那么他会去获取缓存锁,如果获取缓存锁失败,他直接返回过期数据,在线程2释放锁之后,如果线程4恰好获取缓存命中,缓存没有过期。
缓存雪崩
缓存雪崩:是指当同一时间有大量的缓存key同时失效或者Redis宕机的时候,如果有大量的请求直接到达数据库,会给数据库带来极大的压力。
解决的方案:
针对大量的缓存key同时过期的问题:
- 可以采用根据字面意思解决的方式,既然是同时过期会出现问题,那么就让他实现不同时过期,可以在每个key的过期时间上随机增加1-5分钟左右的时间,保证大量的key不会在同一时间过期。
针对Rredis宕机的问题:
- 可以采用Redis的集群模式提高服务的高可用性(哨兵模式,集群模式)
- 给缓存添加降级熔断策略(nginx,Spring Cloud,Gateway)
- 给业务添加多级缓存(例如使用Guava或者Caffeine作为一级缓存,使用Redis作为二级缓存)
降级限流策略可以作为缓存穿透,缓存击穿,缓存雪崩的保底层略。
双写一致性问题
当系统中同时使用了Redis和Mysql的时候,如何保证数据的一致性呢?
尤其是针对具有高并发需求的系统,在大量并发来临的时候,难免会出现数据不一致的问题
双写一致性:当修改了数据库的数据之后同时更新缓存,缓存和数据库的数据要保持一致性。
问题出现的分析:
那么在高并发的情况下其实无论是先清除缓存,或者先删除删除数据库都会出现问题:
- 先清除缓存,再清除数据库:
理想情况下:线程1先删除缓存,然后更新数据库,线程2查询缓存,未命中,查询数据库,写入缓存。
当然如果都这样的话那么就没必要说了不是,那么接下来看看异常情况
异常情况下:线程1删除缓存,那么正好这个时候线程2来了,查询缓存未命中,然后去查询数据库,写入缓存10,然后线程1这个时候正好将数据库数据更新到20,那么就会出现一个问题,缓存为10,数据库为20。
如下图:
- 先操作数据库,再删除缓存
理想情况下:线程2更新了数据库,随后线程2删除缓存,线程1查询缓存,未命中,查询数据库,写入缓存。
还是那句话,想象很美好,现实会让你哭泣,哈哈。
异常情况下:当线程1查询数据的时候缓存中数据过期了,表示缓存中没有数据,线程1查询缓存,未命中,查询数据库10.这个时候线程2更新数据库,然后删除缓存,这个时候切换回线程1,线程1直接将从数据库中查询到的10写入了缓存中。
如下图所示
上面这些问题基本上是在高并发情况下,数据一致性问题中经常会出现的情况,那么就会导致很严重的数据不一致性的问题,因此这个问题是必须解决的,那么最常用的方法就是
延时双删
的策略,但是延时双删只是极大的控制了数据不一致性的风险,同时也由于延迟时间的不确定性,他也会产生一定的脏数据,做不到绝对的强一致性。
那么如何才能做到强一致性呢?
分布式锁的方式:
通俗的讲就是加锁,只有一个线程能去操作他,操作完成之后释放锁,让另一个线程去操作他。
如下图所示:
写入缓存的数据一般是读多写少,读少写多的话不建议存入缓存。
读写锁的控制:
读写锁的实现需要基于以下两种锁来进行实现:
共享锁:也叫读锁ReadLock,加锁之后其他线程可以共享读的操作,不能进行写的操作。
排它锁:也叫独占锁WriteLock,加锁之后,阻塞其他线程的读写操作。
相对与分布式锁的话,这种读写锁的方式效率会更加高一点。
读写锁的创建方式如下:
以上是强一致性的解决方案,主要是强调的数据的一致性
异步通知保证数据一致性的方案:
原理使用MQ进行消息的发布和订阅的方式来保证数据的最终一致性:
双写一致性:
可以使用阿里推出的Canal中间件:
数据持久化
Redis怎么可以实现数据持久化,这个肯定是有的,大佬对这个中间件的设计的非常的全面的,对于我们这些菜鸟来说,哈哈。
下面是实现数据持久化的两种方式:
- RDB:全称Redis Database Backup File(Redis数据备份文件)也叫Redis数据快照,简单的说就是把内存中所有的数据记录在磁盘上,当Redis实例故障重启之后,从磁盘中读取快照文件,恢复数据。
- AOF:全称Append Only File(追加文件)。Redis处理的每一个命令都要记录在AOF文件,可以看做是命令日志文件。
首先先说RDB的方式,他是Redis默认的数据备份的的方式,默认是开启的
- RDB中有两种主动备份数据的方式:
- 第一种主进程执行,阻塞所有进程
save
- 第二种子进程执行,主进程不受影响
bgsave
- 第一种主进程执行,阻塞所有进程
- Redis内部也有触发RDB机制的相关的配置,在redis.conf文件中可以找到。格式如下:
RDB的执行原理:
bgsave
在开始的时候会去fork主进程得到一个子进程,然后子进程是可以共享主进程的内存数据的,完成fork后读取内存数据写入RDB文件中,fork采用的是copy-on-write技术: - 当主进程执行读操作的时候,访问共享内存
- 当主进程执行写操作的时候,则会拷贝一份数据,执行写操作。
如下图所示:
AOF在Redis中是默认关闭的,需要修改redis.conf来配置文件开启AOF
# 是否开启AOF功能 默认no
appendonly yes
# AOF文件名称
appendfilename "appendonly.aof"
AOF的命令记录的频率可以通过redis.conf文件来配置:
# 表示每执行一次写命令,立即记录到aof文件
appendfsync always
# 写命令执行完先放入AOF缓存区,然后表示每隔1秒将缓冲区数据写到AOF文件,是默认方案
appendfsync everysec
# 写命令执行完毕之后先放入缓冲区,由操作系统决定将缓冲区内容写回磁盘。
appendfsync no
配置项 | 刷盘同步 | 优点 | 缺点 |
---|---|---|---|
Always | 同步刷盘 | 可靠性高,几乎不丢失数据 | 性能影响大 |
everysec | 每秒刷盘 | 性能适中 | 最多丢失1秒数据 |
no | 操作系统控制 | 性能最好 | 可靠性差,可能丢失大量的数据 |
因为AOF是记录命令,所以AOF文件回避RDB文件大的很多,而且AOF会记录同一个key的多次写操作,但是只有最后一次写操作才有意义,通过执行bgrewriteof,可以让AOF文件执行重写功能,用最少得命令达到相同的效果。
Redis也会触发阈值自动去重写AOF文件,阈值也可以在redis.conf中配置:
# AOF文件比删词文件 增多超过百分比触发重写
auto-aof-rewrite-percentage 100
# AOF文件体积最小多大以上才会触发重写
auto-aof-rewrite-min-size 64mb
RDB和AOF对比
RDB和AOF各有自己的优缺点,如果数据安全性要求高,在实际开发中往往会结合两者来使用。
缓存过期策略
加入redis中的key过期之后会立即删除吗?
redis对数据设置数据的有效时间,数据过期之后,就需要将数据从内存中删除掉,可以按照不同的规则进行删除,这种删除方式称为删除策略。
惰性删除策略
定义:设置key过期了之后,我们不去管他,当需要key的时候,我们再检查key是否过期,如果过期,我们就删除,反之返回key
优点:对cpu友好,只会使用可以时才会进行定期的检查。对于很多用不到的key不会浪费时间进行定期检查
缺点:对内存不友好,如果key已经过期,但是一直没有使用,那么该key就会一直存在内存中,内存永远不会释放。
定期删除策略:
定义:每隔一段时间我们就对key进行检查,删除过期的key(从一定数量的数据库中取出一定数量的随机key进行检查,并删除其中过期的key)
定期删除的两种模式:
- slow模式是定时任务,执行频率默认是10hz,每次不超过25hm,可以通过redis.conf的hz选项调整这个参数
- fast模式执行频率不固定,但两次间隔不低于2ms,每次耗时不超过2ms
优点:可以通过限制删除操作执行时长和频率来减少操作对CPU的影响。另外定期删除,也能有效释放过期键占用的内存,
缺点:很难确定删除操作执行的时长和效率。
redis缓存过期删除策略,惰性删除+定时删除两种策略配合使用
数据淘汰策略
如果缓存过多的话,redis内存是有限的,内存是完全有可能会占满的
那么这里就需要用到redis的数据淘汰策略:
Redis中支持8
中不同的策略来删除key:
- noevication:不淘汰任何key,但是内存满时不允许写入数据,这是默认策略
- volatile:设置了TTL(过期时间的key),随机进行淘汰
- allkeys-random:对全体的key,随机进行淘汰。
- volatile-random:对设置了TTL的key,随机进行淘汰
- allkeys-lru:对全体key基于LRU算法进行淘汰
LRU:最近最少使用,用当前时间减去最后一次访问的时间,这个值越大则淘汰的优先级越高
LFU:最少频率使用,会统计每隔key的访问频率,值越小淘汰优先级越高。
- volatile-lru:对设置了过期时间的key,基于LRU算法进行淘汰。
- allkeys-lfu:对全体key,基于LFU进行淘汰
- volatile-lfu:对设置了TTL的key,基于LFU算法进行淘汰。
数据淘汰策略-使用建议:
- 优先使用allkeys-lru策略,充分利用LRU算法的优势,把最近最常访问的数据保留在内存中,如果业务中有明显的冷热数据,建议使用。
- 如果业务中数据访问频率差别不大,没有明显的冷热数据区分,建议使用allkeys-random,随机选择淘汰。
- 如果业务中有制定的需求,可以使用volatile-lru策略,同时配置数据不设置过期时间,这些数据就会一直不被删除,会淘汰其他过期时间的数据。
- 如果业务中有短时高频访问的数据,可以使用allkeys-lfu或者volatile-lfu策略。
这里有一个常见问题:
数据库中有1000万数据,redis只能缓存20w条数据,如何保证redis中的数据都是热点数据?
使用allkeys-lru(挑选最近最少使用的策略淘汰,留下来的都是经常访问的热点数据)
redis的内存用完会发生什么?
默认策略的话noevication不删除任何数据,内存不足会直接报错。
分布式锁
使用场景:集群情况下定时任务,抢单,幂等性问题
Redis分布式锁细节:
- redis分布式锁主要是利用了redis中
setnx
命令,setnx
命令是set if not exists(如果不存在则set)的缩写
执行流程:
Redis实现分布式锁如何合理控制锁的有效时长? - 根据业务执行时间预估,给锁续期
redisson实现分布式锁的执行流程:
redisson实现的分布式锁是可重入的,但是会出现主从数据不一致的问题,两个线程同时持有一把锁,那么锁就失去了互斥行的特性。
这里可以使用红锁RedLock
,不能在一个redis实例上创建锁(n/2 + 1),避免redis实例上加锁
但是:
红锁部署的成本高,维护的成本高,运维成本高
高并发的情况下性能很差,官方也不建议使用红锁解决主从不一致问题
Redis是AP思想,保证是高可用性
如果保证强一致性建议使用CP思想的Zookeeper
Redis集群方案
Redis集群中主从数据同步的流程是什么?
主从复制:单节点的redis的并发能力是有上限的,要进一步提高redis的并发能力,就需要搭建主从集群,实现读写分离。
- 主节点master负责数据的写操作,从节点slave负责数据的读操作
- 但是需要注意的是主节点master写入数据必须同步到从节点slave上,这样才能实现真正的读写分离。
主从同步的原理:
全量同步过程:
全量同步的过程:
- slave从节点首先执行replicaof命令建立连接
- slave从节点向master主节点请求数据同步,replid,offset
- master主节点判断是否第一次同步,也就是replid是否一致
- 如果不一致表示第一次,向slave从节点返回master的数据版本信息replid,offset
- slave保存节点版本信息
- master执行bgsave,生成一个RDB文件
- master主节点发生RDB文件
- slave从节点清除本地数据,加载RDB文件
- 在slave加载RDB期间,master节点会记录RDB的所有命令,存储repl_backlog日志文件
- master主节点发送repl_backlog中的命令
- 执行接收到的命令
Replication Id
:简称replid,是数据集的标记,id一致则说明同一个数据集,每个master都有唯一的replid,slave节点则会集成master节点的replid
offset
:偏移量,随着记录在repl_backlog中的数据增多而逐渐增大。slave完成同步时也会记录当前同步的oddset。如果slave的offset小于master的offset,说明slave数据落后于mater,需要更新。
主从增量同步的原理:
执行原理:
- slave从节点重启之后
- pssync replid offset
- master主节点判断请求replid是否一致
- master主节点判断如果不是第一次,回复continue
- master主节点去repl_backlog中获取offset后的数据记录在repl_backlog
- 主节点master发送offset后的命令
- slave从节点执行命令
介绍一下redis的主从同步
单节点的redis并发能力有限,要进一步提高redis的并发能力,就需要搭建主从集群,实现读写分离,一般都是一主多从,主节点负责写入数据,从节点负责读取数据。
能说一下,主从数据同步的流程:
全量同步:
- 从节点请求主节点同步数据(replication id、offset)
- 主节点判断是否第一次请求,是第一次就与从节点同步版本信息(replication id和offset)
- 主节点执行bgsave,生成RDB文件后,发送给从节点去执行
- 在RDB生成执行期间,主节点会以命令的方式记录到缓冲区(一个日志文件)
- 把生成之后的命令日志文件发送给从节点进行同步
增量同步:
- 从节点请求主节点同步数据,主节点判断不是第一次请求,不是第一次就获取从节点的offset值
- 主节点从命令日志中获取offset值之后的数据,发送给从节点进行数据同步。
哨兵模式
Redis提供了哨兵(Sentinel)机制实现了主从集群的自定故障恢复,哨兵的作用如下:
- 监控:sentinel会不断检查你的master和slave是否正常预期进行工作
- 自动故障恢复,如果master故障,sentinel会将slave提升为master,当故障实例恢复后也会以新的master为主
- 通知:sentinel充当了redis客户端服务发现来源,当集群发生故障转移的时候,会将最新的消息推送给redis客户端
服务状态监控:
Sentinel基于心跳机制监测服务状态,每隔1秒向集群的每个实例发送一个ping命令: - 主观下线:如果Sentinel节点发现某个实例未在规定的时间内响应,则认为该实例主观下线
- 客观下线:若超过制定数量(quorum)的sentinel都认为该实例主观下线,则该实例客观下线,quorum值最好超过sentinel实例数量的一半。
哨兵选主规则:
- 首先判断主从节点断开时间长短,如超过指定值就排除该从节点
- 然后判断从节点的slave-priority(从机优先值,值越小优先级越高)值,越小优先级越高
- 如果slave-prority一样则判断slave节点的offset值,越大优先级越高
- 最后判断slave节点的运行id的大小,越小优先级越高
Redis集群(哨兵模式)脑裂问题:
一个正常的redis哨兵模式,如果主节点与从节点网络出现问题,那么sentinel可能访问不到主节点,那么就可能会从从节点选择一个节点作为主节点,可是这个时候原来的主节点并没有挂,RedisClient还在继续写入数据,这样就出现了脑裂问题,当网络恢复了之后,原来的master会被强制降为slave节点,然后从新的master节点中同步数据,删除本节点的数据,这样的话,RedisClient客户端写入的数据就会丢失。
解决脑裂问题的方案:
脑裂问题无法彻底解决,但是可以通过以下两个参数进行配合来解决:
min-replicas-to-write 1
表示最少得slave节点为1个min-replicas-max-lag 5
表示数据复制和同步的延迟不能超过5秒
如何保证Redis的高并发可用?
哨兵模式:实现主从集群的自动故障恢复(监控,自动故障恢复,通知)
Redis集群的脑裂问题,如何解决?
集群脑裂:是由于主节点和从节点和sentinel处于不同的网络分区,使得sentinel没有能够心跳感知到主节点,所以通过选举的方式提升一个新节点作为主节点,这样就存在了两个master,就像大脑分裂一样,这样就导致了客户端还在往主节点写数据,新的节点无法同步数据,当网络恢复之后,sentinel会将老的主节点降为从节点,然后再从新的master中读取数据,导致数据丢失。
解决:我们可以通过修改redis的配置,可以设置最少节点数量和缩短主从数据同步的延迟时间,达不到要求就拒绝请求,就可以避免大量的数据丢失。
分片集群结构
主从和哨兵可以解决高并发,高并发读的问题,但是依然有两个问题没有解决:
-
海量数据存储的问题
-
高并发写的问题
使用分片集群的方式可以解决上述的问题,分片集群的特征: -
集群中有多个master,每个master保存不同的数据
-
每个master都可以有多个slave节点
-
master之间通过ping监测彼此的健康状态
-
客户端请求可以访问集群的任意节点,最终都会转发到正确的节点上
Redis分片集群-数据读写
Redis分片集群中引入哈希槽的概念,Redis集群中有16384个哈希槽,每个key通过CRC16校验后对16384取模来决定放置那个槽,集群的每个节点负责一部分的哈希槽
Redis分片集群中有什么作用 -
集群中有多个master,每个master保存不同的数据
-
每个master都可以有多个slave节点
-
master之间通过ping监测彼此的健康状态
-
客户端请求可以访问集群的任意节点,最终都会被转发到正确的节点
Redis分片集群中数据是怎么储存和读取的?
- Redis分片集群引入hash槽的概念,Redis集群有16384个哈希槽
- 将16384个插槽分配到不同的实例
- 读写数据:根据key的有效部分计算哈希值,对16384取余(有效部分,如果key前面有大括号,大括号的内容就是有效内容,如果没有,则以key本身作为有效部分)余数作为插槽,寻找插槽所在的实例
Redis单线程高效的原因
Redis是单线程的,但是为什么还是那么快?
- Redis是纯内存操作,执行速度快
- 采用单线程,避免的不必要的上下文切换可竞争条件,多线程还要考虑线程安全的问题
- 采用I/O多路复用模型,非阻塞IO
能解释下I/O多路复用模型吗?
Redis是纯内存操作,执行速度快,他的性能瓶颈是网络延迟而不是执行速度,I/O多路复用模型主要实现了高效的网络请求
- 用户空间和内核空间
- 常见的I/O模型
- 阻塞I/O
- 非阻塞I/O
- I/O多路复用
- Redis网络模型
I/O多路复用:是指利用单个线程来同时监听多个socket,并在某个socket可读,可写时得到通知,从而避免无效的等待,充分利用CPU资源,目前的I/O多路复用都是采用epoll模式实现的,他会通知用户进程socket就绪的同时,把已经就绪的socket斜土用户空间,不需要挨个遍历socket来判断是否就绪,提高了性能。
Redis网络模型:就是使用I/O多路复用结合事件的处理器来应付多个socket请求
连接应答处理器
命令回复处理器,在redis6.0之后,为了提高性能,采用了读线程来处理回复事件
命令请求处理器,在redis6.0之后,将命令转化使用了多线程,增加命令转化速度,在命令执行的时候,依然是单线程
笔记是对黑马课程中的知识进行的个人总结,图片借鉴了课程视频中的资料,感谢黑马程序员的开源精神,哈哈,如有问题联系我删除!