首页 > 数据库 >Redis面试总结(一)

Redis面试总结(一)

时间:2024-11-01 09:49:54浏览次数:6  
标签:总结 缓存 过期 Redis cache db 面试 Redisson

1、除了Redis,你还知道其他分布式缓存方案吗?

redis痛点问题:内存占用高,数据可靠性差,业务维护缓存和存储一致性繁琐。

腾讯开源的Tendis也是分布式高性能KV存储数据库。

Tendis特征:

  • 完全兼容Redis协议,支持绝大多数redis的指令
  • 持久化存储:使用rocksdb作为存储引擎
  • 去中心化架构:类似于redis cluster分布式实现,所有节点通过gossip协议通讯,可指定hashtag来控制数据分布和访问,使用和运维成本极低。
  • 水平拓展:集群支持增删节点,并且数据可以按照slot在任意两节点之间歉意,扩容和缩放过程中对应用韵味人员透明,支持拓展至1000个节点。
  • 故障高可用:自动检测故障节点,当故障发生,slave会提升为master继续对外服务。
  • redis冷热混合存储关键组件:得益于Tendis存储版的设计和内部优化,RedisTendis存储版可以一起工作成为Tendis冷热混合存储。混合存储区非常适用于KV存储场景,并平衡了性能和成本。对于redis,占用大量存储空间的冷数据降冷后可以最多减少80%的成本,同时保证热数据在redis的访问性能。

选择redis原因:经历过多年不断考验、生态优秀、资料全面

为什么要用redis?

  • 访问速度快

​ redis基于内存,内存的访问速度比磁盘快很多,引入Redis后,可以把高频访问的数据放到Redis中,下次直接从内存中读取,速度提升几十倍甚至上百倍。

  • 高并发

​一般像 MySQL 这类的数据库的 QPS 大概都在 4k 左右(4 核 8g) ,但是使用 Redis 缓存之后很容易达到 5w+,甚至能达到 10w+(就单机 Redis 的情况,Redis 集群的话会更高)。

QPS(Query Per Second):服务器每秒可以执行的查询次数。

可以考虑把数据库中的部分数据转移到缓存中去,这样用户的一部分请求会直接到缓存这里而不用经过数据库。

  • 功能全面

​ redis除了缓存之外,还可用于分布式锁、限流、消息队列、延时队列等场景。

2、常见的缓存读写策略有哪些?

包括:旁路缓存、读写穿透、异步缓存写入

1. Cashe Aside Pattern (旁路缓存模式)

缓存读写模式,比较适合读请求比较多的场景

​ 其中服务端需要同时维系 db 和 cache,并且是以db的结果为准。

写:

  • 先更新db
  • 然后直接删除cache。

读:

  • 从cache中读取数据,读取到就直接返回
  • cache中读取不到的话,就从db中读取数据返回
  • 再把数据放到cache中

问题:

在写数据的过程中,可以先删除cache,后更新db吗?

答案:不行,这样会造成db和缓存不一致问题,比如,请求 1 先把 cache 中的 A 数据删除 -> 请求 2 从 db 中读取数据->请求 1 再把 db 中的 A 数据更新

在写数据的过程中,先更新db,后删除cache就没有问题了吗?

答案:理论上来说还是有可能出现数据不一致问题,不过概率非常小,因为缓存写入的速度比数据库的写入速度快很多。简单理解为,请求 1 从 db 读数据 A-> 请求 2 更新 db 中的数据 A(此时缓存中无数据 A ,故不用执行删除缓存操作 ) -> 请求 1 将数据 A 写入 cache

Cashe Aside Pattern 存在缺陷:

  • 首次请求数据一定不在cache问题

解决办法:将热点数据提前放到cache中

  • 写操作比较繁琐的话,导致cache中的数据会被频繁删除,这样会影响缓存命中率。

解决办法:

数据库和缓存数据强一致场景:更新 db 的时候同样更新 cache,不过我们需要加一个锁/分布式锁来保证更新 cache 的时候不存在线程安全问题。

可以短暂地允许数据库和缓存数据不一致的场景:更新 db 的时候同样更新 cache,但是给缓存加一个比较短的过期时间,这样的话就可以保证即使数据不一致的话影响也比较小。

2. Read/Write Through Pattern (读写穿透)

服务端把cache视为主要数据存储,从中读取数据并将数据写入其中。cache服务负责将此数据读取和写入db,从而减轻了应用程序的职责。

写:

  • 先查 cache,cache 中不存在,直接更新 db。
  • cache 中存在,则先更新 cache,然后 cache 服务自己更新 db(同步更新 cache 和 db)。

读:

  • 从 cache 中读取数据,读取到就直接返回 。
  • 读取不到的话,先从 db 加载,写入到 cache 后返回响应。

3. Write Behind Pattern (异步缓存写入)

Read/Write Through 是同步更新 cache 和 db,而 Write Behind 则是只更新缓存,不直接更新 db,而是改为异步批量的方式来更新 db。

面临问题:比如 cache 数据可能还没异步更新 db 的话,cache 服务可能就挂掉了。

应用:消息队列中消息的异步写入磁盘、MySQL 的 Innodb Buffer Pool 机制都用到了这种策略。

Write Behind Pattern 下 db 的写性能非常高,非常适合一些数据经常变化又对数据一致性要求没那么高的场景,比如浏览量、点赞量。

3、Redis除了做缓存,还能做什么?

  • 分布式锁
  • 限流:通过Redis + Lua 脚本的方式实现限流
  • 消息队列:支持消息持久化、ACK机制
  • 延时队列:Redisson内置了延时队列
  • 分布式Session:利用String 或 Hash数据类型
  • 复杂业务业务场景:通过Redis 以及 Redis拓展(Redisson)提供的数据结构。业务:通过Bitmap统计活跃用户、通过Sorted Set 维护排行榜

4、如何基于Redis实现分布式锁?

SETNX如果key不存在,设置key的值,如果key已存在,SETNX啥也不做。

SETNX命令加锁:如果对应的key不存在则加锁,如果存在那么获取锁失败

基于Lua脚本释放锁:判断key(锁)对应的value是否相等 --> 执行DEL命令释放锁,如果不相等,那么释放锁失败。

存在问题:释放锁逻辑的程序突然挂掉,可能会导致锁无法被释放,造成共享资源无法再被其他线程/进程访问。

5、为什么要给锁设置一个过期时间?

解决问题:避免锁无法被释放 --> 给这个key(锁)设置一个过期时间。

127.0.0.1:6379> SET lockKey uniqueValue EX 3 NX
OK

lockKey:加锁的锁名;

uniqueValue:能够唯一标识锁的随机字符串;

NX:只有当 lockKey 对应的 key 值不存在的时候才能 SET 成功;

EX:过期时间设置(秒为单位)EX 3 标示这个锁有一个 3 秒的自动过期时间。与 EX 对应的是 PX(毫秒为单位),这两个都是过期时间设置。

存在:共享资源操作时间大于过期时间,就会出现锁提前过期的情况,如果锁的时间设置过长,会影响程序性能。

6、如何合理的设置锁的过期时间?

Redisson(Java语言Redis客户端) 中的分布式锁自带自动续期机制,使用起来非常简单,原理也比较简单,其提供了一个专门用来监控和续期锁的 Watch Dog( 看门狗),如果操作共享资源的线程还未执行完成的话,Watch Dog 会不断地延长锁的过期时间,进而保证锁不会因为超时而被释放。

//默认 30秒,支持修改
private long lockWatchdogTimeout = 30 * 1000;

public Config setLockWatchdogTimeout(long lockWatchdogTimeout) {
    this.lockWatchdogTimeout = lockWatchdogTimeout;
    return this;
}
public long getLockWatchdogTimeout() {
   return lockWatchdogTimeout;
}

主要逻辑

private void renewExpiration() {
         //......
        Timeout task = commandExecutor.getConnectionManager().newTimeout(new TimerTask() {
            @Override
            public void run(Timeout timeout) throws Exception {
                //......
                // 异步续期,基于 Lua 脚本
                CompletionStage<Boolean> future = renewExpirationAsync(threadId);
                future.whenComplete((res, e) -> {
                    if (e != null) {
                        // 无法续期
                        log.error("Can't update lock " + getRawName() + " expiration", e);
                        EXPIRATION_RENEWAL_MAP.remove(getEntryName());
                        return;
                    }

                    if (res) {
                        // 递归调用实现续期
                        renewExpiration();
                    } else {
                        // 取消续期
                        cancelExpirationRenewal(null);
                    }
                });
            }
         // 延迟 internalLockLeaseTime/3(默认 10s,也就是 30/3) 再调用
        }, internalLockLeaseTime / 3, TimeUnit.MILLISECONDS);

        ee.setTimeout(task);
    }

默认情况下,没过10s看门狗就会执行续期操作,将锁的超时时间设置为30s。

protected CompletionStage<Boolean> renewExpirationAsync(long threadId) {
    return evalWriteAsync(getRawName(), LongCodec.INSTANCE, RedisCommands.EVAL_BOOLEAN,
            // 判断是否为持锁线程,如果是就执行续期操作,就锁的过期时间设置为 30s(默认)
            "if (redis.call('hexists', KEYS[1], ARGV[2]) == 1) then " +
                    "redis.call('pexpire', KEYS[1], ARGV[1]); " +
                    "return 1; " +
                    "end; " +
                    "return 0;",
            Collections.singletonList(getRawName()),
            internalLockLeaseTime, getLockName(threadId));
}

调用renewExpirationAsync()方法实现锁的异步续期

protected CompletionStage<Boolean> renewExpirationAsync(long threadId) {
    return evalWriteAsync(getRawName(), LongCodec.INSTANCE, RedisCommands.EVAL_BOOLEAN,
            // 判断是否为持锁线程,如果是就执行续期操作,就锁的过期时间设置为 30s(默认)
            "if (redis.call('hexists', KEYS[1], ARGV[2]) == 1) then " +
                    "redis.call('pexpire', KEYS[1], ARGV[1]); " +
                    "return 1; " +
                    "end; " +
                    "return 0;",
            Collections.singletonList(getRawName()),
            internalLockLeaseTime, getLockName(threadId));
}

使用Redisson分布式可重入锁RLock为例来说明如何使用Redisson实现分布式锁:

// 1.获取指定的分布式锁对象
RLock lock = redisson.getLock("lock");
// 2.拿锁且不设置锁超时时间,具备 Watch Dog 自动续期机制
lock.lock();
// 3.执行业务
...
// 4.释放锁
lock.unlock();

只有未指定锁超时时间,才会使用到 Watch Dog 自动续期机制。

// 手动给锁设置过期时间,不具备 Watch Dog 自动续期机制
lock.lock(10, TimeUnit.SECONDS);

7、如何基于Redis实现延时任务?

场景:订单在 10 分钟后未支付就失效、红包 24 小时未被查收自动退还

两种方案:1. Redis过期时间监听;2. Redisson内置的延时队列

Redis 过期事件监听的存在时效性较差、丢消息、多服务实例下消息重复消费等问题,不被推荐使用。

Redisson 内置的延时队列具备下面这些优势:

  1. 减少了丢消息的可能:DelayedQueue 中的消息会被持久化,即使 Redis 宕机了,根据持久化机制,也只可能丢失一点消息,影响不大。也可以使用扫描数据库的方法作为补偿机制。
  2. 消息不存在重复消费问题:每个客户端都是从同一个目标队列中获取任务的,不存在重复消费的问题。

8、Redisson延迟队列原理是什么?有什么优势?

Redisson 是一个开源的 Java 语言 Redis 客户端,提供了很多开箱即用的功能,比如多种分布式锁的实现、延时队列。

借助 Redisson 内置的延时队列 RDelayedQueue 来实现延时任务功能。

Redisson 的延迟队列 RDelayedQueue 是基于 Redis 的 SortedSet 来实现的。SortedSet 是一个有序集合,其中的每个元素都可以设置一个分数,代表该元素的权重。Redisson 利用这一特性,将需要延迟执行的任务插入到 SortedSet 中,并给它们设置相应的过期时间作为分数。

Redisson 使用 zrangebyscore 命令扫描 SortedSet 中过期的元素,然后将这些过期元素从 SortedSet 中移除,并将它们加入到就绪消息列表中。就绪消息列表是一个阻塞队列,有消息进入就会被监听到。这样做可以避免对整个 SortedSet 进行轮询,提高了执行效率。

跟 Redisson 内置的延时队列相比,消息队列可以通过保障消息消费的可靠性、控制消息生产者和消费者的数量等手段来实现更高的吞吐量和更强的可靠性,实际项目中首选使用消息队列的延时消息这种方案。

标签:总结,缓存,过期,Redis,cache,db,面试,Redisson
From: https://blog.csdn.net/m0_74119287/article/details/143374733

相关文章

  • 今日总结
    《程序员修炼之道》深度探索之旅的后续感悟在完成了《程序员修炼之道》阅读,我仿佛经历了一次心灵的洗礼,而接下来的内容则引领我进入了更为广阔的编程世界,让我对“从小工到专家”的旅程有了更加全面且深刻的体悟。书中后半部分(为避免直接使用“后半本书”的表述,以下均用“后续内......
  • 10.31每日总结:《程序员修炼之道》读后感3
    读完《程序员修炼之道:从小工到专家》,我对编程这一职业有了更深刻的认识。这本书强调了程序员应具备的各种品质和技能。它提醒我们要注重代码的可读性和可维护性,这不仅利于自己日后对代码的修改,也方便团队中的其他成员理解和协作。就像建造一座坚固的大厦,清晰的代码结构是坚实的基......
  • 大模型算法面试题总结
    更多面试题总结,请移步至​https://i.afbcs.cn/naPbNY​1.什么是大型语言模型(LLMs)以及它们的工作原理是什么?大型语言模型(LLMs)是设计用来理解、处理和生成类似人类文本的高级人工智能系统。例子包括GPT(生成预训练变换器)、BERT(来自变换器的双向编码器表示)、Claude和Llama。这些......
  • Java面试题中高级进阶(JVM篇Java内存)
    前言本来想着给自己放松一下,刷刷博客,突然被几道面试题难倒!说说Java内存结构?说说对象分配规则?描述一下JVM加载class文件的原理机制?似乎有点模糊了,那就大概看一下面试题吧。好记性不如烂键盘***12万字的java面试题整理***Java内存结构方法区和堆是所有线程共享的内存区域;而j......
  • Java常见面试题之事务
    博主介绍上海交大毕业,大厂资深Java后端工程师《Java全套学习资料》作者专注于系统架构设计和高并发解决方案和面试辅导阿里云开发社区乘风者计划专家博主@author[vx]vip1024p(备注java)ACID特性A:原子性,Atomictiy,事务是最小的执行单位,不允许分割,事务的原子性确......
  • 【面试题系列Java】Java基础面试题
    对于Java开发工程师而言,掌握Java基础、数据库、框架、Java虚拟机、Java并发编程等知识是必不可少的。以下是一些常见的Java开发工程师面试题及答案,供大家参考。博主介绍上海交大毕业,大厂资深Java后端工程师《Java全套学习资料》作者专注于系统架构设计和高并发解决方......
  • fetch 与 xmlHttpRequest 请求总结
    文章目录fetch、XMLHttpRequest、ajax简要介绍fetch与xmlHttpRequest比较fetch、XMLHttpRequest、ajax简要介绍FetchAPI概述Fetch是一种现代的JavaScriptAPI,用于在浏览器中进行网络请求。它提供了一种更灵活、更强大的方式来获取资源,相比传统的XMLHttpRe......
  • C语言复习总结超详细版(1)小白转身即变 有实例超级详细
    废话不多说直接开整注:本博文超级详细但是还是适合有C语言基础的观看 耗时很久,内容不会有问题但是 ⚠️字体晦涩望见谅引子第一个C语言程序#include<stdio.h>intmain(){printf("Hello,LJY!\n");return0;} main函数每个C语⾔程序不管有多少⾏代码......
  • 财务结算-博客总结
    [StreamliningFinancialPrecision:Uber’sAdvancedSettlementAccountingSystem](https://www.uber.com/en-IN/blog/ubers-advanced-settlement-accounting-system/?uclick_id=a98d832d-96db-41cd-b53f-2911beb06f3f)   总结:这张图片是一个关于信用卡支付处理价值......
  • 代码随想录之链表刷题总结
    目录1.链表理论基础2.移除链表元素3.设计链表4.翻转链表5.两两交换链表中的节点6.删除链表中的第N个节点7.链表相交8.环形链表1.链表理论基础链表是一种通过指针串联在一起的线性结构,每一个节点由两部分组成,一个是数据域一个是指针域(存放指向下一个节点的指针),最后......