当前位置: 首页 > news >正文

JAVA后端面试笔记(三)

《关于redis的面试题》

1.Redis-数据类型及其使用场景

1.1 string(字符串)

key是String,value不仅可以是String,也可以是数字

使用场景:

  • 缓存数据,提升查询效率
  • 计算器 - 全局ID(保证唯一性)
  • 分布式session
  • 限速

1.2 hash(哈希)

使用场景

用一个对象来存储用户信息,商品信息,订单信息等等。

key=JavaUser293847
value={"id": 1,"name": "SnailClimb","age": 22,"location": "Wuhan, Hubei"
}

1.3 list(列表)

list 就是链表,Redis list 的实现为一个双向链表。可以对列表两端插入(pubsh)和弹出(pop),还可以获取指定范围的元素 列表、获取指定索引下表的元素等,列表是一种比较灵活的数据结构,它可以充当栈和队列的角色

key=sss
value={ hah, sonw, wek}

使用场景:

  • 存储用户url的权限-缓存list结构数据
  • ...

1.4 set(集合)

与列表类似,不同的是集合中不允许重复的元素

Set中的元素是无序的,不能通过索引下标获取元素

Set支持集合内的增删改查,同时还支持多个集合取交集、并集、差集

Set是通过哈希表实现的,所以添加、删除、查找的复杂度都是O(1)

Set提供了某个成员是否在set集合内的重要接口,这个也是list不能提供的

使用场景:

1.标签(tag):如一个用户对娱乐、体育感兴趣,另一个对新闻感兴趣,这些兴趣就是标签,可获得共同爱好(取交集)的标签。

2.spop/srandmember=random item(生成随机数,比如抽奖)

两者的区别在于spop是随机返回数据并删除,需修改集合(如任务分配、抽奖)

srandmember随机访问(如推荐、采样)

3.sadd + sinter :社交需求使用场景

4.sinterstore key1 key2 key3 求交集

1.5 zset(有序集合)

和set相比,zset增加了一个权重参数score,集合内的元素按照权重进行有序排列,这个参数在添加/修改元素时可以指定,每次指定后,zset集合会自动重新排序

使用场景:适用于需要排序的场景,例如:排行榜(按照时间、播放量、获取的赞数进行排序)

1.6 bitmap (位图)

使用场景:高效地统计大量数据

2.Redis为什么性能高,速度快?

2.1 Redis为什么速度很快

Redis 提供了一些基本且高效的数据结构

数据存放在内存,内存的读写速度是磁盘(数据库)的一百倍左右

Redis是单线程的,但内部使用了IO多路复用提高性能

2.2 Redis单线程的优缺点

单进程单线程,避免多线程竞争锁的性能消耗,避免多线程切换的CPU消耗

缺点是无法发挥多核CPU性能,性能瓶颈主要在网络IO操作

3.Redis持久化AOF,RDB区别

redis默认持久化配置是RDB

RDB是一个紧凑压缩二进制文件,代表Redis在某个时间点上的数据快照,适用备份、全量复制等场景

AOF以独立日志的方式记录每次写命令,重启时再重新执行AOF文件达到恢复数据,实现持久化数据

3.1 AOF与RDB的区别

AOF支持持久化,RDB不支持持久化,频繁执行成本高

AOF可用性好,是增量操作;RDB可用性查,文件过大会造成服务器的堵塞

AOF兼容性好,RDB兼容性差

AOF易读性好,aof文件有序地保存对数据库执行的所有写入操作;RDB易读性差

AOF恢复大数据的速度 < RDB(其文件比aof小)

4.Redis-重写机制(减小AOF文件大小)

重写后AOF文件变小的原因

  • 进程内过期的数据不再写入文件
  • 会删除旧的AOF文件中的无效命令
  • 将多条写命令合并为一个

5.Redis有哪些原子命令?

  • Redis所有单个命令都是原子性

事务机制:通过MULTI和EXEC实现多个命令的原子性执行

6.Redis-缓存穿透-含义/原因/解决方案

6.1 含义

缓存穿透:指大量查询一个根本不存在的数据,缓存层没有命中,然后去查数据库(持久层),在请求过多的时候会导致数据库压力过大,严重会击垮数据库。

正常的操作流程

客户端请求--->缓存层(miss)----->存储层(miss)--->返回

缓存穿透造成的后果:将导致不存在的数据每次请求都要到存储层查询,失去了缓存保护后端存储的意义。缓存穿透问题可能会使后端数据库负载加大,可能造成数据库宕机。

可以在程序中分别统计总调用数、缓存层命中数、存储层命中数,如果发现大量存储层空命中,可能就是出现了缓存穿透问题。

6.2 缓存穿透的原因

恶意攻击、爬虫、自身业务逻辑问题

6.3 缓存穿透的解决方案

方案1:缓存空对象

存储层不命中后,仍然将空对象保留到缓存层中,之后再访问这个数据将会从缓存中获取,这样就保护了后端数据源。

缓存空对象的两个问题

①内存占用增加,解决方案:设置较短的过期时间,自动删除

②缓存层和存储层的会有一段时间窗口的数据不一致,可能会对业务有一定影响

解决方案:将数据插入到存储层时,清除掉缓存层中的空对象

String get(String key) {// 从缓存中获取数据String cacheValue = cache.get(key);// 缓存为空if (StringUtils.isBlank(cacheValue)) {// 从存储中获取String storageValue = storage.get(key);cache.set(key, storageValue);// 如果存储数据为空, 需要设置一个过期时间(300秒)if (storageValue == null) {cache.expire(key, 60 * 5);}return storageValue;} else {// 缓存非空return cacheValue;}
}

方案2:布隆过滤器

在访问缓存层和存储层之前,将存在的key用布隆过滤器提前保存起来,做第一层拦截

查询的流程:

①如果key在布隆过滤器中,则去查询缓存

   如果查询到,则返回缓存中的数据

   如果没查询到,则穿透到数据库中查询

②如果key不在布隆过滤器,直接返回

7.Redis-缓存击穿-含义/原因/解决方案

7.1 含义

缓存击穿是指一个热点Key,在不停的扛着大并发,大并发集中对这一个点进行访问,当这个Key在失效的瞬间,持续的大并发穿破缓存,直接请求数据库,就像在一个完好无损的桶上凿开了一个洞

7.2 解决方案

①设置热点数据永不过期

②互斥锁

8.Redis-缓存雪崩-含义/原因/解决方案

8.1 含义

指的是缓存层宕掉后,流量会像奔逃的野牛一样,打向后端存储。

8.2 解决方案

保证Redis服务高可用

9.Redis的发布订阅机制及其使用场景

9.1 使用场景

1.业务解耦

聊天室、公告牌、服务之间利用消息解耦都可以使用发布订阅模式

2.框架应用

Redisson的分布式锁的实现就采用了发布订阅模式:获取锁时,若获取不成功则订阅释放锁的消息,在收到释放锁的消息前阻塞,收到释放锁的消息后再去循环获取锁。

3.异步处理 -- 秒杀功能

  1. 秒杀之前,将产品的库存从数据库同步到Redis
  2. 秒杀时,通过lua脚本保证原子性
    1. 扣减库存
    2. 订单数据通过Redis的发布订阅功能发布出去
    3. 返回1(表示成功)
  3. 订单数据的Redis订阅者处理订单数据

9.2 命令

Redis主要提供了发布消息、订阅频道、取消订阅以及按照模式订阅和取消订阅等命令。

9.2.1  发布消息

publish channel message

9.2.2 订阅消息

subscribe channel [channel ...]

10.Redis内存回收机制是怎样的?(回收 + 淘汰)

Redis 中内存的释放主要分为两类:内存回收内存淘汰

  • 内存回收: 将过期的 key 清除,以减少内存占用
  • 内存淘汰: 在内存使用达到上限(max_memory), 按照一定的策略删除一些键,以释放内存空间

10.1 内存回收

过期策略:定时过期、惰性过期、定期过期

10.1.1 定时过期

为每个key都创建一个定时器,时间到了,就会将这个key清除

这个策略可以立刻清除过期的数据,对内存很友好,但是会占用大量的CPU,影响缓存的响应速度

10.1.2 惰性过期

key过期了,但是不进行处理,当后续访问到这个key时,才会判断该key是否已经过期,过期则清除

该策略可以最大地节省CPU资源,但对内存不友好。极端情况可能会出现大量的过期key没有再次被访问,从而不会被清除,占用大量内存。

10.1.3 定期过期

将所有的key维护在一起,每隔一段时间从中扫描一定数量的key(采样),并清除其中已经过期的key,通过调整定时扫描的时间间隔和每次扫描的耗时,可以在不同情况下使得CPU和内存资源达到最优的平衡效果。

在Redis的实现是通过 惰性过期 + 定期过期 2种策略配合,达到内存回收的效果。

10.2 内存淘汰

10.2.1 淘汰算法

LRU (Least Recently Used): 最近最少使用算法。优先移除最近最少使用的数据。

LFU (Least Frequently Used): 最近最少频率使用算法。优先移除最近使用频率最少的数据。

10.2.2 内存淘汰策略

如下是redisObject的数据结构,其中最后一次访问时间引用计数,在内存淘汰策略中用到

typedef struct redisObject {// 类型unsigned type:4个bit;// 编码unsigned encoding:4个bit;// 对象最后一次被访问的时间unsigned lru:REDIS_LRU_BITS; /* LRU_BITS为24bit*///引用计数int refcount;4个字节// 指向实际值的指针void *ptr;8个字节
} robj;

11.Redis中间件运用的场景

12.Redis-变慢原因及排查方法

Redis执行命令流程

Redis是单线程操作,如果在Redis中执行耗时较长的操作,就会阻塞其他请求

Redis客户端执行一条命令,分为4部分:发送命令=>命令排队=> 命令执行=> 返回结果

Redis 提供了慢日志命令的统计功能,它记录了有哪些命令在执行时耗时比较久。

可能导致变慢的原因

12.1 使用复杂度过高的命令

经常使用 O(N) 以上复杂度的命令,例如 SORT、SUNION、ZUNIONSTORE 聚合类命令

Redis 在操作内存数据时,时间复杂度过高,要花费更多的 CPU 资源

解决方案:对于数据的聚合操作,放在客户端做

12.2 操作bigkey(value很大)

如果都是 SET / DEL 这种简单命令出现在慢日志中,那么你就要怀疑你的实例否写入了 bigkey

 写入的 value 非常大,那么 Redis 在分配内存时会比较耗时

12.3 集中过期

如果有大量的 key 在某个固定时间点集中过期,在这个时间点访问 Redis 时,就有可能导致延时变大。

12.4 开启AOF

12.5 重写

12.6 碎片整理

Redis 的数据都存储在内存中,当我们的应用程序频繁修改 Redis 中的数据时,就有可能会导致 Redis 产生内存碎片。

Redis 的碎片整理工作是也在主线程中执行的,当其进行碎片整理时,必然会消耗 CPU 资源,产生更多的耗时,从而影响到客户端的请求。

12.7 绑定cpu

系统方面-redis变慢的原因

  • 内存实例达到上限
  • 开启内存大页
  • 网络带宽

13.Redis-多线程竞争同一key-解决方案

Redis命令连续的情况:使用Lua脚本,其是原子执行的,执行过程中不会插入其他命令

Redis命令不连续的情况:比如先读redis,操作之后,再写入redis,用下方的方案

  • 乐观锁
  • 分布式锁
  • 时间戳
  • 消息队列

14.Redis秒杀的解决方案

方案一:使用商品ID作为分布式锁,加锁后查询扣减库存

实现流程:

用户发起秒杀请求到redis,redis先使用商品ID作为key尝试加,保证只有一个用户进入之后的流程,保证原子性;如果加锁成功,则查询库存,如果库存充足,则扣减库存,代表秒杀成功,若库存不足,直接返回秒杀失败。

 /*** @description: Redis秒杀方法一:先加分布式锁,然后查询缓存,根据库存数量进行后续操作* 如果库存数大于0,扣减库存,返回true;若库存小于等于0,返回false*/public Boolean secKillByRedisFun1(Integer goodId){// 根据商品id构建keyString goodKey = "good-stock-" + goodId;String userId = Thread.currentThread().getName() + "-" + System.currentTimeMillis();// 使用商品作为锁String lockId = "sec-kill-lock-" + goodId;return this.subStock(lockId, userId, goodKey);}/*** 使用分布式锁秒杀,加锁后查询redis库存,最后扣减库存* @param lockId 锁id* @param userId 用户id* @param goodKey 商品id* @return 秒杀成功返回true 否则返回false*/private boolean subStock(String lockId,String userId,String goodKey){// 尝试先加锁,加锁成功再进行库存查询和扣减库存操作,此时只有一个线程进入代码块if (redisLock.lock(lockId,userId,4000)){try{// 查询库存Integer stock =  (Integer) redisTemplate.opsForValue().get(goodKey);if (stock==null){System.out.println("商品不在缓存中");}// 如果剩余数库存大于0,则扣减库存if (stock > 0 ){redisTemplate.opsForValue().decrement(goodKey);return true;} else {return false;}}finally {// 释放锁redisLock.unlock(lockId,userId);}}return false;}

该方案存在一些缺点:

  • 用户先抢锁再进行库存查询、操作,但存在无用的争抢,即抢锁前库存已经为零
  • 锁的是商品ID,锁粒度太大

解决方案:

  • 抢锁前先查询库存,如果库存为零,直接返回false,不必参与抢锁
  • 使用商品ID+库存量作为,降低锁粒度

方案二:使用商品ID+库存ID作为分布式锁,查询后加锁扣减库存

用户发起秒杀请求到redis,redis先查询库存量,然后根据商品id+库存量作为key尝试加锁,保证只有一个用户进入之后流程,保证原则性。

如果库存充足,加锁并减扣库存,代表秒杀成功;若库存不足,则直接返回秒杀失败。

注意:查询库存量可以过滤大量请求

    /*** @description: Redis秒杀方法二:先查询缓存,根据库存数量进行后续操作(加锁、扣减库存)* 如果库存数大于0,加锁,扣减库存,返回true;若库存小于等于0,返回false*/public Boolean secKillByRedisFun2(Integer goodId){// 根据商品id构建keyString goodKey = "good-stock-" + goodId;// 查询库存量Integer curStock = (Integer) redisTemplate.opsForValue().get(goodKey);if (curStock <= 0){return false;}String userId = Thread.currentThread().getName() + "-" + System.currentTimeMillis();// 使用商品 + 库存量 作为锁String lockId = "sec-kill-lock-" + goodId + "-" + curStock;return this.subStock(lockId, userId, goodKey);}/*** 使用分布式锁秒杀,加锁后查询redis库存,最后扣减库存* @param lockId 锁id* @param userId 用户id* @param goodKey 商品id* @return 秒杀成功返回true 否则返回false*/private boolean subStock(String lockId,String userId,String goodKey){// 尝试先加锁,加锁成功再进行库存查询和扣减库存操作,此时只有一个线程进入代码块if (redisLock.lock(lockId,userId,4000)){try{// 查询库存Integer stock =  (Integer) redisTemplate.opsForValue().get(goodKey);if (stock==null){System.out.println("商品不在缓存中");}// 如果剩余数库存大于0,则扣减库存if (stock > 0 ){redisTemplate.opsForValue().decrement(goodKey);return true;} else {return false;}}finally {// 释放锁redisLock.unlock(lockId,userId);}}return false;}

保证查询库存扣减库存操作的原子性,可以使用lua脚本实现这两个操作的原子性,这样就不需要额外维护分布式锁的开销。

方案三:使用INCRDECR原子操作扣减库存

直接使用DECR操作扣减库存,不需要提前查询缓存

  • 如果返回大于0,说明库存充足,表示秒杀成功
  • 如果返回小于0,说明库存不足,需要使用INCR操作恢复库存,秒杀失败
     /*** 秒杀方案三:使用原子操作DECR和INCR扣减库存* @param goodId 商品ID* @return*/public Boolean secKillByRedisFun3(Integer goodId){// 根据商品id构建keyString goodKey = "good-stock-" + goodId;Long stockCount = redisTemplate.opsForValue().decrement(goodKey);if (stockCount >= 0){return true;}else {// 恢复库存redisTemplate.opsForValue().increment(goodKey);return false;}}

不足:后期库存为零,大量请求扣减库存后需要恢复库存,造成大量无用操作

解决方案:可以提前查询库存,如果库存为0,直接返回false,秒杀失败

15.用Redis如何实现延迟队列?

使用redis数据结构中的zset(有序集合)实现延时队列

zset的添加、查询、删除命令

ZADD命令:
key-键 score-权重 member-元素值
ZADD key score member[score member...]ZRANGEBYSCORE命令:
ZRANGEBYSCORE key min maxZREM命令:
ZREM key member[member...]
  • 将延时任务的到期执行时间作为zset的score(权重)延时任务序列号化作字符串作为zset的member,使用zadd命令添加进zset有序集合中
  • zset有序队列会为这些延时任务按照到期执行时间进行score(权重)排序
  • 设置多线程轮询zset获取到期的任务
  • 在zset中删除这些到期任务
  • 到期任务放进list,在list中使用blpop/brpop消费任务

redislist数据结构中的方法实现队列消费模式

brpop意思 block right pop 阻塞式右侧出队
blpop意思 block left pop   阻塞式左侧出队

16.Redis如何保证缓存和数据库的一致性

缓存和数据库一致性方案

  • 小厂模式(缓存单删)
  • 小厂优化模式(延时双删)
  • 中厂模式(定时更新+增量查询)
  • 大厂模式(监听binlog+MQ)

16.1 缓存单删

在更新数据前先删除缓存;然后再更新库,每次查询的时候发现缓存无数据,再从库里加载数据放入缓存。

此方案主要解决的是当时在面试回答方案中的弊端;为什么不更新数据时同步进行缓存的更新了?

主要是有些缓存数据,需要进行复杂的计算才能获得;而这些经过复杂计算的数据,并不一定是热点数据,节省缓存空间,没必要存储;所以采取缓存删除,当需要的时候在进行计算放入缓存中,节省和缓存中数据量

16.2 小厂优化模式(延时双删)

具体操作为:操作数据前,先删除缓存;接着更新DB;然后延迟一段时间,再删除缓存

???延迟多少时间才刚刚好

16.3 中厂模式(定时更新+增量查询)

定时更新+增量查询:主要是利用数据库行数据的更新时间字段+定时增量查询

16.4 大厂模式(监听binlog+mq)

主要是通过监听数据库的binlog,通过binlog把数据库数据的更新操作日志采集后,通过MQ的方式,把数据同步给下游的消费者,下游消费者拿到数据的操作日志并拿到对应业务数据后,再放入缓存。

17.Redisson-分布式锁的原理

Redis做分布式锁如何处理超时时间?

问题:比如分布式锁的超时时间是5s,但是要执行的时间是20s,相当于没锁住;或者是锁的时间是20s,但是要执行的时间是5s,造成锁资源浪费。-- 使用Redisson分布式锁

Redisson分布式锁方案优点

1.Redisson通过Watch Dog(看门狗)机制很好的解决了锁的续期问题

2.通过Redisson实现分布式可重入锁,比原生的SET mylock userId NX PX millseconds + lua 效果更好

3.在进程等待申请锁的实现做了优化,减少无效的锁申请,提升了锁的利用率

为什么Redisson不用setnx实现分布式锁?

或者说setnx实现分布式锁的缺点

锁过期时间不能自动续约:使用setnx命令实现分布式锁,如果获取锁的客户端执行时间过长,导致锁过期,其他客户端就有可能获取到这个锁。自动续约机制是:在锁的持有者自身没有释放锁的情况下,对锁进行续约以保证该锁持续生效

不支持可重入:如果某个线程已经持有一个锁,再次对这个锁进行加锁时,setnx命令会认为这个键已经存在,无法再次进行加锁。而支持可重入的锁允许同一线程多次加锁,且要求解锁次数与加锁次数相等

不支持锁的释放:setnx命令只能通过设置过期时间或者等待过期时间释放锁,如果某个线程异常退出或者未能及时释放锁,就有可能导致死锁的发生。而支持释放的锁允许设置锁的自动释放时间或者手动释放锁

Redisson提供的分布式锁可以解决以上三个问题,例如使用Lua脚本来保证原子性,使用Redis的watch机制来实现分布式锁的释放,使用watchdog机制实现分布式锁的续期

Redisson使用Redis的发布订阅机制来加锁。流程是:

获取锁,若获取不成功则订阅释放锁的消息,在收到释放锁的消息前阻塞,收到释放锁的消息后再去循环获取锁。

这里有一个细节:如果服务宕机了,Watch Dog 机制的线程也就没有了,此时就不会延长 key 的过期时间,到了 30s 之后就会自动过期了,其他线程就可以获取到锁。

----- 红锁的出现解决了这一个问题???

《关于集群Cluster的面试题目》

http://www.dtcms.com/a/602321.html

相关文章:

  • 【剑斩OFFER】算法的暴力美学——寻找峰值
  • 【DeepSeek实战】高质量提示词的六种类型
  • 从零开始学习PX4源码30(定高(ALTITUDE)模式)
  • 中国建设银行对公网站中国500强企业排名
  • 做网站的为什么不给域名和密码个人网页制作与网站建设
  • GIT基础使用教程
  • 想建设个人网站去那里建设宁德做网站的公司
  • wordpress网站seo专业展馆展厅设计
  • LangGraph智能知识库系统架构设计方案 - 多agent架构
  • 在线C语言编译 | 提供便捷高效的在线编程环境
  • 二级网站建设费用网站有备案号吗
  • 搭建Golang gRPC环境:protoc、protoc-gen-go 和 protoc-gen-go-grpc 工具安装教程
  • flutter项目老是卡在Running Gradle task ‘assembleRelease‘......
  • 东莞清溪镇做网站公司对网站有效的优化软件
  • Python的asyncio核心组件
  • 建立网站要多少钱销售平台有哪些
  • 诸暨公司做网站免费项目进度管理软件
  • leetcode:逆波兰表达式求值
  • sql中left join和inner join的区别
  • 最小栈--leetcode
  • 做网站的学什么代码wordpress 主题末班
  • 网站建设二公司psd转wordpress主题
  • 线性代数 - 3 阶方阵的行列式 可视化
  • 营销型网站首页模板做纺织生意用什么网站好
  • flink部署选型方案以及flink-on-k8s部署
  • 3GPP标准各个版本的介绍和演变
  • 网站设置的参数江西建设厅网站查询施工员
  • 程序员个人网站开发模板之家网页模板
  • 彭阳网站建设多少钱做网站怎么发展客户
  • 做软件项目的网站百度制作企业网站多少钱