Redis 使用场景及常见面试题
缓存穿透及应对措施
缓存穿透: 查询一个缓存中不存在的数据,并且数据库中也查询不到,从而无法写入缓存,导致每次查询都直接请求到数据库,给数据库造成巨大压力,这种情况大概率遭到了攻击。
解决方案一: 对不存在的key缓存一个空数据,缺点是会导致内存不断增大。
解决方案二: 使用布隆过滤器,在查询缓存之前先去布隆过滤器中查询一遍。
关于布隆过滤器,查看这篇文章,讲解的很细。 布隆(Bloom Filter)过滤器——全面讲解,建议收藏
缓存击穿及应对措施
缓存击穿: 对于一个设置了过期时间的热点key,恰好在某个时间点过期,且大量并发请求同时访问这个key,于是所有请求直接访问数据库,可能会瞬间把DB压垮。
解决方案1: 互斥锁。当缓存失效时,不立即访问db,先使用redis的setnx设置一个互斥锁,当操作成功返回时再进行db操作并写入缓存,否则重试get缓存的方法。
解决方案2: 设置当前key逻辑过期。 大致思路:
- 在设置key的时候,设置一个过期时间字段一起存入缓存中,不给当前key设置过期时间。
- 当查询的时候,从redis中取出数据后判断添加的过期时间字段是否过期。
- 如果过期,则启动新的线程进行数据同步,当前线程正常返回数据,但这个数据可能不是最新的。
两种方案各有利弊。如果选择数据的强一致性,建议使用互斥锁的方案,性能上可能不高,因为锁需要等待,也有可能产生死锁的问题。
如果优先考虑高可用性,性能比较高,建议选择给key添加逻辑过期时间,但是数据同步做不到强一致性。
缓存雪崩及应对措施
缓存雪崩: 在同一时间段大量的缓存key同时失效或者Redis服务宕机,导致大量请求到达数据库,带来巨大压力。
解决方案:
- 给不同的key设置随机过期时间
- 利用redis集群提高服务的可用性(哨兵和集群模式)
- 给缓存业务添加降级限流策略(降级可作为系统的保底策略,适用于穿透、击穿和雪崩)
- 给业务添加多级缓存 (Guava和Caffeine)
如何对数据库的数据和缓存的数据进行同步?(读写一致性)
主要分为延时一致性和强一致性的同步。
延时一致性的业务场景: 比如发表文章、发布商品的场景,不需要实时性很高,像这些场景的业务就可以采用延时一致的解决方案。
强一致性的业务场景: 比如抢券、秒杀的场景,实时性要求非常高,需要知道是否有券或者商品剩余,这些实时性非常高的场景可以使用强一致性的同步方案。
允许延时一致的业务采用的异步通知
- 使用MQ中间件,更新数据后,再更新缓存
- 使用Canal中间件,不需要修改业务代码,伪装为mysql的一个从节点,canal通过读取binlog数据更新缓存
强一致性的业务使用Redisson的读写锁进行同步
- 共享锁: 获取读锁ReadLock,加锁之后,其他线程可以共享读操作
- 排他锁: 也叫独占锁WriteLock,加锁之后,阻塞其他线程读写操作。 通过使用排他锁,就可以保证在写数据的时候不会让其他线程读取数据,避免了脏数据。需要注意的是,获取读写锁的读方法和写方法需要使用同一把锁。
延时双删
如果是写操作,我们先把缓存中的数据删除,然后更新数据库,最后再延时删除缓存中的数据,其中这个延时多久不太好确定(数据库主节点同步到从节点的时间不确定多久),在延时的过程中可能出现脏数据(如果延时的时间小于数据同步的时间,那么读取的数据可能是旧数据),并不能保证强一致性。
Redis的持久化
Redis中有两种持久化方式:RDB和AOF。
RDB(Redis Database Backup file)是一个快照文件,它是把Redis内存中存储的数据保存到磁盘上,方便从RDB的快照文件中恢复数据。
|
|
AOF(Append Only File)是追加文件,当redis操作写命令的时候,都会存储在这个文件中。当需要恢复数据时,重新执行该文件的命令即可恢复数据。
修改redis.config 文件来修改配置
|
|
配置项 | 刷盘时机 | 优点 | 缺点 |
---|---|---|---|
always | 同步刷盘 | 可靠性高,几乎不丢数据 | 性能影响大 |
everysec | 每秒刷盘 | 性能适中 | 最多丢失1秒数据 |
no | 操作系统控制 | 性能最好 | 可靠性较差,可能丢失大量数据 |
bgrewriteaof命令对aof文件执行重写,用最少的命令达到相同效果
|
|
RDB | AOF | |
---|---|---|
持久化方式 | 定时对整个内存做快照 | 记录每次执行的命令 |
数据完整性 | 不完整,两次备份之间会丢失 | 相对完整,取决于刷盘策略 |
文件大小 | 会有压缩,文件体积小 | 记录命令,文件体积很大 |
宕机恢复速度 | 很快 | 慢 |
数据恢复优先级 | 低,因为数据完整性不如AOF | 高,因为数据完整性更高 |
系统资源占用 | 高,大量CPU和内存消耗 | 低,主要是IO磁盘资源,但AOF重写时会占用大量CPU和内存 |
使用场景 | 可以容忍数分钟的数据丢失,追求更快的启动速度 | 对数据安全性要求较高 |
这两种存储方式,哪种恢复的更快呢
RDB是二进制文件,在保存的时候体积也是比较小的,它恢复的较快,但是它有可能会丢失数据,通常也会使用AOF恢复数据,虽然AOF的恢复速度慢,但是它丢失的数据风险较小,在AOF文件中可以设置刷盘策略,使用较多的是每秒批量写入一次命令。
Redis的数据过期策略
惰性删除: key的过期时间到期后,不会自动删除,而是当再次查询时,先检查key是否过期,如果过期则删除,否则直接返回该key
定期清理: 每隔一段时间,对一些key进行检查,删除过期的key。定期清理有两种模式:
- slow模式是定时任务,执行频率默认为10hz,每次不超过25ms,通过修改redis.conf的hz选项调整次数。
- fast模式执行频率不固定,每次事件循环会尝试执行,但每次间隔不低于2ms,每次耗时不超过1ms。 Redis的过期删除策略,是惰性删除和定期删除两种策略配合使用。
Redis的数据淘汰策略
数据的淘汰策略: 当Redis的内存不够用时,再向redis中添加新的key时,那么redis就会按照某种规则将内存中的数据删除掉,这种规则称为内存的淘汰策略。
- noeviction: 不淘汰任何key,但是内存满时不允许写入任何数据,直接报错,这是默认策略。
- volatile-ttl: 对设置了ttl(Time to live 存活时间)的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的访问频率,值越小淘汰优先级越高。
使用建议:
- 优先使用allkeys-lru淘汰策略。充分利用LRU算法的优势,把最常访问的数据保留在缓存中,如果业务有明显的冷热数据区分,建议使用。
- 如果业务中数据访问频率差别不大,没有明显的冷热数据区分,建议使用随机淘汰策略allkeys-random。
- 如果业务中有置顶的需求,可以使用volatile-lru策略,同时置顶数据不要设置过期时间,那么这些数据就会一直不被删除,会淘汰其他设置过期时间的数据。
- 如果业务中有短时高频访问的数据,建议使用allkeys-lfu或volatile-lfu策略。
问:数据库中有1000w数据,Redis只能缓存20w数据,如何保证redis中的数据都是热点数据?
答:使用allkeys-lru(最近最少访问的数据优先淘汰)淘汰策略,留下来的都是经常访问的热点数据。
问:Redis的内存使用完了会发生什么?
答:主要看设置的数据淘汰策略是什么,如果是默认的noevction,内存满后继续添加key会报错。其他的策略都会淘汰某些key后,继续写入。
分布式锁
分布式锁一般是多个进程同步数据时加的锁,而平时代码中的写的lock,是同一个进程下多个线程同步时加的锁。
通常情况下,使用分布式锁的场景有:集群情况下的定时任务、抢券、幂等性场景
Redis分布式锁主要利用Redis的setnx命令,setnx是SET if not exist的简写。
|
|
问:Redis分布式锁如何合理地控制锁的有效时长?
答:1.根据业务执行时间预估(不靠谱);2.给锁续期
Redisson分布式锁的看门狗(Watch Dog)机制
|
|
lua学习教程 https://www.runoob.com/lua/lua-tutorial.html
redisson的分布式锁是可重入的
|
|
通过哈希结构区分锁的不同线程访问记录。field保存线程唯一标识,value保存重入次数。
redisson分布式锁的主从一致性
在哨兵模式等主从同步模式中,如果主节点Master没有成功同步数据到从节点Slave时,有一个请求线程获取了一个锁,与此同时,Master主节点宕机了,然后会从一个从节点Salve中选举一个主节点,又一个请求线程获取了同一个锁。这种场景就会导致多个线程获取同一把锁,失去了锁的意义,可能或导致脏数据。
RedLock(红锁): 不能只在一个redis实例上创建锁,而应该在多个(n/2+1)实例上创建锁,(n/2+1)表示至少一半的实例。
官方不建议使用红锁解决主从不一致问题。因为实现复杂,且高并发下性能差,运维繁琐。
那如何解决这一问题呢? Redis是AP思想(高可用),应该使用CP思想的Zookeeper。
分布式锁FAQ
问:Redis分布式锁如何实现?
答:在redis中提供了一个命令setnx(set if not exists),由于redis是单线程的,用来命令之后,只能有一个客户端对某个key设置值,在没有过期或者删除key时,其他客户端是不能设置这个key的。
问:如何控制redis分布式锁的有效时长呢?
答:redis的setnx指令不好控制这个问题,可以采用redisson框架实现。在redisson中可以手动加锁,并且可以控制锁的失效时间和等待时间,当锁住的业务还没执行完毕时,redisson中引入了看门狗机制,就是说每隔一段时间就检查当前业务是否持有锁,如果持有锁就增加锁的持有时间,当业务执行完成后释放锁就可以了。还有一个好处是,在高并发场景下,如果客户1获取了锁,客户2来了后并不会马上拒绝,它会不断尝试获取锁,如果客户1释放锁之后,客户2会马上持有锁,性能也得到了提升。
问:redisson的分布式锁是可以重入的吗?
答:是可重入的。这样做是为了避免死锁的发生。这个重入其实在内部就是判断是否是当前线程持有的锁,如果是就会计数器加1,如果释放锁就会减1。在存储数据的时候采用的是hash结构,大key可以按照业务进行定制,小key是线程的唯一标识,value是当前线程的重入次数。
问: redisson的分布式锁能解决主从一致性的问题吗?
答: 不能。比如,当线程1加锁成功后,master节点数据会异步复制到从节点slave,此时当前持有redis锁的master节点宕机,slave节点被提升为新的master节点,之前的master节点变成slave节点,假如现在又来了一个线程2,两个线程持有同一把锁,执行业务可能导致脏数据问题。其实Redis采用的是高并发思想(AP),可以考虑使用强一致性思想的Zookeeper(CP)。
Redis集群方案
主从复制
单节点的Redis的并发能力是有上限的,要进一步提高Redis的并发能力,就需要搭建主从集群,实现读写分离。一般是一主多从,主节点负责写,从节点负责读。主节点将数据同步到从节点。
全量同步原理:
- 从节点请求主节点同步数据(replicationId,offset)
- 主节点判断是否是第一次请求,是第一次请求就与从节点同步版本信息(replicationId和offset)
- 主节点执行bgsave,生成rdb文件后,发送给从节点执行
- 在rdb生成执行期间,主节点会以命令的方式记录到缓冲区(一个日志文件repl_baklog)
- 把生成的命令日志文件发送到从节点执行,从而完成全量数据同步
增量同步原理:
- 从节点从主节点请求同步数据,主节点判断不是第一次请求,不是第一次请求就获取从节点的offset值
- 主节点从命令日志文件中获取offset值之后的数据,发送到从节点进行数据同步
哨兵模式
Redis提供了哨兵Sentinel机制来实现主从集群的自动故障恢复。
哨兵的作用:
- 监控:Sentinel会不断检查Master和Slave是否按预期工作;
- 自动故障恢复:如果master故障,Sentinel会自动将一个slave提升为master,当故障实例恢复后以新的master为主;
- 通知:Sentinel充当redis客户端的服务发现来源,当集群发生故障转移时,会将最新信息推送给Redis客户端。
这样,通过哨兵模式就可以实现redis的高并发高可用。
服务状态监控 Sentinel基于心跳机制监测服务状态,每隔1s向集群的实例发送ping命令:
- 主观下线: 如果某个Sentinel节点发现某个实例在规定时间内没有响应,则认为该实例主观下线;
- 客观下线:若超过指定数量(quorum)的sentinel都认为该实例主观下线,则该实例客观下线。quorum值最好超过sentinel实例数量的一半
哨兵选主规则:
- 首先判断主节点与从节点的断开时间长短,如果超过指定值就排除该从节点;
- 然后判断从节点的slave-priority的值,越小优先级越高;
- 如果slave-priority的值一样,则判断slave节点的offset值,越大说明从主节点同步的数据越多,优先级也就越高;
- 最后时判断slave节点的运行id大小,越小优先级越高
Redis集群(哨兵模式)脑裂问题 集群脑裂redis的主节点、从节点和哨兵集群Sentinel处于不同的网络分区,使得sentinel没有能够检测到主节点的心跳,所以就通过选举的方式提升一个从节点作为主节点,这样就存在了两个主节点master,就像大脑裂开了一样,这样会导致客户端还在老的主节点那里写入数据,新节点无法同步数据,当网络恢复后,sentinel会将老的主节点降级为从节点,这时再从新的master同步数据,就会导致数据丢失。
解决办法: Redis中有两个配置参数:
min-replicas-to-write 1
表示最少的slave节点为1个min-replicas-max-lag 5
表示数据复制和同步的延迟不能超过5秒
达不到这两个要求的就拒绝请求,可以避免大量数据丢失。
一般规模的应用使用一主一从+哨兵就可以了,单节点不超过10GB内存,如果Redis内存不足则可以给不同的服务分配独立的Redis主从节点。