java面试题--Redis
一、说一下redis的持久化机制原理?
- RDB文件: redis database。存储的是某个时间点的数据库内容的快照,是结果。
- redis默认的持久化策略。
- 落盘策略:使用SAVE或者BGSAVE命令。(1)SAVE: 有主线程执行,会阻塞客户端。(2)BGSAVE:会fork出一个子进程,不会出现阻塞问题。子进程使用写时拷贝的策略,子进程页表项指向与父进程页表项相同的物理内存页,所以不会占用大量的内存。 可以使用命令配置自动执行落盘的条件。例如:save 900 2 900秒之内至少有2次修改就执行BGSAVE。
- 恢复策略:自动加载。redis在启动时检测是否存在RDB文件,存在的话,就载入。在载入完成之前,服务对外不可用。
- AOF文件:AppendOnlyFile。存储的执行成功过的命令、参数。是过程。
- 落盘策略:分为三个阶段。(1)命令传播:命令、参数传播到AOF程序。(2)缓存追加:协议文本追加到aof_buf末尾。(3)文件写入和保存:调用faof.c/flushAppendOnlyFile 函数。这个函数执行以下两个工作:1、WRITE:根据条件,将 aof_buf 中的缓存写入到 AOF 文件。2、SAVE:根据条件,调用 fsync 或 fdatasync 函数,将 AOF 文件保存到磁盘中。
- 三种保存模式:(1)AOF_FSYNC_NO:不保存。WRITE 都会被执行, 但 SAVE 会被略过。(2)AOF_FSYNC_EVERYSEC:每一秒钟保存一次(默认)。(3)AOF_FSYNC_ALWAYS:每执行一个命令保存一次。(不推荐)
- AOF重写:名称BGREWRITEAOF
- Redis可以在 AOF体积变得过大时,自动地在后台(Fork子进程)对 AOF进行重写。
- Redis 增加了一个 AOF 重写缓存, 这个缓存在 fork 出子进程之后开始启用,Redis 主进程在接到新的写命令之后, 除了会将这个写命令的协议内容追加到现有的 AOF 文件之外,还会追加到这个缓存中。
-
当子进程在执行 AOF 重写时, 主进程需要执行以下三个工作:
- 处理命令请求。
- 将写命令追加到现有的 AOF 文件中。
- 将写命令追加到 AOF 重写缓存中。
-
当子进程完成 AOF 重写之后, 它会向父进程发送一个完成信号, 父进程在接到完成信号之后, 会调用一个信号处理函数, 并完成以下工作:
-
将 AOF 重写缓存中的内容全部写入到新 AOF 文件中。
-
对新的 AOF 文件进行改名,覆盖原有的 AOF 文件。
-
Redis数据库里的+AOF重写过程中的命令------->新的AOF文件---->覆盖老的。
-
当步骤 1 执行完毕之后, 现有 AOF 文件、新 AOF 文件和数据库三者的状态就完全一致了。
当步骤 2 执行完毕之后, 程序就完成了新旧两个 AOF 文件的交替。
-
- AOF恢复策略:
-
创建一个不带网络连接的伪客户端(fake client):因为Redis的命令只能在客户端上下文中执行,而载入AOF文件时所使用的命令直接来源于AOF文件而不是网络连接,所以服 务器使用了一个没有网络连接的伪客户端来执行AOF文件保存的写命令,伪客户端执行命令 的效果和带网络连接的客户端执行命令的效果完全一样。
-
从AOF文件中分析并读取出一条写命令。
-
使用伪客户端执行被读出的写命令。
-
一直执行步骤2和步骤3,直到AOF文件中的所有写命令都被处理完毕为止。
-
-
- RDB和AOF混合
- 使用RDB头+AOF部分。如果AOF文件以REDIS开头,则先执行RDB的数据快照。再执行AOF命令。
二、说一下对redis缓存雪崩、穿透、击穿的理解?
- (1)缓存穿透:大量请求根本不存在的key。(2)缓存击穿:redis中一个热点key过期。(3)缓存雪崩:redis中大量key集体过期。
- 缓存穿透解决方案:
- 对空值进行缓存,但是过期时间很短。最长不超过5分钟。
- 优点:处理简单
- 缺点:(1)缓存大量不存在的key, 会消耗redis内存。(2)key之前不存在,后来存在了,可能会发生数据不一致问题。
- 使用布隆过滤器:将所有可能存在的数据哈希到一个足够大的BITMAP中,一个一定不存在的数据会被这个bitmap拦截掉,从而避免了对底层存储系统的查询压力。
- 做法:缓存预热时,同步预热布隆过滤器。
- 优点:内存占用较少,没有多余key。
- 缺点:实现复杂,存在误判。
- 网警。
- 对空值进行缓存,但是过期时间很短。最长不超过5分钟。
- 缓存击穿解决方案:
- 加互斥锁:在未命中缓存时,先使用Redis的setNx去设置一个互斥锁,通过加锁避免大量请求访问数据库(只有一个线程可以进行热点数据的重构)。
- 这种方法能保证数据强一致性,但是性能差。
逻辑过期:物理不过期,也就是不设置过期时间。而是在redis的value对象中定义一个过期时间字段。查线程1查询的时候,从Redis中取出判断时间是否过期。如果过期则开通另一个线程2进行数据同步,当前线程1正常返回已过期的数据。 - 这种方法是高可用的,性能优,但是数据一致性差些。
- 加互斥锁:在未命中缓存时,先使用Redis的setNx去设置一个互斥锁,通过加锁避免大量请求访问数据库(只有一个线程可以进行热点数据的重构)。
- 缓存雪崩解决方案:
- 给不同key的TTL添加随机值,将缓存失效时间分散开。比如可以在原有的失效时间基础上增加一个随机值,比如1-5分钟随机。
- 事前:这种方案就是在发生雪崩前对缓存集群实现高可用。如果是使用redis,可以使用主从+哨兵、Redis Cluster来避免redis全盘崩溃的情况。
- 事中:给缓存业务添加降级限流策略。nginx或者spring cloud gateway。比如设置允许一秒2000个请求通过该组件,那么过来5000个请求时,有3000个请求会走限流逻辑,然后去调用我们自己开发的降级组件,比如设置一些默认值,以此来保护最后的MySQL不会被大量请求打死。
- 事后:开启redis持久化机制,尽快恢复缓存集群。
- 给业务添加多级缓存。使用Guava或者Caffeine作为一级缓存,Redis作为二级缓存。
三、redis的集群模式?
- 主从复制模式。
- 一主多从。
- 无法实现故障转移。
- 哨兵模式。
- 一主多从。
- 故障转移步骤:
- master主观下线;
- master客观下线;
- Sentinel集群选举出Leader;
- Leader指定一个新的master。
- Jedis Cluster模式。
- 多主多从。
四、redis与数据库的双写一致性问题
1、概念:当修改了数据库的数据也要同时更新缓存的数据,缓存和数据库的数据要保持一致。但它不是强一致的。
2、延迟双删办法解决双写一致性问题
读操作:缓存命中,直接返回;缓存未命中查询数据库,写入缓存,设定超时时间
写操作:延迟双删
public static void main(String[] args) { String key = "key"; //删除缓存 redisTemplate.delete(key); //更新数据库 updateDb(item); //延迟N秒删除缓存 Thread.sleep(N); redisTemplate.delete(key); }
1、为什么要删除两次缓存?
无论是先删除缓存再删除数据库,还是先删除数据库再删除缓存,多线程并发下,都会造成缓存与数据库双写不一致问题。所以需要删除两次缓存。
2、为什么要延时删除?
因为数据库大都是主从分离的,数据从主库复制到从库需要等一会,等数据都复制到从库后,再执行删除。所以要延时删除,但是延时多长时间是不好控制的。
3、延迟的时间如何确定?
延迟的时间要大于一次写操作的时间。因为如果延迟时间小于写入redis的时间,会导致请求1删除了缓存,请求2还未写入缓存。
在业务程序运行时,统计业务逻辑执行读数据和写缓存的操作时间,以此为基础来进行估算延迟删除的时间。一般都会小于5秒。
3、加锁解决双写一致性问题
缓存中的数据具有读多写少的特点。根据这个特点,添加读写锁,能有效保证强一致性。
- 共享锁:读锁ReadLock,加锁之后,其他线程可以共享读操作。
- 排他锁:独占锁WriteLock,加锁之后,阻塞其他的读写操作。
- 这种方法可以保证数据的强一致性,但是性能低,在写锁时,其他线程都会被阻塞。
-
/** * 读数据 * @param name * @return */ public Item getByName(String name) { //读写锁 RReadWriteLock readWriteLock = redissonClient.getReadWriteLock("ITEM_READ_WRITE_LOCK"); //读之前加读锁,读锁的作用就是等待该lock key释放写锁之后再读 RLock readLock = readWriteLock.readLock(); try { //加锁 readLock.lock(); Item item = (Item) redisTemplate.opsForValue().get("item" + name); if (null != item) { return item; } //查询业务数据 item = new Item("item1", 1); //写入缓存 redisTemplate.opsForValue().set("item" + name, item); return item; } finally { readLock.unlock(); } } /** * 更新数据时,调用该接口。1、更新数据库。2、删除缓存 * @param name */ public void updateByName(String name) { //拿到跟读接口相同的锁 RReadWriteLock readWriteLock = redissonClient.getReadWriteLock("ITEM_READ_WRITE_LOCK"); //写之前加写锁,写锁加锁成功,读锁只能等待 RLock writeLock = readWriteLock.writeLock(); try { //加锁 writeLock.lock(); //更新业务数据 Item item = new Item("item1", 2); try { Thread.sleep(10000); } catch (InterruptedException e) { e.printStackTrace(); } //删除缓存 redisTemplate.delete("item" + name); } finally { writeLock.unlock(); } }
4、异步通知保证数据的最终一致性。
适用于对数据实时一致性要求不高的业务。
5、基于Canal的异步通知。
适用于对数据实时一致性要求不高的业务。
Canal是基于mysql的主从同步来实现的。
二进制日志(binlog)记录了所有的DDL(数据定义语言)语句和DML(数据操纵语言)语句,但不包括数据查询语句(select,show)。