RedisInterview
缓存穿透
缓存穿透指的是部分请求中的数据,在Redis中不存在,而且在MySQL中同样不存在这部分数据,导致这部分请求将始终穿透Redis打到MySQL上,会导致MySQL压力增加
解决办法:
- 过滤非法请求,将明显不符合规范的请求过滤掉;
- 对于不存在的这部分数据在Redis中以空值进行存储,但需要注意在MySQL数据更新时将Redis数据更新,保证数据一致性;
- 使用bloom过滤器,在查询之前判断该请求的数据是否存在,不存在则不用请求MySQL
缓存雪崩
缓存雪崩指的是同一时间大面积key同时失效,导致此时请求大面积都打到了MySQL上,直接增加了数据库的负载,也有可能是由于Redis服务器宕机导致的,这种情况就需要高可用的Redis集群架构
解决办法:
- 缓存时间随机分布,避免大量key同时过期
- 引用二级缓存,即本地缓存
- 预加载缓存,缓存即将过期时提前加载新的缓存数据
- 限流和熔断,确保不会有大量请求直接打到数据库
缓存击穿
缓存击穿指的是在高并发环境下,某个热点key突然失效,导致大量请求同时打到数据库,导致数据库负载骤增。
解决办法:
- 使用互斥锁,即在缓存失效时,只允许一个线程去重加载缓存数据,其余线程等待,避免大量请求同时打到数据库
- 热点数据永不过期,对于某些可提前预知的热点数据设置永不过期
- 使用本地缓存,减轻数据库压力
- 监控告警,熔断限流
Redis热点key
Redis热点key(Hotspot Key)是指在Redis缓存中经常被访问的某个特定的缓存键。这些热点key可能是数据、计数器、频繁访问的用户信息等等,而访问热点key的频率远高于其他缓存数据。处理Redis热点key是很重要的,因为它们可能成为系统的性能瓶颈,导致Redis服务器压力过大,从而影响系统的稳定性和性能。
以下是一些处理Redis热点key的常见策略:
- 分片:将相同类型的热点key分散到多个Redis实例或集群中,以减轻单个实例的压力。这样可以提高并发处理能力。
- LRU淘汰:使用LRU(最近最少使用)或其他缓存淘汰算法,定期或根据策略删除一些不常访问的热点key,以便腾出内存空间。
- 缓存预热:在系统启动或低峰期,提前加载热点数据到Redis缓存中,以避免在高峰期间突然的大量请求。
- 二级缓存:使用二级缓存,例如本地缓存(如Guava Cache)来减轻Redis的压力。在本地缓存中,可以缓存一部分热点数据,以加快访问速度。
- 限流:通过限制对热点key的访问频率,控制同时对同一key的请求数量。
- 使用ZSET来分散请求:如果有一个热点key用于存储某种排名,可以使用ZSET(有序集合)来分散对该排名的读取请求。
- 数据分片:对于某些数据,可以将其按一定规则分片存储在多个key中,从而分散访问热点。
- 定时刷新:周期性地更新或刷新热点key中的数据,以减轻热点key压力。
在处理Redis热点key时,需要根据具体应用场景和数据特点来选择合适的策略。一种通用的策略可能无法适应所有情况,因此需要根据需求和性能分析来选择和调整策略。
Redis大key
Redis大key指的是占用内存较大的key,但并不是指key很大,而是该key指向的value很大,导致内存的过度使用,也有可能是存储了大量的元素在集合(set/zset/hash)中。
可能导致以下问题:
-
内存消耗:大key会占用大量的内存,这可能导致Redis服务器的内存不足。
-
性能下降:操作大key可能会导致性能下降,因为Redis是单线程的,处理大key可能需要更长的时间。
-
备份和持久化问题:在备份和持久化时,大key可能导致备份文件变得庞大,增加了备份和恢复的时间。
-
网络传输延迟:大key的读写操作可能会导致网络传输延迟,尤其是在集群部署中。
解决策略:
-
拆分大key:如果可能的话,将大key拆分成多个小key,以减小单个键的大小。
-
优化数据结构:考虑使用适当的数据结构,以减小数据的大小。例如,使用压缩数据结构或将一些数据存储在多个小键中。
-
限制数据大小:在应用程序中限制数据写入Redis的大小。可以设置最大值,以防止数据过大。
-
监控和清理:定期监控Redis中的大key,然后执行清理操作,删除不再需要的大key。
-
使用分布式缓存:考虑使用多个Redis实例或分布式缓存系统,以分担大key的内存压力。
Redis&MySQL数据一致性
延时双删:
在数据更新时,首先删除Redis中的缓存数据,然后更新MySQL中的数据,数据更新后延时一段时间后再次将Redis中的缓存数据删除;
为了防止删除失败,还可以将删除Redis缓存的请求作为消息发送出去或者多次进行重试操作;
其次可以通过监听binlog的变更,然后删除Redis中的缓存数据并进行重载。
Redis持久化
RDB
在指定的时间间隔内将内存中的所有数据集快照写入磁盘,也就是行话讲的 Snapshot 快照,它执行的是全量快照,它恢复时是将快照文件直接读到内存里。
Redis 会单独创建(fork)一个子进程来进行持久化,会先将数据写入到一个临时文件中,待持久化过程都结束了,再用这个临时文件替换上次持久化好的文件。整个过程中,主进程是不进行任何 IO 操作的,这就确保了极高的性能,如果需要进行大规模数据的恢复,且对于数据恢复的完整性不是非常敏感,那 RDB 方式是比较高效的。
RDB 的缺点是最后一次持久化后的数据可能丢失。
Redis 不是单进程的吗?
Redis 使用操作系统的多进程 COW(Copy On Write) 机制来实现快照持久化(在执行快照的同时,正常处理写操作), fork 是类 Unix 操作系统上创建进程的主要方法。COW(Copy On Write)是计算机编程中使用的一种优化策略。 fork 的作用是复制一个与当前进程一样的进程。新进程的所有数据(变量、环境变量、程序计数器等)数值都和原进程一致,但是是一个全新的进程,并作为原进程的子进程。子进程读取数据,然后序列化写到磁盘中。
AOF
以日志的形式来记录每个写操作,将 Redis 执行过的所有写指令记录下来(读操作不记录),只许追加文件但不可以改写文件,Redis 启动之初会读取该文件重新构建数据,也就是「重放」。换言之,Redis 重启的话就根据日志文件的内容将写指令从前到后执行一次以完成数据的恢复工作。
说到日志,我们比较熟悉的是数据库的写前日志(Write Ahead Log, WAL),也就是说,在实际写数据前,先把修改的数据记到日志文件中,以便故障时进行恢复(DBA 们常说的“日志先行”)。
不过,AOF 日志正好相反,它是写后日志,“写后”的意思是 Redis 是先执行命令,把数据写入内存,然后才记录日志
rewrite(AOF 重写)
- 是什么:AOF 采用文件追加方式,文件会越来越大,为了避免出现这种情况,新增了重写机制,当 AOF 文件的大小超过所设定的阈值时,Redis 就会启动 AOF 文件的内容压缩,只保留可以恢复数据的最小指令集,可以使用命令
bgrewriteaof
,这个操作相当于对 AOF 文件“瘦身”。在重写的时候,是根据这个键值对当前的最新状态,为它生成对应的写入命令。这样一来,一个键值对在重写日志中只用一条命令就行了,而且,在日志恢复时,只用执行这条命令,就可以直接完成这个键值对的写入了。 - 重写原理:AOF 文件持续增长而过大时,会 fork 出一条新进程来将文件重写(也是先写临时文件最后再rename),遍历新进程的内存中数据,转换成一条条的操作指令,再序列化到一个新的 AOF 文件中。 PS:重写 AOF 文件的操作,并没有读取旧的 AOF 文件,而是将整个内存中的数据库内容用命令的方式重写了一个新的 AOF 文件,这点和快照有点类似。
- 触发机制:Redis 会记录上次重写时的 AOF 大小,默认配置是当 AOF 文件大小是上次 rewrite 后大小的一倍且文件大于 64M 时触发 我们在客户端输入两次
set k1 v1
,然后比较bgrewriteaof
前后两次的 appendonly.aof 文件(先要关闭混合持久化)
Redis为何采用哈希槽而不是一致性hash
一致性Hash可以解决传统hash取模分片造成的扩容问题(即在扩容过程中存在大量数据迁移问题)。
采用一致性hash算法进行数据分片时会涉及两个核心阶段:
- 完成key到slot槽位之间的映射
- 需要完成slot槽位到redis node节点之间的映射。
第一阶段,对2^32取模,相当于2^32个槽位,通过这个槽位的计算可以确定key=>slot之间的映射关系。
第二阶段,需要完成slot槽位到redis node节点之间的映射,那该如何完成slot槽位到node节点之间的映射呢?
这里就需要用到Hash槽位环,把一致性hash算法对2^32个slot槽位虚拟成一个圆环,环上对应0~2^32刻度。
如何完成slot槽位和node节点映射?
假设有4个node节点,可以将2^32个槽位分为4段,每一个node节点负责存储一个slot片段。
如何对每一个key进行路由?
第一步,进行slot槽位计算,对key进行hash运算后被2^32取模得到slot槽位;第二步,在hash槽位环上顺时针寻找最近的redis节点。
一致性hash的数据不平衡问题(数据倾斜)
如果在一致性hash算法过程中某个node节点宕机,可能会导致该宕机节点的数据迁移到下一个节点,导致数据不均衡。此时就需要引入虚拟节点的概念来解决这个问题。
虚拟节点
虚拟节点可以理解额为逻辑节点,不是物理节点。假设在hash环上引入32个虚拟节点,则需要额外增加一次虚拟节点到物理节点的映射,那么此时如果某个节点宕机,就需要把该节点负责的逻辑节点中的数据二次分配到其他几个物理节点就行了。
回到最初的问题,Redis为什么使用hash槽而不是一致性Hash?
一致性hash存在两大优点,很少的数据迁移和很少的数据倾斜;但是由于Redis集群架构特点的原因,一个是去中心化,一个是方便伸缩
Redis哈希槽和一致性哈希,总体的流程是差不多的,都是两个阶段
- 第一阶段,Hash取模
- 第二阶段,node映射
第一阶段都是hash之后取模分片。分为两步: - 第一步:hash,hash算法确保数据分布均匀。Redis cluster采用crc16哈希算法
- 第二步:取模。就是槽位的数量,redis把数据分为16484个槽位,一致性hash分为2^32个槽位。
为什么redis cluster不采用2^32个槽位?主要是考虑节点数在1000的规模以下,而且使用gossip去中心一致性协议,数据包不能太大,16k个二进制位2k字节已经很大了
第二阶段是:node映射 - 一致性hash是哈希环顺时针映射
- redis哈希槽是静态映射
一致性哈希环顺时针映射优先考虑的是:如何实现最少的节点数据发生数据迁移。一致性hash环上,只有被干掉的节点顺时针方向最近的一个节点设计数据迁移,其他节点不涉及数据迁移。
redis cluster哈希槽静态映射优先考虑的是:如何实现数据的均匀。redis cluster各个节点都会参与数据迁移,优先保证各个节点承担相同的访问压力。
同时,redis cluster哈希槽静态映射还有一个优点,手动迁移。redis cluster可以自动分配,也可以根据节点的性能手动调整slot的分配。