- 缓存穿透
缓存穿透:查询一个不存在的数据,mysql查询不到数据也不会直接写入缓存,就会导致每次请求都查询数据库。
造成原因:1、业务误操作导致
2、有人恶意攻击,查询一些不存在的数据
解决方案一:
缓存空数据,查询返回的数据为空,仍把这个空数据进行缓存,例如:{key:1,value:null}
优点:简单
缺点:消耗内存,可能会发生不一致的问题
解决方案二:
布隆过滤器
bitmap(位图):相当于是一个以(bit)位为单位的数组,数组中每个单元只能存储二进制数0或1
布隆过滤器作用:可以用于检索一个元素是否在一个集合中
数组越小,误判率越大,数组越大误判率越小,但是同时带来了更多的内存消耗。
布隆过滤器的实现方案:Redisson、Guava
缓存击穿
缓存击穿:给某一个key设置了过期时间,当key过期的时候,恰好这个时间点对这个key有大量的并发请求过来,这些并发的请求可能会瞬间把DB压垮。
解决方案一:互斥锁(强一致,性能差)
解决方案二:逻辑过期(高可用,性能优)
缓存雪崩
缓存雪崩是指在同一时间段有大量的的缓存key同时失效或者Redis服务宕机,导致大量请求到达数据库,带来巨大压力。
解决方案:
- 给不同的Key的TTL添加随机值
- 利用Redis集群提高服务的高可用性 (哨兵模式、集群模式)
- 给缓存业务添加降级限流策略 (nginx或spring cloud gateway)
- 给业务添加多级缓存 (Guava或Caffeine)
双写一致性
双写一致性:当修改了数据库的数据,也要同时更新缓存的数据,缓存和数据库的数据要保持一致。
读操作:缓存命中,直接返回,缓存未命中查询数据库,写入缓存,设定超时时间
写操作:延迟双删
先删除缓存还是先修改数据库???
先删除缓存,再修改数据库
1、当线程1删除缓存后,将数据库值改为20,线程2此时来查询数据,未命中缓存,查询数据库获得值为20,此时不会出现问题。
2、当线程1删除缓存后,还没来得及修改数据库值,线程2此时来查询数据,未命中缓存,查询数据库获得值为10(未修改的数据),线程2将值缓存到redis中,此时线程1将数据库中值改为20,此时会出现数据不一致的情况(脏数据)。
先操纵数据库,再删除缓存
1、线程1更新数据库中数据为20,并且删除缓存中数据,此时线程2来查询数,命中缓存中数据20,此时不会出问题。
2、线程1查询缓存,由于数据过期,未命中缓存,查询数据库数据,获得数据为10,此时线程切换,线程2修改数据库中值为20,删除缓存,此时线程1将数据10写入缓存,此时会造成数据不一致的问题。
为什么要删除两次缓存??
为了避免脏数据的产生
为什么还要延时双删?
因为我们一般数据库都是主从模式,所以要延时一会,让主数据库将数据同步到从数据库中,
延时这个时间不好控制,在延时的过程中,依然可能出现脏数据的产生,所以延时双删只能避免一部分脏数据产生的风险。做不到绝对的强一致。
如何解决双写一致问题?
1、添加分布式锁,性能比较差
添加redisson提供的共享锁(读锁)和排他锁(写锁)(强一致,性能差)
允许延时一致的业务,采用异步通知。
具体代码实现:
1、RabbitMQ,使用MQ中间件,更新数据之后,通知缓存删除(允许延时一致)
,
2、canal监听mysql数据库的binlog文件变化,不需要修改业务代码,伪装为mysql的一个从节点,canal通过读取binlog数据更新缓存(允许延时一致)
- 二进制日志(binlog)记录了所有的DDL(数据定义语言)语句和DML(数据操纵语言)语句,但不包括数据查询(SELECT,SHOW)语句。
问:redis作为缓存,mysql的数据如何与redis进行同步呢?(双写一致性)(强一致性)
答:我们的项目中,里面有xxx的功能,需要让数据库与redis高度保持一致,因为要求时效性比较高,我们当时采用的读写锁保证的强一致性。
我们采用的时redisson实现的读写锁,在读的时候添加共享锁,可以保证读读不互斥,读写互斥。当我们更新数据的时候添加排他锁,它是读写,读读都互斥,这样就能保证在写数据的同时是不会让其他线程读数据的,避免了脏数据。这里需要注意的是读方法和写方法需要使用同一把锁才行。
问:那这个排他锁是如何保证读写、读读互斥的呢?
答:其实排他锁底层使用的也是setnx,保证了同时只能有一个线程操作锁住的方法。
问:你听说过延时双删吗?为什么不适用它?
答:延迟双删,如果是写操作,我们先把缓存中的数据删除,然后更新数据库,最后再延时删除缓存中的数据,其中这个延时多久不太好确定,在延时的过程中,可能会出现脏数据,并不能保证强一致性,所以没有采用它。
问:redis作为缓存,mysql的数据如何与redis进行同步呢?(双写一致性)(允许延时一时的业务)
答:我们的项目中,里面有xxx的功能,数据同步可以有一定的延时(符合大部分业务)
我们当时采用的是阿里的canal组件实现数据同步,不需要更改业务代码,部署一个canal服务。
canal服务把自己伪装成mysql的一个从节点,当mysql数据更新以后,canal会读取binlog数据,然后在通过canal的客户端获取到数据,更新缓存即可。
持久化
RDB,全称Redis DataBase Backup file(Redis数据备份文件),也被叫做redis数据快照,简单来说就是把内存中的所有数据都记录到磁盘中,当redis实例故障重启后,从磁盘读取快照文件,恢复数据。
RDB的执行原理:
bgsave开始时会fork主进程得到子进程,子进程共享主进程的内存数据,完成fork后读取内存数据并写入RDB文件。
fork采用的是copy-on-write技术:
- 当主进程执行读操作时,访问共享内存
- 当主进程执行写操作时,则会拷贝一份数据,执行写操作
AOF,全称为Append only File(追加文件)。Redis处理每一个写命令都会记录在AOF文件,可以看作是命令日志文件。
因为是记录命令,AOF文件会比RDB文件大的多,而且AOF会记录对同一个key的多次写操作,但只有最后一次写操作才有意义。通过执行bgrewriteaof命令,可以让AOF文件执行重写功能,以最少的命令达到相同的效果。
Redis也会在触发阈值时自动去重写AOF文件。阈值也可以在redis.conf中配置
数据过期策略
redis对数据设置数据的有效时间,数据过期以后,就需要将数据从内存中删除掉,可以按照不同的规则进行删除,这种删除规则被称为数据的删除策略(数据过期策略)。
惰性删除:设置该key过期时间后,我们不去管它,当需要该key时,我们再检查其是否过期,如果过期,我们就删除它,反之,返回该key。
优点:对CPU比较友好,只会在使用该key时才会进行过期检查,对于很多用不到的key不用浪费时间进行检查。
缺点:对内存不友好,如果一个key已经过期,但是一直没有使用,那么该key就会一直存在内存中,内存永远不会释放。
定期删除:每隔一段时间,我们就会对一些key进行检查,删除里面过期的key(从一定数量的数据库中取出一定数量的随机key进行检查,并删除其中的过期key)。
定期清理有两种模式:
- SLOW模式是定时任务,执行频率默认为10hz,每次不超过25ms,以通过修改配置文件redis.conf的hz选项来调整这个次数。
- FAST模式执行频率不固定,但每次间隔不低于2ms,每次耗时不超过1ms。
优点:可以通过限制删除操作执行的时长和频率来减少删除操作对CPU的影响。另外定期删除,也能有效释放过期键占用的内存。
缺点:难以确定删除操作执行的市场和频率。
Redis的过期删除策略:惰性删除 + 定期删除两种策略进行配合使用
数据淘汰策略
数据的淘汰策略:当Redis的内存不够用时,此时在向redis中添加新的key,那么redis就会按照某一种规则将内存中的数据删除掉,这种数据的删除规则被称之为内存的淘汰策略。
redis支持8中不同策略来选择要删除的key:
- noeviction:不淘汰任何key,但是内存满时不允许写入新数据,默认就是这种策略。
- volatile-ttl:对设置了TTL的key,比较key的剩余TTL值,TTL越小越先被淘汰。
- allkeys-random:对全体key,随机进行淘汰。
- volatile-random:对设置了TTL的key,随机进行淘汰。
- allkeys-lru:对全体key,基于LRU算法进行淘汰。
- volatile-lru:对设置了TTL的key,基于LRU算法进行淘汰。
- allkeys-lfu:对全体key,基于LFU算法进行淘汰。
- volatile-lfu:对设置了TTL的key,基于LFU算法进行淘汰。
LRU(Least Recently used):最近最少使用,用当前时间减去最后一次访问时间,则淘汰优先级越高。
LFU(Least Frequently used):最少频率使用,会统计每个key的访问频率,值越小淘汰优先级越高。
数据淘汰策略-使用建议
1、优先使用allkeys-lru策略。充分利用LRU算法的优势,把最近最常访问的数据留在缓存中,如果业务有明显的冷热数据区分,建议使用。
2、如果业务中数据的访问频率差别不大,没有明显冷热数据区分,建议使用allkeys-random,随机选择淘汰。
3、如果业务中有置顶的需求,可以使用volatile-lru策略,同时置顶数据不设置过期时间,这些数据就一直不被删除,会淘汰其他设置过期时间的数据。
4、如果业务中有短时高频访问的数据,可以使用allkeys-lfu或volatile-lfu策略。
问:数据库有1000万数据,redis只能缓存20w数据,如何保证redis中的数据都是热点数据?
答:使用allkeys-lru(挑选最近最少使用的数据淘汰)淘汰策略,留下来的都是经常访问的热点数据。
问:redis的内存用完了会发生什么?
答:主要看你数据淘汰策略是什么,如果是默认的配置(noeviction),会直接报错。
redis分布式锁
使用场景:集群情况下的定时任务、抢单、幂等性场景
如果是单体项目,加synchronized锁是没有问题的,如下图:
如果是集群模式下:synchronized锁是jvm的,每个服务都有各自的jvm,它只能解决同一个jvm下线程的互斥,解决不了多个jvm下线程的互斥。所以集群模式下就不能使用本地锁来解决,需要使用外部锁来解决。其实就是分布式锁
Redis实现分布式锁主要利用Redis的setnx命令,setnx是SET if not exit(如果不存在,则SET)的简写。
获取锁:
注意:此处如果不设置过期时间会导致死锁的问题。
#添加锁,NX是互斥,EX是设置超时时间
SET lock value NX EX 10
释放锁:
#释放锁,删除即可
DEL key
Redis实现分布式锁如何合理的控制锁的有效时长?
场景解释:如果业务执行时间比较长,超过了锁的有效时间
如果设置了”锁自动释放时间“这个参数,就不会使用watch-dog这个机制了。
加锁、设置过期时间等操作都是基于lua脚本完成。lua脚本可以保证命令执行的原子性
lua脚本能够调用redis命令,保证多条命令执行的原子性
redisson实现的分布式锁-可重入
redisson实现的分布式锁-主从一致性
redis集群的情况下,有线程1获取锁之后将数据缓存到redis-master中,此时master宕机了,还没来的及将数据同步到slave中,此时又进来一个线程2,由于数据没有同步,所有线程2可以获取到同一个分布式锁,将数据缓存到由哨兵模式选中的master中,此时会出现脏数据。
Redisson锁能解决主从数据一致的问题吗?
不能解决,但是可以使用redisson提供的红锁进行解决,但是这样的话,性能就太低了,如果业务中非要保证数据的强一致性,建议采用实现分布式锁。
问:redis分布式锁如何实现?
答:在redis中提供了一个命令setnx,由于redis是单线程的,用了命令之后,只能由一个客户端对某一个key设置值,在没有过期或删除key的时候,其他客户端是不能设置这个key的。
问:如何控制redis实现分布式锁的有效时长?
答:redis的setnx不好控制这个问题,我们当时采用redis的一个框架redisson实现的。
在redisson中需要手动加锁,并且可以控制锁的失效时间和等待时间,当锁住的一个业务还没有执行完成的时候,在redisson中加入了一个看门狗机制,就是说每隔一段时间就检查当前业务是否还持有锁,如果持有就增加加锁的持有时间,当业务执行完成之后需要使用释放锁就可以了。
还有一个好处就是在高并发下,一个业务有可能会执行很快,先客户1持有锁的时候,客户2来了以后并不会马上拒绝,它会自旋不断尝试获取锁,如果客户1释放锁之后,客户2就可以马上持有锁,性能也得到了提升。
问:redisson实现的分布式锁是可重入的吗?
答:是可以重入的,这样做是为了避免死锁的产生。这个重入其实在内部就是判断是否是当前线程持有的锁,如果是当前线程持有的锁就会计数,如果释放锁就会在计算上减一。在存储数据的时候采用的hash结构,大key可以按照自己的业务进行定制,其中小key是当前线程的唯一标识。value是当前线程重入的次数。
问:redisson实现的分布式锁能解决主从一致性的问题吗?
答:这个是不能的,比如,当线程1加锁成功后,master节点会异步复制到slave节点,此时当前持有redis锁的master节点宕机,slave节点被提升为新的master节点,假如现在来了一个新的线程2,再次尝试获取锁,就可以直接获得锁,并进行数据更新到新的master节点,从而导致主从不一致。
redis集群
在redis中提供的集群方案由三种:
- 主从复制
- 哨兵模式
- 分片集群
1、redis主从数据同步的流程是什么?
Replication id:简称replid,是数据集的标记,id一致说明是同一数据集,每一个都有唯一的replid,
salve则会继承master节点的replid
offset:偏移量,随着记录在repl_baklog中的数据增多而逐渐增大,slave完成同步时也会记录当前同步的offset,如果slave的offset小于master的offset,说明slave数据落后于master,需要更新。
redis的主从同步
单节点redis的并发能力是有上限的,要进一步提高redis的并发能力,就需要搭建主从集群,实现读写分离,一般都是一主多从,主节点负责写数据,从节点负责读数据
主从同步数据的流程是什么?
全量同步
- 从节点请求主节点同步数据(replication id, offset)
- 主节点判断是否时第一次请求,是第一次就与从节点同步版本信息(replication id 和offset)
- 主节点执行bgsave,生成rdb文件后,发送给从节点去执行
- 在rdb生成执行期间,主节点会以命令的方式记录到缓冲区(一个日志文件)
- 把生成之后的命令日志文件发送给从节点进行同步
增量同步
- 从节点请求主节点同步数据,主节点判断不是第一次请求,不是第一次就获取从节点的offset值
- 主节点从命令日志中获取offset值之后的数据,发送给从节点进行数据同步
2、怎么保证redis的高并发高可用?
Redis提供了哨兵(Sentinel)机制来实现主从集群的自动故障恢复,哨兵的机构和作用如下:
- 监控:Sentinel会不断检查您的master和salve是否按预期工作
- 自动故障恢复:如果master故障,Sentinel会将一个slave提升为master。当故障实例恢复后也以新的master为主。
- 通知:Sentinel充当redis客户端的服务发现来源,当集群发生故障转移时,会将最新信息推送给redis的客户端
服务状态监控
Sentinel基于心跳机制监测服务状态,每隔1秒向集群的每个实例发送ping命令:
- 主管下线:如果某sentinel节点发现某实例未在规定时间响应,则认为该实例主管下线
- 客观下线:若超过指定数量(quorum)的sentinel都认为该实例主观下线,则该实例客观下线。quorum值最好超过Sentinel实例数量的一半。
哨兵选主规则
- 首先判断主与从节点断开时间长短,如超过指定值就排该从节点
- 然后判断从节点的salve-priority值,越小优先级越高
- 如果slave-prority一样,则判断slave节点的offset值,越大优先级越高
- 最后是判断salve节点的运行id大小,越小优先级越高
3、你们使用redis是单点还是集群,哪种集群?
主从(1主1从)+哨兵就可以了。单节点不超过10GB内存,如果redis内存不足则可以给不同服务分配独立的redis主从节点。
4、redis集群脑裂,该怎么解决?
集群脑裂是由于主节点、从节点和sentinel处于不同的网络分区,使得sentinel没有能够心跳感知到主节点,所以通过一个选举的方式提升了一个从节点为主,这样就存在两个master,就像大脑分裂了一样,这样会导致客户端还在老的主节点那里写入数据,新节点无法同步数据,当网络恢复后,sentinel会将老的主节点降为从节点,这时再从新master同步数据,就会导致数据丢失。
解决方案:我们可以修改redis的配置,可以设置最少的从节点数量以及缩短主从数据同步的延迟时间,达不到要求就拒绝请求,就可以避免大量的数据丢失。
5、redis分片集群中数据是怎么存储和读取的?
分片集群结构
主从和哨兵可以解决高可用、高并发读的问题,但是依然有两个问题没有解决
- 海量数据存储问题
- 高并发写的问题
使用分片集群可以解决上述问题,分片集群特征:
- 集群中有个多个master,每个master保存不同的数据
- 每个master都可以有多个slave节点
- master之间通过ping监测彼此健康状态
- 客户端请求可以访问集群任意节点,最终都会被转发到正确节点
分片集群结构-数据读写
Redis分片集群引入了哈希槽的概念,Redis集群有16384个哈希槽,每个key通过CRC16校验后对16384取模来决定放置哪个槽,集群的每个节点负责一部分hash槽
Redis是单线程的,但是为什么还那么快?
- redis是纯内存操作,执行速度非常快
- 采用单线程,避免不必要的上下文切换可竞争条件,多线程还要考虑线程安全问题
- 使用I/O多路复用模型,非阻塞IO
解释一下IO多路复用模型?
redis是纯内存操作,执行速度非常快,它的性能瓶颈是网络延迟而不是执行速度,I/O多路复用模型主要就是实现了高效的网络请求。
- 用户空间和内核空间
- 常见的IO模型
- 阻塞IO(Blocking IO)
- 非阻塞IO(Nonblocking IO)
- IO多路复用(IO Multiplexing)
- Redis网络模型
用户空间和内核空间
linux系统中一个进程使用的内存情况划分为两部分:内核空间、用户空间
- 用户空间只能执行受限的命令(Ring3),而且不能直接调用系统资源,必须通过内核提供的接口来访问
- 内核空间可以执行特权命令(Ring0),调用一切系统资源
linux系统为了提高IO效率,会在用户空间和内核空间都加入缓冲区:
- 写数据时,要把用户缓冲数据拷贝到内核缓冲区,然后写入设备
- 读数据时,要从设备读取数据到内核缓冲区,然后拷贝到用户缓冲区
阻塞IO
顾名思义,阻塞IO就是两个阶段都必须阻塞等待:
阶段一:
- 用户进程尝试读取数据(比如网卡数据)
- 此时数据尚未到达,内核需要等待数据
- 此时用户进程也处于阻塞状态
阶段二:
- 数据到达并拷贝到内核缓冲区,代表已就绪
- 将内核数据拷贝到用户缓冲区
- 拷贝过程中,用户进程依然阻塞等待
- 拷贝完成,用户进程解除阻塞,处理数据
可以看到,阻塞IO模型中,用户进程在两个阶段都是阻塞状态
非阻塞IO
顾名思义,非阻塞IO的recvfrom操作会立即返回结果而不是阻塞用户进程。
阶段一:
- 用户进程尝试读取数据(比如网卡数据)
- 此时数据尚未到达,内核需要等待数据
- 返回异常给用户进程
- 用户进程拿到error后,再次尝试读取
- 循环往复,知道数据就绪
阶段二:
- 将内核数据拷贝到用户缓冲区
- 拷贝过程中,用户进程依然阻塞等待
- 拷贝完成,用户进程接触阻塞,处理数据
可以看到,非阻塞IO模型中,用户进程在第一个阶段是非阻塞,第二个阶段是阻塞状态,虽然是非阻塞,但性能并没有得到提高。而且忙等机制会导致cpu空转,cpu使用率暴增。
IO多路复用
IO多路复用:是利用单个线程来监听多个socket,并在某个socket可读、可写时得到通知,从而避免无效的等待,充分利用cpu资源。
阶段一:
- 用户进程调用select,指定要监听的Socket集合
- 内核监听对应的多个socket
- 任意一个或多个socket数据就绪则返回readable
- 此过程中用户进程阻塞
阶段二:
- 用户进程找到就绪的socket
- 依次调用recvfrom读取数据
- 内核将数据拷贝到用户空间
- 用户进程处理数据
IO多路复用是指利用单个线程来同时监听多个socket,并在某个socket可读、可写时得到通知,从而避免无效的等待,充分利用cpu资源。目前的I/O多路复用都是采用epoll模式实现,它会在通知用户进程socket就绪的同时,把已就绪的socket写入用户空间,不需要挨个遍历socket来判断是否就绪,提升了性能。不过监听socket的方法、通知的方式又有多种实现,常见的有:
- select
- poll
- epoll
差异:
- select和poll只会通知用户进程有socket就绪,但不确定具体是哪个socket,需要用户进程逐个遍历socket来确认
- epoll则会在通知用户进程socket就绪的同时,把已就绪的socket写入用户空间
Redis网络模型
redis通过IO多路复用来提高网络性能,并且支持各种不同的多路复用实现,并且将这些实现进行封装,提供了统一的高性能事件库。
就是IO多路复用结合事件的处理器来应对多个socket请求
- 连接应答处理器
- 命令回复处理器,在redis6.0以后,为了提升更好的性能,使用了多线程来处理回复事件
- 命令请求处理器,在redis6.0以后,将命令的转换使用了多线程,增加命令转换速度,在命令执行的时候,依然是单线程