Redis学习笔记--面试题总结

面试题

 
1、Redis是单线程吗?
不完全是,要看具体的版本,在早期的版本中例如:redis4.0及之前的版本是单线程的,而在后续的版本例如redis6.0之后的版本引入了多线程(默认是关闭多线程的)。即使在新版本中引入了多线程,多线程也是在执行一些辅助的任务,例如:持久化(AOF/RDB)、异步删除、网络I/O多线程,对于核心命令的执行依旧采用单线程。
单线程的优势:
1、无锁结构,单线程模型天然的避开了多线程的锁竞争问题。保证了数据的原子性。
2、redis的性能瓶颈并不在CPU上而是内存和网络I/O,单线程已经足够高效了。
3、维护成本低,单线程模型简单,调试和维护成本远低于多线程。
后期的版本中为什么引入了多线程呢?那是因为随着硬件和业务场景的需要,单线程暴露了很多问题。
单线程的劣势:
1、多核CPU利用率不足,现在的硬件CPU普遍是多核的,例如8核的CPU只用其中一个核,无法发挥硬件的性能。
2、解决网络瓶颈,在大量的网络请求中,网络I/O吞吐量和命令的执行会受限于单线程,无法高效的完成工作。
3、对于大key的删除,会导致主线程阻塞,需要引入多线程,将耗时的操作分配给子线程去完成。
在后续的版本中redis引入了多线程,但并不意味着完全放弃了单线程,对于核心的读写命令的执行还是使用单线程来完成的,那么单线程和多线程是如何配合工作的呢?
1、主线程,负责串行的命令执行、定时任务、事件调度
2、多线程处理网络I/O线程和后台线程
2.1、I/O线程,当用户想redis客户端发送请求时,多个线程并行读取客户端请求并解析放入队列中,主线程按顺序执行队列中的命令,I/O线程将结果批量返回给客户端
2.2、后台线程,处理持久化(AOF/RDB)还有异步删除等耗时的任务,不拖累主线程。
 
0
 
2、Redis为什么快?
1、基于内存存储,内存的出现就是为了解决CPU和磁盘速率不匹配的问题。基于内存的读写避免了磁盘I/O瓶颈。存储数据落入磁盘时的数据的持久化(AOF/RDB)是异步或定时触发的,不影响主线程处理请求。
2、I/O多路复用,使用I/O多路复用(一个线程可以同时管理多个Socket套接字)+epoll通过Reactor模式可以实现单线程监听大量连接,通过事件驱动处理请求,最大化利用CPU。在redis6及之后的版本中引入了多线程处理网络I/O,但核心命令的执行还是单线程。Redis利用epoll来实现IO多路复用,将连接信息和事件放到队列中,依次放到文件事件分派器,分派器将事件分发给事件处理器。多个连接共用一个阻塞对象,应用程序只需要在一个阻塞对象上等待,无需阻塞等待所有连接。当某条连接有新的数据可以处理时,操作系统通知应用程序,线程从阻塞状态返回,开始进行业务处理。因为文件事件分派器队列的消费是单线程的,所以Redis才叫单线程模型
3、单线程+多线程的巧妙配合,多线程用来解决网络I/O问题,单线程处理核心命令保证线程安全和高性能。
4、高效的数据结构设计,例如:简单动态字符串、压缩列表、跳跃表、哈希表等等。所有数据类型底层都不是单一的数据结构而是采用阈值或类型作为划分的标准,在不同的阈值区间内使用不同的数据结构。
0
 
3、关于IO的理解?
I/O的本质是数据从内核空间和用户空间的拷贝,当读取数据时:网卡=》内存缓冲区=》程序内存,当写入数据时程序内存=》内存缓冲区=》网卡。
阻塞I/O:也就是BIO,举例子:就像在食堂排队打饭一样,只有前面的人打完饭后面的人才能打饭。每个连接需要一个线程,读取内核数据时会检查内核中数据是否可以被读取,如果数据资源被其它线程占用则需要等待(客户端等待),进程会被阻塞无法处理其它工作,如果没有则可以直接进入。可以理解成排队进入,效率很低,不适用于高并发的业务场景。
优点:实现简单,兼容性好,所有操作系统都支持。
缺点:单线程只能处理一个IO,无法实现并发且会造成阻塞,资源利用率低,会导致CPU空转。
适用场景:适用于简单的单任务场景。
 
非阻塞I/O:也就是NIO,举例子:就像在肯德基麦当劳点餐,点完餐会给一个号牌,我们可以随时到收银台询问配餐情况,直到取餐结束,我们是不需要一直在收银台等着的。它可以实现单个线程通过轮询的方式(客户端轮询)处理多个请求连接,允许程序在IO操作未就绪时立即返回去处理其它工作,无需阻塞等待,从而提升资源利用率,注意:相较阻塞IO,这里是单个线程同时接收多个请求,避免现成阻塞,适合对并发要求不高的业务场景。
优点:可以通过单个线程处理多个IO,兼容性好,所有操作系统都支持。
缺点:需要通过轮询的方式获取处理结果,如果请求多的话等待时间会变长,还会导致大量的CPU空转造成资源浪费。
适用场景:适用于请求量较小的多任务场景。
 
I/O多路复用:IO多路复用就是我们常说的select、poll、epoll这些linux或unix内核的函数,是一种事件驱动模型,通过用一个线程监听多个I/O描述符,可以在内核中感知IO是否就绪,当某个事件就绪时通知用户线程处理。将轮询的工作交给内核,用户线程只需要等待通知即可。它的核心思想是利用最小的资源开销来实现高并发的IO处理,解决了传统阻塞IO和非阻塞IO模型的并发问题。所谓 I/O 多路复用机制,就是说通过一种机制,可以监视多个文件描述符,一旦某个描述符就绪(一般是读就绪或写就绪),能够通知程序进行相应的读写操作。这种机制的使用需要 select 、 poll 、 epoll 来配合。
 
4、如何一次性插入1千万数据到redis?
通过java操作redis的批量插入或者命令行set命令是不可行的,因为数据量太大了。需要通过在命令行执行脚本来完成。
4.1、通过脚本执行,把命令写入到txt中
0
 
4.2、结合上一步,通过管道pipe来执行
0
 
5、keys * 命令为什么不能在生产环境使用?
keys * 命令用于遍历当前redis库下的所有key,由于redis命令的执行是单线程串行处理的,在生产环境或者大数据量的场景下,会造成redis上锁时间过长,影响其它命令执行。最终可能导致雪崩而宕机。所以对于一些危险的命令一定要禁用
 
6、如何禁用一些危险命令?
在redis中大数据量或生产环境下keys *会造成阻塞引发宕机,flushdb 、 flushall会导致全部数据被清空。对于这类危险的命令需要禁用它。在redis.conf中将需要禁用的命令重命名为空字符串即可。
修改配置后要重启redis
0
执行效果
0
 
7、keys * 的平替游标查询?
keys * 会导致大数据量下redis阻塞卡顿,需要找到平替的命令来替代它的工作,可以使用scan扫描命令来完成。
scan是基于游标的迭代器,每次执行会返回一个游标值和当前遍历的结果集,用户在下次迭代时需要使用上次迭代的游标返回值做当前游标的开始值,游标值从0开始到0结束,每次默认返回10条记录,这个游标和ES有点像。
SCAN是如何执行的呢?
使用实例:
0
 
8、大key相关?
拒绝大key,String类型10kb以上,hash、list、set、zset元素个数超过5000都算大key。
大key的危害:
1、内存不均匀,集群迁移困难。
2、删除困难,造成阻塞。
 
大key的产生:
1、大博主的粉丝列表。
2、数据汇总统计,报表数据等等。
 
大key如何发现:
memory usage命令可以发现大key,会返回key所占用的字节数
0
 
大key的删除:
对于大key的删除不建议直接使用del,建议分批次获取数据,先减少key中的数据量然后再del key
String类型:可以使用del删除,过于庞大的使用unlink删除
hash类型:分批次删除,先用hscan游标查询获取少量的field-value数据去做删除,在使用hdel删除整个key
List类型:分批次删除,先用ltrim分批次删除例如每次删100个,最后在使用del删除整个key
set类型:分批次删除,先用sscan游标查询获取部分元素,再试用srem删除每个元素,最后使用del删除整个key。
zset类型:分批次删除,先用zscan游标查询获取部分元素,在使用zremrangebyrank删除每个元素,最后使用del删除整个key。
 
大key删除的调优:
redis的删除有两种:
1、redis默认使用的是del阻塞删除,意味着服务器会停止新命令的执行来删除key,删除小的key还好,但是对于大key的删除会造成长时间的阻塞导致系统卡顿。
2、redis可以使用非阻塞的异步删除,例如:unlink、flushall、flushdb的async,由于是异步删除这些命令会在很定的时间内执行,对系统影响较小。
对于删除的调优,可以调整redis.conf中关于删除的配置项,开启惰性释放,分批次的删除大key中的数据,惰性删除的核心思想是:将耗时较长的内存释放操作转移到后台线程异步处理,主线程将待删除的key标记为“可删除”后继续处理其它请求,后台线程负责大key的实际回收工作。
0
 
9、redis和mysql的双写一致性?
从业务场景的应用情况来说可以分成三个维度去设计一致性解决方案。无论是哪个解决方案都是以mysql为数据基准,mysql数据有最终解释权。
对于弱一致性场景使用延迟双删即可,对于实时的强一致性场景需要加锁,对于高可用的场景只要保证最终一致性即可。
1、弱一致性场景:使用经典的延迟双删策略
读流程:读redis命中即返回,未命中则读mysql将数据回写到redis并返回。
写流程:先更新数据库,立即删除redis,开始延时等待(500ms-1s,这个时间的估算要大于整次更新的时长,例如:MySQL 主从同步延迟约 200ms,则总延迟可设为 500ms。),等待结束后再次删除redis,然后将更新到mysql的数据回写到redis并返回。
二次删除:是为了解决在第一次删除redis后可能有其他线程读取到旧数据重新写入redis,这些可能的并发操作会导致有脏数据写入,二次删除可以清理掉这些脏数据。
优点:
1、可以减少数据不一致的时间窗口。
2、实现相对简单,不需要引入其它中间件。
缺点:
1、无法保证完全杜绝脏数据,若在二次删除前的延迟期间,又有新线程写入脏数据,仍可能不一致。
2、延迟时间难以估算,时间设置不合理可能导致性能浪费。
代码示例:
public void updateData(Data data) { // 1. 更新数据库 db.update(data); // 2. 第一次删除缓存 redis.delete(data.getId()); // 3. 延迟后再次删除缓存(异步执行) scheduleTask(() -> { // 等待 500ms Thread.sleep(500); // 第二次删除缓存 redis.delete(data.getId()); }, 500); }
 
2、高可用的最终一致性场景
适用于高可用的场景,通过cancl异步监听mysql的bin log日志将数据变更做生产者转发给mq,redis做mq的消费者获取消息并保证最终的一致性。
流程:
1、mysql更新数据
2、canal伪装成msyql的从机监听主库binlog变化,并转换数据结构为json
3、canal做生产者将监听结果发送到MQ
4、redis消费MQ中的消息更新缓存,如果更新失败再将数据写回MQ达成补偿机制
有点:
1、对并发支持友好,对业务性能影响小。
2、支持主从延迟场景
3、完全解耦业务逻辑和数据同步,扩展性强。
缺点:
1、实现复杂,需要引入mq和canal监听工具
2、存在短暂不一致时间窗口,依赖消息队列消费速度
 
3、实时强一致性场景
对于金融和电商超卖场景,需要保证数据的强一致性。
流程:
0、redis缓存未命中
1、获取分布式锁,lock.lock或者
2、更新mysql数据
3、删除或更新缓存
4、释放锁
5、数据返回
优点:
强一致性。
缺点:
性能损耗大,复杂程度高,适用于写操作频次较低的场景。
 
 
10、hyperLogLog?
它是一种高效的数据结构,用于大规模数据集的基数统计(即集合中唯一元素的个数)自带去重功能,只做数据统计不存储原始数据,可以使用很小的内存空间(12k)和常数的时间复杂度来完成大数据量的存储和操作,它的大数据量是存在一定误差的,会有0.81%的误差,适用于不那么精确的数据统计场景使用,例如:UV、关键词搜索次数等等。
优点:
1、占用内存低,适合高内存地并发的应用场景,占用空间仅有12kb,因为是2的14次方=16384
2、常数级别的时间复杂地
3、支持分布式合并
缺点:
1、无法查看原始数据,只存储个数。
2、存在误差,误差率0.81%
应用场景:
1、UV统计,统计每日网站用户访问数,支持合并多天的UV计算。
2、实时数据分析,统计广告点击的UV
3、日志处理,统计不同错误代码出现的次数
 
11、hyperLogLog为什么占用12kb?
hll的核心思想是将数据哈希后分到多个桶中,每个桶固定是16384(2的14次方),每个桶占用6bit,这6bit刚好可以覆盖所有可能的前导零数量。
 
12、hyperLogLog为什么会有误差?
HLL是估算基数,核心原因是HLL统计模型的设计导致了存在误差的存在,通过哈希值进行分桶统计(如前导零的最大数量)来估算的,不同元素可能哈希到同一桶并产生相同的前导零数量,导致高估或低估,当实际数据分布不均匀时估算会偏离真实值所以会有误差,这个误差只能是优化不可能完全消除。说白了就是因为hash冲突导致的。
 
12、GEO地理坐标的应用?
1、美团查看附近N公里内的商家:获取用户当前坐标位置,在redis的geo类型内计算经纬度差值来划定范围内的商家。
2、查看外卖小哥离我还有多远:获取外卖小哥的坐标位置,获取用户坐标位置,通过redis的geo计算得出两个坐标的经纬度差值,转换成距离值。
推荐使用ES做复杂的条件搜索,用redis计算实时坐标。当然mysql也可以实现但不建议使用,因为并发量是一个性能瓶颈,不适合高频次的检索。
 
13、布隆过滤器?
它是一个概率性数据结构,用于快速判断集合中是否存在某个元素,具有空间效率高(占用空间小)查询时间快(时间复杂度低)等特点。它可以添加元素,但是不支持删除元素,它的构建和使用是建立在redis的bitmap类型基础上的。
优点:占用空间小,不保存数据信息,只在内存中做一个是否存在的标记flag。
缺点:
1、存在误判:因为hash冲突可能出现多个元素共用一个坑位,也就是说多个元素的哈希函数值可能映射到相同的数组位置,即使一个元素未插入过,其哈希位可能被其它元素设置为1导致了存在误差,这个误差只能是优化(更换算法或多几次hash来减小误差)不可能完全消除,当一个元素如果判断存在,不一定真的存在,如果判断不存在那肯定是不存在的。布谷鸟过滤器可以解决这个问题,但布隆过滤器已经可以满足大部分的企业开发,所以布谷鸟过滤器应用不多见。
2、不能删除元素:布隆过滤器可以添加元素,但不能删除元素,由于hashcode判断依据,删除元素会导致误判率增加。
应用场景:
1、redis+布隆过滤器可以防止缓存穿透。
2、黑名单校验,识别垃圾邮件,非法用户、IP等等。
实践代码:
package com.example.study.redis; import com.alibaba.fastjson.JSONObject; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.redis.core.Cursor; import org.springframework.data.redis.core.RedisCallback; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.data.redis.core.ScanOptions; import org.springframework.stereotype.Component; import org.springframework.util.CollectionUtils; import org.springframework.util.StringUtils; import javax.annotation.PostConstruct; import java.nio.charset.StandardCharsets; import java.util.HashSet; import java.util.Set; /** * @ClassName BoolmFilterInit * @Description 布隆过滤器初始化--随项目启动 * @Author shipengyan * @Date 2025/3/8 14:21 **/ @Component public class BoolmFilterInit { @Autowired RedisTemplate redisTemplate; @PostConstruct public void init() { System.out.println("初始化布隆过滤器----start"); //分批次获取所有的key,每次获取5个。 long offset = 5; Cursor<byte[]> execute = (Cursor<byte[]>) redisTemplate.execute((RedisCallback<Cursor<byte[]>>) connection -> connection.scan(ScanOptions.scanOptions().count(offset).match("*").build())); //将key整理至Set集合中 Set<String> keys = new HashSet<String>(); while (execute.hasNext()){ byte[] next = execute.next(); keys.add(new String(next, StandardCharsets.UTF_8)); } //关闭游标 execute.close(); //打印key集合 System.out.println(JSONObject.toJSONString(keys)); //对key进行hash运算,失败后重试一次 String boolmKey = "allKey"; this.setBoolmFilter(keys, boolmKey, true); } //注入布隆过滤器 public boolean setBoolmFilter(Set<String> keys , String BoolmFilterKey , boolean flag){ if(CollectionUtils.isEmpty(keys) || StringUtils.isEmpty(BoolmFilterKey)){ return false; } //初始化布隆过滤器 Set<String> firstSet = new HashSet<>(); for (String key : keys) { //获取当前key的hash值 int hashValue = Math.abs(key.hashCode()); //在当前key的hash值上取模,得到布隆过滤器中的索引值 long index = (long)(hashValue % (Math.pow(2 , 32) - 1)); //设置布隆过滤器。 //返回值仅表示旧值,与操作是否成功无关。意思就是只返回上次这个位置存的是什么值。 //如果返回0代表首次设置因为默认值为0,如果返回1代表之前已经有过设置。 Boolean res = redisTemplate.opsForValue().setBit(BoolmFilterKey, index, flag); if(!res){ firstSet.add(key); } } System.out.println("首次设置的key=" + JSONObject.toJSONString(firstSet)); return true; } }
 
14、布隆过滤器的构建和查询过程?
构建key:使用多个hash函数(避免哈希冲突)对key进行hash运算得到一个整数索引值,对位数组长度取模运,计算应该放到数组的哪个位置上,每个hash函数都会得到不同的位置,将这几个位置都置为1完成添加操作。
查询key:将被查询的key通过多次hash函数进行运算(避免哈希冲突)然后对数组长度取模,查看对应位置,只要其中一位是0就表示这个key肯定不存在,如果都是1则不一定存在对应的key。
构建和查询过程可以参考13中的代码实践部分。
 
15、什么是hash冲突或者hash碰撞?
hash冲突和hash碰撞是一回事,表示的都是:不同的输入数据经过哈希函数计算后得到相同的哈希值。示例如下图:
0
 
16、缓存穿透?
正常来说,数据访问的流程是先访问reids如果redis没有在访问mysql。当redis和数据库也没有的情况下,每次查询都要访问数据库,这就是缓存穿透。总结:频繁的请求不存在的数据,缓存和数据库均无结果,请求直达数据库,把数据库打爆了。
危害:缓存穿透最大的问题是当有大量的请求来查询不存在的数据时redis无法命中,会导致mysql的数据查询压力倍增,甚至会拖垮数据库。
解决方案:
1、入参校验:对请求参数进行合法性校验,拦截恶意请求,如ID<=0直接拦截
2、缓存控制:对不存在的key设置空值并设置较短的过期时间(1、3、5分钟)
3、布隆过滤器:可以使用布隆过滤器来解决缓存穿透的问题,将已存在的redis的key存在布隆过滤器中,数据访问流程变成了:请求-->布隆过滤器-->redis-->mysql。当有请求访问时,先去布隆过滤器查询是否存在,根据布隆过滤器的特性可知:如果布隆过滤器中不存在那百分百是不存在的,如果布隆过滤器存在再去redis,如果redis不存在再去查询mysql。这样就可以把一部分流量拦截在redis之前,从而减轻因为缓存穿透带来的数据访问压力。如果最终判断redis、mysql都没有,那么可以在查完mysql后给这个redis的key赋一个空值过期时间五分钟(避免长时间的过期时间把内存吃满),下次再有类似的空值就可以把这部分流量挡在mysql之前了。
0
 
17、缓存击穿?
产生原因:热点数据过期的瞬间有大量请求直接打穿了数据库。例如:两个相近的秒杀时间段内,旧商品下架时删除了redis的key,此时,新的商品数据还没有灌入到redis时就会出现缓存击穿。
解决方案:
1、双检加锁(互斥更新):通过加锁保证一次只允许一个请求重建缓存数据,其它线程等待,lock.lock()加速方法
2、永不过期:不对热点key设置过期时间
3、本地缓存:前端本地缓存分摊redis的压力
4、提前预热数据:将后续需要的数据提前进行预热,避免出现新旧数据青黄不接的窗口期,例如在相近时间段内的两个秒杀活动,redis中旧数据还没有删除完成,新数据也没有灌入到redis中就可以能因为空窗期导致缓存击穿,这个时候可以将旧数据的过期时间多设置一些,例如2小时的秒杀活动可以设置为2小时零五分,新数据提前10分钟进行预热灌入到redis缓存中,避免空窗期引发缓存击穿。
 
18、缓存雪崩?
产生原因:大量key同时失效(过期时间相同)或redis服务宕机,导致请求集中到数据库。
解决方案:
1、差异化过期时间:基础过期时间+随机偏移量(如:30min+100秒内的随机数来组合过期时间)
2、高可用:通过redis Cluster来防止单点故障
3、熔断降级:通过Sentinel等工具在mysql访问量过大时拒绝部分请求
 
19、缓存预热?
产生原因:系统启动时缓存无数据,首波请求直接压垮数据库。
解决方案:
1、手动预热:上线前通过脚本加载热点数据或者通过@postconstruct在项目启动时预热数据
2、动态预热:基于历史访问数据,定时刷新热点key
 
20、缓存击穿、缓存床头、缓存雪崩、缓存预热对比关联?
0
共同点:都是发生在缓存未命中导致数据库压力倍增。
不同点:
缓存穿透:数据本身就不存在,多是恶意请求
缓存击穿:数据本身是存在的但是某一个时刻key过期了导致单个key失效
缓存雪崩:数据本身是存在的,短时间内key大规模失效或宕机
 
21、谷歌的Guava布隆过滤器?
原理:通过多次的哈希函数将元素映射到位数组中,标记存在性。默认误差率是3%,也可以手动指定
优点:空间效率高,查询时间快,时间复杂度O(k) k取决于hash次数,hash次数取决于误差率高低
缺点:存在误判,不支持删除
使用场景:黑白名单、防止缓存穿透、防爬虫、防止抖音或者淘宝重复视频和商品的推荐,对某个用户而言已经推荐过的视频和商品就放到布隆过滤器里面,下次给用户推荐前先到布隆过滤器里找一遍看看是否在某个时间段内推荐过。
示例代码:
BloomFilter<String> filter = BloomFilter.create( Funnels.stringFunnel(), 1000000, // 预期元素数量 0.01 // 误判率 ); filter.put("key1"); if (filter.mightContain("key2")) { /* 可能存在 */ }
 
22、布谷鸟过滤器?
原理:基于布谷鸟哈希,存储每个元素的指纹,每个元素对应两个候选桶
优点:支持删除操作,空间利用率高,时间复杂度接近O(1)
缺点:实现复杂
 
23、Guava布隆过滤器和布谷鸟过滤器如何选择?
无需删除选布隆过滤器,需要删除或更高效的时间空间效率选择布谷鸟过滤器
 
24、布谷鸟过滤器为什么支持删除操作?它的实现原理是什么?
布谷鸟过滤器存储每个元素的指纹(Fingerprint),并通过两个候选桶定位数据。删除时只需移除对应指纹,而布隆过滤器的位数组无法区分具体元素。
 
25、缓存穿透中,缓存空值时为什么要设置较短的过期时间?
防止攻击者持续构造不同非法Key耗尽缓存空间,短过期时间可自动清理无效数据。
 
26、redis除了缓存外还有哪些应用?
排行榜、黑白名单、去重(HyperLogLog)、过滤器(Bloom Filter​)、会话存储(用户中心User服务)、GEO地理位置服务等等.
 
27、为什么要用分布式锁?
之前提到的锁lock和synchronized都是运行在单个虚拟机内的,在分布式环境下redis会被部署到多个机器上组成集群,单机的锁是没办法约束到其它机器上的线程的,单机锁已经不再起作用,所以要引入分布式锁。
 
28、分布式锁要具备哪些特性?
1、独占性:任何时刻有且只能有一个线程持有当前锁。
2、高可用:在集群环境下,不能因为单节点故障出现获取锁和释放锁失败的情况。
3、不乱抢:A线程加的锁,只能由A线程释放,不能被其它线程释放,不能出现张冠李戴的情况。
4、防死锁:锁必须要有超时释放机制或者撤回机制,不能因为死锁卡死后续操作。
5:重入性:同一个节点的同一个线程获取锁之后,它可以再次获得这个锁的而不用重新上锁。
 
29、Lua脚本在redis中的作用?
Lua脚本可以把多个不相关的redis命令捆绑到一起去执行,保证了数据的原子性。
 
30、在redis中setnx和hset做锁有什么区别?
最大的区别在于锁的可重入性,setnx是不支持可重入的,hset是支持可重入的。在使用hset做可重入锁的时候可以用hincrby替代hset会更方便,直接+1就可以了。
 
31、自旋和自旋锁是什么?
自旋是一种等待策略,而自旋锁是基于这种策略的具体同步机制实现。在Java中,虽然可以直接实现自旋锁,但通常建议使用标准库中的并发工具,它们在自旋和阻塞之间做了智能平衡。
自旋机制示例:
AtomicBoolean flag = new AtomicBoolean(true); // 自旋等待直到flag变为false while (flag.get()) { // 空循环,持续检查flag状态 Thread.yield(); // 可选:让出CPU时间片 }
自旋锁示例:
import java.util.concurrent.atomic.AtomicBoolean; public class SpinLock { private final AtomicBoolean locked = new AtomicBoolean(false); public void lock() { // 自旋直到成功获取锁 while (!locked.compareAndSet(false, true)) { // 可选的优化:Thread.onSpinWait()(Java 9+) } } public void unlock() { locked.set(false); } } // 使用示例 SpinLock spinLock = new SpinLock(); void doWork() { spinLock.lock(); try { // 临界区代码 } finally { spinLock.unlock(); } }
 
32、如何写一个自研的redis分布式锁?为什么不用自研得要用Redisson?
1、遵循JUC规范,实现Lock接口,填写lock和unlock的代码,要考虑全面
lock:考虑加锁、自旋、续期、可重入。
unlock:考虑可重入性的递减问题,加几次锁就递减几次,最后到零,直接删除,要防止误删,只能删自己的锁不能删别人的锁,防止张冠李戴。
2、自研锁适用于一些低并发的场景,对于单点故障无法解决,单点故障后会引发一个锁被多个服务创建的情况,所以需要用官网出的RedLock来解决也就是Redisson,这样可以更好的适用于redis的分布式锁。
 
33、Redisson对机器的要求?
Redisson是redis官方出的分布式锁的解决方案,使用奇数个(官方推荐5个)独立的互不相干的Redis组成,建议部署Redisson的机器和Redis的集群分开部署,以免出现宕机引发的不必要的麻烦,奇数个机器便于单点故障后的选举,计算公式如下:
N=2X-1 N代表需要的总机器数,X代表可以接受的最大宕机数量。
 
34、Redisson的加锁和解锁工作流程?
Redisson加锁和解锁操作都是通过Lua脚本捆绑多个Redis来实现的,使用Lua脚本可以保证命令执行时的原子性。Redisson的加解锁是通过hset来实现的,对于可重入性,hset是非常合适的。对于未指定过期时间的锁的超时时长通过看门狗来自动续期。
加锁:
1、通过exists判断hset类型的当前线程的key是否存在,key=当前线程号+UUID
2、如果不存在则认为是首次加锁,通过hset的hincrby命令对当前key递增+1并设置过期时长。
3、如果存在则认为是重入锁,通过hset的hincrby命令对当前key递增+1并延长过期时长。
4、如果以上都不是则说明当前锁并不是当前线程所持有的锁,则不进行任何操作,返回过期时间。
0
解锁:
1、检查当前线程的锁是否存在,如果不存在直接返回null
2、如果存在则通过hincrby递减1,先释放一次锁并获取当前的可重入锁剩余次数
3、如果大于0则刷新当前锁的过期时长
3、如果小于0则直接删除当前key,并发布锁释放的消息。
0
 
35、看门狗?
如果在加锁时间不指定锁的过期时长则使用看门狗机制来避免锁因业务逻辑执行时间过长而提前释放锁,在每次加锁成功后都会刷新锁的过期时间,Redisson会通过Watch Dog(看门狗)来为当前锁自动续期。当调用unlock或客户端进程崩溃看门狗会停止续期,锁最终会因为过期自动释放(避免死锁)不指定过期时长时锁使用默认的过期时长30S,看门狗的续期时间隔默认是锁时长的三分之一,即默认10S自动续期一次。如果指定了过期时长用过期时长除以3用作看门狗的续期时长。下图是指定过期时长和不指定过期时长的对比:
0
 
36、Redis内存是多小?
默认内存是0,0代表不限制内存大小。如果要设置一般推荐设置内存最大值的四分之三,预留一部分内存空间给系统。
这个配置可以通过配置文件和命令行修改,配置文件修改后需要重启才能生效,命令行修改只针对当前生效,机器重启后自动失效。
查看redis内存使用情况可以通过命令:info memory 和 config get maxmemory查看。
如果存储数据达到redis设置的内存上限会和JVM一报OOM内存溢出。
 
37、如何修改Redis的配置项?
1、通过命令行设置,无需重启只对当前生效,重启后会使用配置文件中的配置项。
适用场景:临时修改、生产环境不停机修改某些配置或者做一些尝试。
2、通过配置文件设置,需要重启,重启后自动生效。
适用场景:允许重启的场景。
以我的经验来看,这两种配置修改方式在绝大多数工具中都是使用的例如:ES、MySql、Influxdb等等,基本都支持这两种方式。
 
37、Redis的缓存淘汰策略?
立即删除:用CPU时间换内存空间,在数据过期后,立马删除,可以释放出很多的内存空间但是对CPU不友好,会导致CPU使用率飙升,立即删除会把压力转移给CPU
惰性删除:用内存空间换CPU时间,在数据过期后,不做任何操作,只有需要访问这个key的数据的时候发现key过期了再做删除操作并返回为空,这样的操作对内存不友好,因为会堆积大量的过期key(冷数据),如果这些key一直不访问就会一直堆在内存里,使得很多无用数据占用内存。惰性删除默认是关闭的,如果需要开启要修改配置项lazyfree-lazy-eviction=yes
定期删除:是对立即删除和惰性删除的折中解决方案,定期抽取部分key(不是全部key)检查是否已过期,如果过期就删除。定期删除的难点在于扫描频率和执行时长的预估和配置,如果设置的频率过高会变成立即删除,如果设置的频率过低会变成惰性删除,因此Redis官方推出了八中缓存淘汰策略,在实际应用中更推荐allkeys-lru这种策略是更折中的优先选择,八种策略如下:
这八种淘汰策略不太好记,可以分为两种场景、五种方式
两种场景:全部key、设置过期时间的key。
五种方式:LRU、LFU、TTL、RANDOM、永不过期
除永不过期外和TTL外,其它方式均可以排列组合二三得六来得到下面的淘汰策略
 
noeviction
​行为:内存不足时,新写入操作返回错误(如OOM),不删除任何数据。
​适用场景:数据绝对不可丢失,且业务能容忍写入失败。
​allkeys-lru
​行为:从所有键中淘汰 ​最近最少使用(LRU)​ 的键。
​适用场景:需要保留热点数据,访问模式偏向最近访问。
​volatile-lru
​行为:仅从 ​设置过期时间 的键中淘汰 LRU 键。
​适用场景:部分数据可永久保留,需淘汰临时数据。
​allkeys-random
​行为:随机淘汰任意键。
​适用场景:数据访问无明确规律,简单快速淘汰。
​volatile-random
​行为:随机淘汰 ​设置过期时间 的键。
​适用场景:需随机清理临时数据。
​volatile-ttl
​行为:优先淘汰 ​剩余存活时间(TTL)最短 的键。
​适用场景:需优先清理即将过期的数据。
​allkeys-lfu​(Redis 4.0+)
​行为:淘汰 ​最不频繁使用(LFU)​ 的键。
​适用场景:数据访问频率差异大,需保留高频访问数据。
​volatile-lfu​(Redis 4.0+)
​行为:从 ​设置过期时间 的键中淘汰 LFU 键。
​适用场景:临时数据中需保留高频访问部分。
 
38、阻塞、非阻塞、同步、异步?
0
阻塞/非阻塞:关注 调用方是否被挂起。
同步/异步:关注 结果通知由谁主动推动。
记住组合:
同步可以是阻塞或非阻塞(区别在于调用方是否等待)。
异步一定是非阻塞的(调用方无需等待)。
 
39、为什么Redis单线程快,它的单线程是如何实现的?
Redis利用epoll来实现IO多路复用,将连接信息和事件放到队列中,依次放到文件事件分派器,事件分派器将事件分发给事件处理器。多个连接共用一个阻塞对象,应用程序只需要在一个阻塞对象上等待,无需阻塞等待所有连接。当某条连接有新的数据可以处理时,操作系统通知应用程序,线程从阻塞状态返回,开始进行业务处理。因为文件事件分派器队 列的消费是单线程的,所以Redis才叫单线程模型。
0
0
 
40、Reactor模型和事件驱动模型?
Reactor模型是事件驱动模型的一种实现方式,是在高并发IO中的一种高效实践,核心思想是通过中央分发器监听所有IO事件,并在事件就绪时同步调用对应的处理器执行非阻塞的读写操作和业务逻辑。
 
41、为什么Redis选择单线程的Reactor?
1、可以使用单线程来避免锁竞争。
2、对内存友好,没有上下文切换开销,适合内存操作。
3、保证了操作的原子性,所有操作按顺序执行,无需考虑并发冲突问题。
 
42、什么是上下文切换?
上下文切换是指操作系统为了实现多任务并发,在不同的进程或线程之间切换执行的过程。例如我们一遍使用浏览器一遍听音乐,这个过程中操作系统是通过保存当前任务的运行状态(比如:寄存器、程序计数器等等)然后加载下一个任务的状态,让它们看起来像是同时在运行,在实际开发中这个过程频繁的上下文切换是很消耗资源的,应当尽量使用线程池或者异步IO来减少上下文切换的次数。
 
42、事件驱动模型--Select函数?
select函数是系统提供的一种基于事件驱动模型的实现,select函数用于实现IO多路复用的系统调用,允许程序在单个线程中监听多个文件描述符(如Socket套接字)的状态变化,例如:是否可读、是否可写或异常。它的核心原理是将一组文件描述符传递给内核,内核轮询这些文件描述符,在某个文件描述符就绪时通过内容通知应用程序。不过select在监听大量文件描述符时性能较差,因为每次都要重新传递所有文件描述符且内核遍历的时间复杂度是O(n),在高并发场景下并不适用,通常使用poll和epoll替代。
优点:
1、适用性广,几乎所有操作系统都支持,例如场景的win、mac、linux都支持。
2、简单易用,适合少量链接的场景。
缺点:
1、性能较差,每次都需要将所有的文件描述符传递到内核,频繁调用性能开销大,时间复杂度是O(n)。
2、文件描述符数量限制(通过bitmap位图实现),例如在Linux中默认是1024,无法扩展。
3、状态无法复用,每次调用后需要重新设置fd的状态,无法复用之前的状态。
 
43、事件驱动模型--Poll函数?
Poll函数是对Select函数的一种改进,也是事件驱动模型的一种实现,允许单个线程监听多个文件描述符(如Socket套接字)状态的变化,例如:是否可读、是否可写或异常。通过传递一个pollfd结构体数组到内核中,内核通过轮询检查这些描述符的可用性,在某个描述符就绪的时候通过内核通知应用程序。适用于中等并发的业务场景。
优点:
1、没有文件描述符数量限制,使用动态数组替代了bitmap位图,可以监控数万连接。
2、无需计算大量文件描述符,直接传递动态数组的长度,避免了select中存在的缺点。
缺点:
1、性能瓶颈,和select一样需要通过轮询来遍历所有文件描述符,时间复杂度是O(n)。
2、跨平台性差,主要在Linux和Unix中使用,win、mac无法使用。
3、状态无法复用,每次调用后需要重新设置fd的状态,无法复用之前的状态。
 
44、事件驱动模型--Epoll函数?
Epoll是一种高效的IO多路复用机制,通过红黑树来管理文件描述符(高效的增删改查),通过就绪列表直接保存并返回活跃的文件描述符,无需遍历全部文件描述符,时间复杂度是O(1),适用于大量请求下的高并发场景。支持水平触发和边缘触发两种模式。相较select、poll两种函数,epoll避免了重复传递和遍历文件描述符的开销,因为更适合高并发场景,可以作为高并发场景的首选。但它仅适用于Linux,对于跨平台的需求优先选择poll或者select。Redis、Nginx中都使用了epoll。
水平触发:只要数据未读完,每次调用都会通知。
边缘触发:仅在状态变化时通知一次,需要非阻塞IO循环读写,性能更高。
相较于select、poll它的时间复杂度更低是O(1),没有文件描述符的限制,支持水平触发和边缘触发两种模式
 
45、select、poll、epoll都是IO多路复用机制,它们的核心差异在哪里?
1、select使用bitmap位图监控文件描述符,文件描述符有数量限制且存在大量的遍历和轮询性能是O(n),适用于低并发或跨平台的业务场景。
2、poll对select进行了改进,使用动态数组取代了bitmap位图,文件描述符数量不在受限制,但性能和select一样都是O(n),适合中等并发的业务场景。
3、epoll是Linux的专属定制方案,通过红黑树管理文件描述符和就绪列表存储活跃的文件描述符实现了O(1)的时间复杂度,支持水平触发和边缘触发,可以轻松地处理数万连接数,适合高并发的业务场景。
0
 
46、为什么Redis更建议部署在Linux中?
因为Linux中可以很好的支持epoll,它是Redis高效的一个核心要素,在常见的win、mac系统中并不支持epoll,因此在其它的系统中使用Redis无法发挥它的最大性能,而且Redis官方文档中也明确说明了要使用Linux。
posted @ 2025-07-25 18:21  茴香饺子、  阅读(20)  评论(0)    收藏  举报