Redis
缓存穿透
什么是缓存穿透
缓存击穿是指查询一个不存在的数据,由于数据库中查不到数据,所以不会写入缓存中,这样就导致每次查询都要请求到数据库,这样会导致数据库受到大量的请求,很可能导致数据库挂掉。
解决方案
-
把这个不存在的数据也缓存到缓存中,例如一个byId查询的请求,数据库中不存在id为10000的这条数据,从数据库中查询出来这个id为10000的数据为null,就把这个key为10000值为null的这条数据也存到redis中的缓存中。
- 会导致redis内存增加
-
使用布隆过滤器,推荐使用Redisson–布隆(Bloom Filter)过滤器
【Redisson】Redisson–布隆(Bloom Filter)过滤器
https://blog.youkuaiyun.com/u011397981/article/details/130690257
-
优点
-
时间复杂度低,增加和查询元素的时间复杂为O(N),(N为哈希函数的个数,通常情况比较小)
-
保密性强,布隆过滤器不存储元素本身
-
存储空间小,如果允许存在一定的误判,布隆过滤器是非常节省空间的(相比其他数据结构如Set集合)
-
-
缺点
- 有点一定的误判率,但是可以通过调整参数来降低
- 无法获取元素本身
- 很难删除元素
缓存击穿
什么是缓存击穿
对于设置了过期时间的key,缓存在某个时间点过去的时候,下好这个时间点对于这个key有大量的并发请求过来。这些请求发现缓存过期,一般都会请求数据库,然后把数据库的数据再重新加载到缓存中,这个时候大量的请求可能瞬间会把数据库压垮。
解决方案
-
使用互斥锁:当缓存失效时,不立即去请求数据库,先使用如Redis的SETNX设置一个互斥锁,当操作成功返回时,在进行请求数据库操作并回设缓存,否则重试get缓存的方法
-
设置key的逻辑过期
- 设置key 的时候,设置一个过期时间字段一块存入缓存中,不给当前key设置过期时间
- 当查询的时候,从Redis去取出数据后判断时间是否过期;
- 如果过期则开通另一个线程进行数据同步,当前线程正常返回数据,这个数据可能不是最新的。
利弊:
- 如果需要数据的强一致性,建议使用分布式做的方案,但性能可能没那么高,且有可能存在死锁的问题。
- 如果选择key 的逻辑删除,优点是高可用性,性能比较高,但数据同步这块做不到强一致性。
缓存雪崩
什么是缓存雪崩
缓存雪崩的意思是,设置缓存的时候采用了相同的过期时间,简单一点就是很多个缓存的key设置成了某个时刻同时过期,这样在key过期的时候,请求全都请求到了数据库,数据库的瞬时压力过大而导致了雪崩。
与缓存击穿的区别
- 雪崩是很多key同一时间过期导致的
- 击穿是某一个key导致的
解决方案
再原有的失效时间的技术上添加一个随机值,就比如现在100个key设置了在今天的中午12:00过期,在这个过期时间的基础上添加一个随机值,让这些key的过期时间变成
12:01、12:05、12:03、12:02…
这样每一个缓存的过期时间重复率就会降低很多,就很难发生集体失效的事件。
Redis作为缓存,如何与MySQL数据同步
使用Redisson实现读写锁
-
在读的时候添加共享锁,可以保证读与读之间不互斥,读与写的时候互斥。
-
在更新数据的时候添加排他锁,读写、读读的时候都互斥,这样就能保证在写数据的时候不会让其他线程读数据,这样就避免了脏数据。
读方法和写方法上需要使用同一把锁
排他锁如何保证读写、读读互斥的
排他锁底层使用的也是 SETNX
,它保证了同时只能有一个线程操作锁住的方法
延迟双删
在执行写操作时,我们采用以下步骤确保数据的一致性:
- 删除缓存中的数据:首先,我们需要删除缓存中对应的数据项,以防止读取操作获取到旧数据。
- 更新数据库:其次,进行数据库的更新操作,确保数据的最新状态被持久化存储。
- 延时删除缓存中的数据:最后,我们设置一个延时,再次删除缓存中的数据。这一步骤是为了处理在第一步和第二步之间可能发生的并发读取操作,这些操作可能会重新将旧数据加载到缓存中。 延迟双删策略有效地减少了缓存与数据库之间的数据不一致问题,特别是在高并发的场景下。
- 可能会出现脏数据
Canal服务
采用阿里的Canal组件实现数据同步:不需要更改业务代码,只需部署一个Canal服务。Canal服务把自己伪装成mysql的一个从节点。当mysql数据更新以后,Canal会读取binlog数据,然后再通过Canal的客户端获取到数据,并更新缓存即可。
数据持久化
RDB
RDB是一个快照文件。它是把redis内存存储的数据写到磁盘上。当redis实例宕机恢复数据的时候,可以从RDB的快照文件中恢复数据
- 因为是二进制文件,所以体积小、速度快,但可能丢数据
AOF
AOF的含义是追加文件。当redis执行写命令的时候,都会存储到这个文件中。当redis实例宕机恢复数据的时候,会从这个文件中再次执行一遍命令来恢复数据。
- 速度慢、的那丢数据的风险要小很多,在AOF文件中可以设置刷盘策略。我们当时设置的就是每秒批量写入一次命令。
过期策略
惰性删除
在设置该key过期时间后,我们不去管它。当需要该key时,我们检查其是否过期。如果过期,我们就删掉它;反之,返回该key。
定期删除
每隔一段时间,我们就对一些key进行检查,并删除里面过期的key。
- SLOW模式,是定时任务,执行频率默认为10hz,每次不超过25ms,可以通过修改配置文件redis.conf的hz选项来调整这个次数;
- FAST模式,执行频率不固定,每次事件循环会尝试执行,但两次间隔不低于2ms,每次耗时不超过1ms。
Redis的过期删除策略是:惰性删除 + 定期删除两种策略配合使用。
淘汰策略
noeviction(默认)
不删除任何数据,内部不足时直接报错。
LRU
LRU的意思就是最少最近使用。它会用当前时间减去最后一次访问时间。这个值越大,则淘汰优先级越高。
LFU
LFU的意思是最少频率使用。它会统计每个key的访问频率。值越小,淘汰优先级越高。我们在项目中设置的是allkeys-lru,它会挑选最近最少使用的数据进行淘汰,把一些经常访问的key留在redis中。
Redis分布式锁
在redis中提供了一个命令SETNX
(SET if not exists)。由于redis是单线程的,用了这个命令之后,只能有一个客户端对某一个key设置值。在没有过期或删除key的时候,其他客户端是不能设置这个key的。
分布式锁的有效时长
采用redis的一个框架Redisson实现的。在Redisson中需要手动加锁,并且可以控制锁的失效时间和等待时间。当锁住的一个业务还没有执行完成的时候,Redisson会引入一个看门狗机制。就是说,每隔一段时间就检查当前业务是否还持有锁。如果持有,就增加加锁的持有时间。当业务执行完成之后,需要使用释放锁就可以了。还有一个好处就是,在高并发下,一个业务有可能会执行很快。客户1持有锁的时候,客户2来了以后并不会马上被拒绝。它会自旋不断尝试获取锁。如果客户1释放之后,客户2就可以马上持有锁,性能也得到了提升。
分布式锁是可重入的吗?
是可以重入的。这样做是为了避免死锁的产生,这个重入其实在内部就是判断是否是当前线程持有的锁,如果是当前线程持有的锁就会计数,如果释放锁就会在计数上减一。在存储数据的时候采用的hash结构,大key可以按照自己的业务进行定制,其中小key是当前线程的唯一标识,value是当前线程重入的次数。
主从一致性
ZooKeeper(能解决)
Redis本身就是支持高可用的,要做到强一致性,就非常影响性能,所以,如果有强一致性要求高的业务,建议使用ZooKeeper实现的分布式锁,它是可以保证强一致性的。
Redisson(不能解决)
比如,当线程1加锁成功后,master节点数据会异步复制到slave节点,此时如果当前持有Redis锁的master节点宕机,slave节点被提升为新的master节点,假如现在来了一个线程2,再次加锁,会在新的master节点上加锁成功,这个时候就会出现两个节点同时持有一把锁的问题。
我们可以利用Redisson提供的红锁来解决这个问题,它的主要作用是,不能只在一个Redis实例上创建锁,应该是在多个Redis实例上创建锁,并且要求在大多数Redis节点上都成功创建锁,红锁中要求是Redis的节点数量要过半。这样就能避免线程1加锁成功后master节点宕机导致线程2成功加锁到新的master节点上的问题了。
但是,如果使用了红锁,因为需要同时在多个节点上都添加锁,性能就变得非常低,并且运维维护成本也非常高,所以,我们一般在项目中也不会直接使用红锁,并且官方也暂时废弃了这个红锁。
集群
主从复制
主从同步
单节点Redis的并发能力是有上限的,要进一步提高Redis的并发能力,可以搭建主从集群,实现读写分离。一般都是一主多从,主节点负责写数据,从节点负责读数据,主节点写入数据之后,需要把数据同步到从节点中。
主从同步数据
-
全量同步
全量同步是指从节点第一次与主节点建立连接的时候使用全量同步
- 从节点请求主节点同步数据,其中从节点会携带自己的replication id和offset偏移量。
- 主节点判断是否是第一次请求,主要判断的依据就是,主节点与从节点是否是同一个replication id,如果不是,就说明是第一次同步,那主节点就会把自己的replication id和offset发送给从节点,让从节点与主节点的信息保持一致。
- 在同时主节点会执行
BGSAVE
,生成RDB文件后,发送给从节点去执行,从节点先把自己的数据清空,然后执行主节点发送过来的RDB文件,这样就保持了一致。
-
增量同步
哨兵模式
哨兵模式可以实现主从集群的自动故障恢复,里面就包含了对主从服务的监控、自动故障恢复、通知;如果master故障,Sentinel会将一个slave提升为master。当故障实例恢复后也以新的master为主;同时Sentinel也充当Redis客户端的服务发现来源,当集群发生故障转移时,会将最新信息推送给Redis的客户端,所以一般项目都会采用哨兵的模式来保证Redis的高并发高可用。
Redis分片集群
分片集群主要解决的是海量数据存储的问题,集群中有多个master,每个master保存不同数据,并且还可以给每个master设置多个slave节点,就可以继续增大集群的高并发能力。同时每个master之间通过ping监测彼此健康状态,就类似于哨兵模式了。当客户端请求可以访问集群任意节点,最终都会被转发到正确节点。
存储和读取
Redis 集群引入了哈希槽的概念,有 16384 个哈希槽,集群中每个主节点绑定了一定范围的哈希槽范围,key通过CRC16校验后对16384取模来决定放置哪个槽,通过槽找到对应的节点进行存储。
取值的逻辑是一样的。
Redis集群脑裂
使用Redis的哨兵模式集群
出现原因
有的时候由于网络等原因可能会出现脑裂的情况,就是说,由于Redis master节点和Redis slave节点和Sentinel处于不同的网络分区,使得Sentinel没有能够心跳感知到master,所以通过选举的方式提升了一个slave为master,这样就存在了两个master,就像大脑分裂了一样,这样会导致客户端还在old master那里写入数据,新节点无法同步数据,当网络恢复后,Sentinel会将old master降为slave,这时再从新master同步数据,这会导致old master中的大量数据丢失。
解决方案
关于解决的话,我记得在Redis的配置中可以设置:第一可以设置最少的slave节点个数,比如设置至少要有一个从节点才能同步数据,第二个可以设置主从数据复制和同步的延迟时间,达不到要求就拒绝请求,就可以避免大量的数据丢失。
Redis速度
-
完全基于内存,由C语言编写
-
采用单线程,避免了不必要的上下文切换和竞争条件
-
使用多路I/O服用模型,非阻塞IO
例如:
BGSAVE
和BGREWRITEAOF
都是在后台执行操作,不影响主线程的正常使用,不会产生阻塞。
I/O多路复用
I/O多路复用是指利用单个线程来同时监听多个Socket,并且在某个Socket可读、可写时得到通知,从而避免无效的等待,充分利用CPU资源。目前的I/O多路复用都是采用的epoll模式实现,它会在通知用户进程Socket就绪的同时,把已就绪的Socket写入用户空间,不需要挨个遍历Socket来判断是否就绪,提升了性能。
其中Redis的网络模型就是使用I/O多路复用结合事件的处理器来应对多个Socket请求,比如,提供了连接应答处理器、命令回复处理器,命令请求处理器;
在Redis6.0之后,为了提升更好的性能,在命令回复处理器使用了多线程来处理回复事件,在命令请求处理器中,将命令的转换使用了多线程,增加命令转换速度,在命令执行的时候,依然是单线程