Java后端面试:Redis高频30题与实战解析

发布时间:2026/9/28 8:14:52
Java后端面试:Redis高频30题与实战解析 最近跟几个准备跳槽的朋友聊大家一致的感觉是2026年的Java后端面试里Redis几乎是躲不掉的一关。不论你面的是初级还是资深数据类型、缓存穿透、分布式锁这老三样基本必考区别只在于追问的深度。这篇内容是我结合近期各大厂面试记录整理出的30道Redis高频题每题都从Java实战的角度给到答题思路和关键细节不是让你干背八股而是让你在面试现场接得住追问。题目的组织方式是按主题走的从底层数据结构到持久化、高可用、缓存治理、分布式锁最后到面试官最爱追问的加分细节。建议你别只把它当题库刷最好每一题都能用自己的话讲一遍面试的时候才不会被当成背答案。1. 数据类型与底层结构先搞清楚Redis到底怎么存数据1.1 五种基础类型与底层实现String、List、Hash、Set、ZSet分别是什么第1题Redis有哪些数据类型底层分别用什么结构实现这是Redis面试最开场的问题也最容易被低估。简单回答五种基本类型String、List、Hash、Set、ZSet加上Bitmaps、HyperLogLog、Geo、Stream是最起码的。但面试官真正想听的是底层结构。我用表格总结一下各类型的底层实现这样你在脑子里会有一个整体图景。类型底层数据结构老版本底层数据结构新版本演进说明Stringint / SDSint / SDS整数用int编码非整数用SDS动态字符串Listziplist linkedlistquicklistquicklist是多个ziplist组成的双向链表Hashziplist / hashtablelistpack / hashtable小哈希用listpack节省内存Setintset / hashtableintset / hashtable全整数且量小时用intsetZSetziplist / skiplistlistpack / skiplist有序集合跳表哈希表组合回答时需要提到Redis 7.0之后ziplist逐步被listpack替代主要是因为ziplist在极端情况下的连锁更新问题。你要是能主动提这个版本差异面试官会明显觉得你是跟进过新版的。数据结构的选择不是固定的Redis内部会按元素数量和元素大小做编码转换。比如Hash当键值对数量不到512默认hash-max-listpack-entries且单个key/value长度不超过64字节时用listpack超了就转成真正的hashtable。这个小用紧凑结构大用高效结构的思路本质是内存和性能的权衡。1.2 String的SDS设计为什么不直接用C语言的字符串第2题Redis的String底层为什么用SDS而不是直接存C字符串这道题考的是C语言功底和设计思维。传统C字符串用char[]存储以\0作为结束符随之而来有三个问题第一获取长度是O(n)需要从头遍历。SDS在结构体头里维护了一个len字段获取长度是O(1)。这在Redis这种追求极致的场景里很重要因为STRLEN这种命令会非常频繁。第二C字符串二进制不安全。只要内容里包含\0字符串就会被截断。SDS用len来判断长度而不是靠结束符所以可以存\0、图片、序列化对象等任意二进制数据。做缓存的时候我们经常会把Java对象序列化后放进去这个二进制安全特性就特别关键。第三缓冲区溢出的风险。C字符串拼接前需要手动分配足够内存否则会越界覆盖相邻内存。SDS的API会检查剩余空间不够就自动扩容同时对字符串的修改还带空间预分配和惰性空间释放的优化避免频繁申请内存。考察频率很高因为这条链路可以一直从SDS问到内存分配器jemalloc再问到内存碎片治理属于典型的一道题问穿一整个知识面。1.3 跳表与红黑树之争ZSet底层为什么选跳表第3题ZSet的底层为什么用跳表而不是红黑树这题我几乎每次模拟面试都会问。先说结论跳表和红黑树的平均查找复杂度都是O(logN)但Redis的场景里跳表有几个优势。跳表实现起来简单得多区间查询的时候跳表只需要沿着next指针往下走就行而红黑树做范围查询需要中序遍历实现复杂。ZSet最核心的操作就是排序和范围查询比如ZRANGEBYSCORE跳表是天然贴合这个场景的。还有一个容易被忽略的点跳表的插入、删除只需要调整指针不需要像红黑树那样为了维持平衡做旋转和变色。虽然两者的平均复杂度一样但跳表在并发修改场景下的实际开销更稳定。需要补充的是ZSet不是单独只用跳表而是哈希表跳表的组合哈希表负责按member快速定位分数跳表负责按分数排序和范围查询。另外Redis还用了dict里面的顺序指针这个细节你要是能说出来面试官会很加分。1.4 高级数据类型Bitmap、HyperLogLog、Geo能解决什么问题第4题Bitmap、HyperLogLog、Geo的底层原理和适用场景是什么先说Bitmap。它不是一个独立的Redis数据结构本质还是String只是把它当成位数组来用。底层是SDS存储的二进制位串可以执行SETBIT、GETBIT、BITCOUNT、BITOP这类位操作。一个亿级用户的在线状态用Bitmap只需要100000000/8/1024/1024≈12MB左右非常省内存。HyperLogLog是基数统计神器。基于概率算法标准误差0.81%最典型的场景是UV统计。一个HyperLogLog最多占用12KB内存却能统计2^64级别的基数。你不能拿到具体是谁只能拿到大约有多少个不重复的。用Java做UV统计的时候一个亿级用户量也不会吃太多Redis内存。Geo底层用的是GeoHash把经纬度二维坐标编码成一维的字符串前缀越接近说明地理位置越近。配合GEORADIUS命令可以快速算出附近的人、附近的店铺。要注意的是Geo在极端情况比如极点附近附近查询准确率会下降生产上一般不会拿它做厘米级精确定位更多是做LBS的粗筛。第5题你项目里实际用过哪些高级类型这题是送分题但也最容易翻车。我自己的经验是用HyperLogLog做每日UV统计在金数据报表上直接展示用Bitmap做用户签到一年365天就是365个bit一个人一年签到记录不到46字节用Stream做异步任务队列比直接依赖MQ轻量得多你要是能把这些场景结合你简历上的业务讲出来比单纯背定义强十倍。1.5 热Key和大Key面试官最爱追问的场景题第6题什么叫热Key线上遇到热Key怎么处理热Key指某个key的访问量QPS极高比如一个爆款商品的详情缓存、一个明星的粉丝列表。单台Redis实例上一个key的访问量一旦超过实例能承受的极限整个实例都会出问题因为Redis是单线程模型一个key过热会拖垮其他key。应对手段我梳理过核心就四类热Key打散在key后面拼随机后缀拆成多个副本分别分布在不同的分片节点上本地缓存把热点数据短暂缓存到JVM内存里比如用Caffeine做一级缓存请求先打本地再打Redis读写分离把读流量分流到从节点限流降级对读取热Key的接口做熔断防止流量全部压到Redis面试时最好举个例子。比如我做商品详情页的时候发现某个秒杀商品的详情查询QPS直接飙到几十万就是靠JVM本地缓存Redis多副本扛住的。这里有一个细节本地缓存一定要设置较短的过期时间比如10秒否则数据新鲜度会出问题。第7题什么是大Key如何发现和治理大Key通常指单个key存储的value很大。业界比较接受的判断标准是String类型的value超过10KB或者集合类型的元素数量单key超过5000个。大Key危害明显删除的时候可能阻塞Redis主线程网络传输的时候耗费带宽内存不均匀导致集群上热点分片。发现的手段主要有三种redis-cli --bigkeys命令扫描通过INFO memory观察内存分布在客户端做DEBUG OBJECT查看序列化长度治理的话String类型可以考虑压缩比如Snappy或者拆成多个小key集合类型可以按业务维度拆分为多个子集合比如按用户ID哈希到不同的key上。删除大Key不要直接DEL极端情况下会阻塞实例要用UNLINK异步删除或者分批SCAN删除。这部分一定要结合你自己的运维经验来讲比如你曾经因为删一个大KeyRedis发生短暂卡顿最后怎么排查出来的——这种真实故事面试官非常买账。2. 持久化与主从复制Redis重启了数据还在不在2.1 RDB和AOF取舍背后的原理第8题Redis的持久化方式有哪些RDB和AOF分别适合什么场景Redis默认单机部署也有持久化目的是防止重启丢数据。两种方式RDB是快照式持久化。BGSAVE或者自动触发的时候Redis fork一个子进程把当前内存里的全量数据写入一个二进制dump文件。RDB文件的恢复速度非常快适合做冷备也适合做灾备恢复。但它的缺点是会丢失两次快照之间的数据丢失量取决于你配置的save策略极端情况可能丢几分钟的数据。AOF是追加式日志。它把每一条写命令以Redis协议格式追加到文件末尾重启时重放命令来恢复数据。AOF保证数据安全性的能力更强因为你可以配置成appendfsync always每个命令都刷盘或者appendfsync everysec每秒刷一次。生产上我见过太多人在这块含糊其辞。你至少要会回答AOF文件体积一般比RDB大恢复速度比RDB慢AOF会出现文件膨胀的问题需要依赖重写机制压缩。很多公司做了不做持久化的配置来换极致性能但如果业务不能接受重启丢数据千万别这么玩。第9题AOF重写机制是干嘛的为什么需要重写AOF文件记录的是每条写命令同一个key可能被反复写很多次。比如SET a 1、SET a 2、SET a 3AOF文件里存了三条记录但实际上最终只需要SET a 3这一条。重写就是把这些冗余的命令压缩成恢复当前数据集所需的最少命令集合。这里有个底层机制值得单独讲AOF重写不是基于现有AOF文件做的而是基于当前内存中的数据状态生成的。BGREWRITEAOF触发后Redis fork子进程用子进程遍历内存数据生成新AOF。与此同时主线程的新写命令会被记录到AOF重写缓冲区子进程完成后把缓冲区的增量命令追加到新文件尾部。如果你能把这个流程说清楚说明你真的读过源码。2.2 混合持久化RDB和AOF的结合优势第10题什么是混合持久化为什么Redis 4.0之后推荐开启混合持久化是Redis 4.0引入的方案配置项是aof-use-rdb-preamble yes。它的做法是AOF文件头部放一个RDB快照RDB之后追加AOF增量日志。这样做的好处直接解决了AOF恢复慢的问题。因为加载的时候先读RDB部分就能把大部分数据快速恢复剩下的增量日志再重放一下就完了。同时它还解决了RDB丢失数据多的问题两次持久化之间的写命令依然落地为AOF日志。生产建议是只要你的Redis不是纯缓存场景我都建议开混合持久化。配合appendfsync everysec绝大部分场景能做到每秒级别的数据可靠性同时恢复速度又不太受影响。第11题RDB持久化的时候Redis会阻塞吗fork子进程有什么坑SAVE命令会阻塞主线程BGSAVE则不会。后端开发同学常被追问的就是BGSAVE了。BGSAVE会调用fork创建一个子进程由子进程负责写RDB文件。fork这个操作本身是有开销的尤其在内存很大比如20GB的实例上fork瞬间要拷贝页表会短暂阻塞主线程。这个阻塞时间通常几十到几百毫秒取决于内存大小和CPU性能。更深一层的机制是写时复制Copy On Write。子进程生成RDB期间主线程收到的写请求会修改页表对应的内存页才会复制一份出来所以子进程生成的RDB文件代表的是fork那一刻的数据快照不会因为后来的修改而变化。这就意味着如果子进程生成RDB期间主线程的写入量大额外的内存开销会很大。面试时能说到这个层次这道题基本就稳了。2.3 主从复制全量同步与增量同步的内幕第12题Redis主从复制的原理是什么全量复制和增量复制分别发生在什么时候主从复制靠的是PSYNC协议。从节点启动后向主节点发送PSYNC命令带上主节点的runid和自己的复制进度offset。全量复制的场景是第一次复制或者主节点发现从节点的复制积压缓冲repl_backlog)里已经没有它需要的偏移量了。流程是主节点执行BGSAVE生成RDB快照同时把新写命令缓存到缓冲区然后通过socket传给从节点从节点清空自己的旧数据加载RDB再追加重放缓冲区里的命令。增量复制发生在从节点断线重连之后只要偏移量还落在主节点的repl_backlog_buffer里主节点就只把缺失的命令发过去不需要重新全量。这里有一个容易被问到但很多资料没讲清楚的当从节点断开时间太长积压缓冲被覆盖了就会退化为全量复制。所以生产上repl-backlog-size不能设置太小建议至少设置为10MB到64MB具体看写入量。第13题主从复制会有延迟吗怎么处理一定有延迟。因为复制是异步的主节点写完就返回客户端从节点靠repl-backlog异步拉取。对于读多写少的业务从节点可能读到旧数据。处理方案有几种强制走主库读把关键数据读操作路由到master或者在代码层面做短暂等待后再读还有一种实用做法是设置min-replicas-max-lag来限制从节点的滞后上限。实际项目里我们做订单详情查询主从架构下就直接要求读主库因为订单数据一致性优先级最高。第14题怎么用Docker部署一套Redis主从你踩过什么坑这个题最近在面试里出现的频率上升了尤其是那些简历里写了Docker的候选人。我用一个简单命令说明常用做法# 先拉镜像 docker pull redis:7.0-alpine # 启动主节点 docker run -d --name redis-master -p 6379:6379 -v /data/redis-master:/data redis:7.0-alpine redis-server --appendonly yes # 启动从节点并指定主节点地址 docker run -d --name redis-slave -p 6380:6379 -v /data/redis-slave:/data redis:7.0-alpine redis-server --slaveof redis-master 6379注意这里有个经典的坑如果在容器间用--slaveof redis-master 6379必须保证从节点容器能和主节点容器互相通信所以你需要提前建一个Docker网络docker network create redis-net docker network connect redis-net redis-master redis-slave否则从节点会一直报MASTER - REPLICA sync started: Cant connect to master。另一个坑是数据目录权限Redis容器内的redis-server默认以redis用户运行宿主机挂载的目录如果权限是root容器会写不进去日志会一直报权限错误。我在本地验证的时候直接挂载目录后忘加--user排查了半天才发现是权限问题。Docker部署本身不难难的是网络、权限、持久化这三个点。你面试时能主动讲出这些坑说明你确实在生产环境里操作过。3. 高可用与集群单点挂了怎么办3.1 哨兵机制如何自动故障转移第15题Redis哨兵机制的原理是什么它会自动切换主从吗哨兵Sentinel是一个独立运行的进程用来监控主从节点的健康状况。多个哨兵会组成一个哨兵集群即使一个哨兵挂了整个监控体系依然能工作。流程是这样的哨兵每秒给主节点发送PING如果主节点在down-after-milliseconds时间内没响应当前哨兵就把它标记为主观下线。接着它向其他哨兵发起投票当多个哨兵都认为主节点下线了超过quorum就升级为客观下线。然后哨兵集群选举出一台leader注意是哨兵之间的选举由它执行故障转移从候选从节点中选一个提升为新主节点配置其他从节点改为复制新主。整个过程对外没有手动干预。这里有一个容易混淆的概念哨兵的作用不是让客户端直连哨兵读写而是客户端通过哨兵获取当前主节点的地址。Java项目里用Lettuce或者Jedis接入哨兵模式是直接连接Sentinel的节点列表让它返回master的地址。第16题主从切换时出现脑裂怎么办脑裂是指网络分区导致主节点和从节点各自独立存活比如主节点所在机器网络异常但节点进程还活着。哨兵认为主挂了从节点被提升为新的主节点但老主节点恢复网络后又变回一个独立的master两个master同时接收写入。如果业务对数据一致性要求高脑裂带来的影响是灾难性的。Redis官方给出的方案是配置两个参数min-replicas-to-write 1主节点接收写请求前至少要有1个健康从节点min-replicas-max-lag 10从节点复制延迟超过10秒主节点拒绝写入这样当网络分区导致主节点和从节点失联后主节点的写入能力会被快速降级把脑裂期间产生的脏数据量限制到最小。面试时能说出官方这两个配置项并且能解释为什么要限制主节点写入而不是一味追求可用性说明你理解分布式系统的权衡。3.2 集群模式16384个槽位到底怎么分第17题Redis Cluster为什么把数据分成16384个槽位为什么不是65536Redis Cluster采用一致性哈希的改良方案——固定槽位。集群把整个数据集映射到0~16383共16384个槽位每个节点负责一部分槽位。写入一个key时客户端计算CRC16(key)%16384得到槽位再根据槽位找到对应的节点。为什么偏偏是16384有几个原因。第一Redis实例之间的心跳包需要携带自己负责的槽位信息16384个槽位对应的bitmap是16384/82048字节2KB心跳包可以控制在非常小的体积如果改成65536个槽位bitmap变成8KB心跳包会增大很多。第二集群的节点规模通常不会超过1000个16384个槽位用来做数据分布已经足够均匀再多没有实际收益。第三槽位越少节点重新分片和迁移的粒度就越大维护成本越低。这个题回答好了是区分背八股和真正理解设计的分水岭。第18题客户端访问集群时如果key不在当前节点会怎么样当客户端向任意节点发送请求节点会通过CRC16(key)%16384算出槽位判断是否为本地槽位。如果不是向客户端返回MOVED 槽位号 目标节点IP:端口错误。这里有一个关键点Java客户端比如JedisCluster在收到MOVED之后会更新本地路由缓存下次请求直接走正确的节点。还有一种情况是槽位正在迁移节点会返回ASK错误表示数据正在从源节点迁往目标节点客户端需要先向目标节点发送ASKING命令再执行真正的操作。MOVED是永久性重定向ASK是临时性重定向这两个概念区分不开面试基本扣分。3.3 集群部署的实操心得与常见坑第19题部署Redis Cluster有哪些注意点生产环境至少要几个节点官方推荐集群最少要3个主节点每个主节点配一个从节点也就是3主3从。因为Redis Cluster要求至少3个master才能形成可用的集群而主节点挂掉时从节点能顶上来做主备切换。部署时最容易踩的坑包括集群节点之间需要开放两个端口一个是客户端通信端口另一个是集群内部总线端口默认端口10000防火墙不放开会导致Gossip通信失败cluster-enabled yes必须显式开启默认是单机模式槽位分配需要覆盖全部16384个槽位否则集群状态是FAIL无法提供服务节点之间要保证时钟基本同步NTP时间偏移过大可能导致主从切换判断异常我之前在生产环境用redis-cli --cluster create一次创建完成后把cluster-node-timeout调成默认值5000毫秒。后来业务高峰出现一次节点抖动集群花了快10秒才完成故障转移业务侧明显感觉到了超时。后来把超时参数调小、并加了每个节点的告警监控才把故障影响控制住。做高可用不只是把集群搭起来就完事故障转移耗时也要纳入设计。4. 缓存治理穿透、击穿、雪崩和数据一致性4.1 经典三大缓存问题穿透、击穿、雪崩第20题什么是缓存穿透如何解决大量请求查一个一定不存在的key请求绕过缓存直接打到DB数据库压力陡增。最常见的是恶意攻击故意用不存在的ID刷接口。第一种方案是缓存空结果。查询DB为空时在Redis里存一个空值TTL设短一些比如60秒这样同样的查询不会直接打DB。缺点是缓存了大量空值而且数据变了之后空值缓存可能还没过期。第二种方案是布隆过滤器。启动时把所有合法ID加载进布隆过滤器请求过来先判断ID是否可能存在不存在就直接返回根本不打DB和Redis。注意布隆过滤器的特性它只能判断绝对不存在不能判断一定存在存在误判。我项目里一般用Redisson的RBloomFilter初始化时指定预计插入量和误判率。这个方案对缓存穿透的防御最彻底。第21题缓存击穿和雪崩的区别是什么怎么应对缓存击穿指一个热点key过期瞬间大量并发请求同时打到了DB就像一根针把缓存打了个洞。缓存雪崩则是大量key同时过期导致大量请求同时打到DB。两者的核心都是缓存失效瞬间的并发冲击区别在于涉及的范围击穿是单个热点key雪崩是大面积key。击穿的解法互斥锁Mutex缓存失效时先尝试SETNX获取重建锁只有一个线程能去查询DB并回填缓存其他线程短暂等待后重新读取缓存。逻辑过期缓存里不设置TTL而是存一个逻辑过期时间。后台异步线程负责检测到逻辑过期后重建缓存所有请求直接返回旧缓存不会打到DB。雪崩的解法TTL加随机值把缓存过期时间设置为基础值随机偏移量避免所有key在同一时刻过期。比如固定5分钟随机加0到60秒。多级缓存Redis之上加一层本地缓存CaffeineDB之上加Redis层层挡。熔断限流极端情况下直接配置Sentinel熔断保护DB不死。面试的时候最好别只背方案而是说清楚我们当时为什么选互斥锁而不是逻辑过期。比如逻辑过期适合数据一致性要求不太高的场景允许短暂读到旧数据如果业务要求尽可能一致就要用互斥锁虽然多了一点阻塞时间但不会读到旧值。4.2 缓存与数据库一致性先更新库还是先删缓存第22题Cache Aside模式下为什么是先更新数据库再删除缓存而不是先删缓存Cache Aside是最常用的缓存模式读的时候先读缓存不命中再读DB并回填写的时候更新DB然后删除缓存。先更新DB再删缓存逻辑上是安全的因为如果先删缓存而随后更新DB失败缓存是空的读到的是旧DB数据吗其实这里存在窗口问题。我直接给结论先删缓存再更新DB如果更新DB失败DB是旧值缓存又是空的下一次读会把旧DB值重新写进缓存不一致的时间窗口非常长。先更新DB再删缓存即使删缓存失败顶多缓存里是旧值但下一次读还会尝试删一次缓存不一致窗口缩短为一个操作的时间。在Java高并发项目里重点不是选哪个顺序而是删缓存失败了怎么办。生产上我们最终的兜底方案有两层一是利用MQ异步重试删除缓存二是通过监听数据库的Binlog比如Canal来触发缓存删除。第23题延迟双删是什么为什么需要延迟延迟双删是处理并发场景的一种补偿方案。流程是先删除缓存再更新DB休眠一小段时间比如500毫秒再次删除缓存。这么做的原因是第一次删除和DB更新之间可能有其他线程读了DB旧值并回填到缓存导致缓存里变成旧值。如果第二次删除能把这些写进去的旧缓存清掉就能规避这个问题。但延迟双删有几个硬伤休眠时间是拍脑袋定的不好保证覆盖所有并发窗口删缓存是异步的如果第二次删除失败依然会不一致。所以别把延迟双删当银弹它是“成本低但效果有限”的兜底。更可靠的做法是Canal订阅Binlog MQ消息 监听删除缓存虽然链路长但一致性窗口可以控制得很小。面试时能这样分层次说会显得你真调研过。4.3 缓存预热与Redis序列化的Java坑第24题怎么做缓存预热系统启动时缓存全空怎么办缓存预热是把热点数据在系统启动前一次性加载到缓存中。最简单的方式是Spring Boot项目里用一个ApplicationRunner或CommandLineRunner启动时执行数据加载逻辑。Component public class CachePreheatRunner implements CommandLineRunner { Autowired private StringRedisTemplate stringRedisTemplate; Override public void run(String... args) { ListItem items itemMapper.selectAllHotItems(); for (Item item : items) { String key item:hot: item.getId(); // 用JSON序列化成String后写入预留2小时过期时间 stringRedisTemplate.opsForValue().set(key, JSON.toJSONString(item), 2, TimeUnit.HOURS); } } }需要注意的是预热时要分批执行比如每次500条避免一次性写入大量key导致Redis主线程阻塞。更稳妥的做法是把预热做成一个独立任务在流量进入之前提前5到10分钟预热完成而不是等服务刚启动就跟业务流量抢资源。第25题Java项目里Redis序列化经常踩什么坑这个问题实战价值极高几乎每次面试都会被问到。Spring Data Redis默认的序列化器是JDK序列化它的后果是key和value在Redis客户端里看着都是乱码比如\xAC\xED\x00\x05t\x00\x0...。排查问题和人工清理key的时候会非常痛苦。常见的序列化方案有StringRedisSerializervalue必须是String适合纯字符串缓存Jackson2JsonRedisSerializer序列化成JSON可读性好但反序列化时需要带上类型信息跨系统时要注意GenericJackson2JsonRedisSerializer会在JSON里写入class字段反序列化时能还原成具体类型但会额外占一点空间我自己项目里的选择是key统一用StringRedisSerializervalue用GenericJackson2JsonRedisSerializer。这样key在客户端里可读value又能还原成Java对象。如果你用Redis Desktop Manager这类可视化客户端查看数据会发现人类能读懂的JSON排查问题轻松很多。5. 分布式锁从SET NX到Redisson源码级拆解5.1 手写一个可靠的分布式锁第26题Redis分布式锁最基本、可靠的实现方式是什么面试必考。最基本也最正确的写法用一条命令完成加锁String lockKey lock:order: orderId; String lockValue UUID.randomUUID().toString(); Boolean locked stringRedisTemplate.opsForValue() .setIfAbsent(lockKey, lockValue, 30, TimeUnit.SECONDS);关键点有三个。第一setIfAbsent就是SET key value NX PXNX保证只有不存在时才写入px设置过期时间。必须用一条原子命令否则如果先SETNX再EXPIRE中间进程崩溃会把锁变成永不过时的死锁。第二value必须是一个随机唯一值通常用UUID。这保证了解锁的时候只删除自己持有的锁不会误删别人的锁。第三释放锁要用Lua脚本判断value一致后再删除String script if redis.call(get, KEYS[1]) ARGV[1] then return redis.call(del, KEYS[1]) else return 0 end; stringRedisTemplate.execute( new DefaultRedisScript(script, Long.class), Arrays.asList(lockKey), lockValue );为什么不能先GET再DEL因为这两步不是原子的。线程A拿到锁后还没执行完锁过期了线程B拿到了新锁此时线程A执行完业务去DEL就会把线程B的锁误删。有了Lua脚本里的value判断这个隐患就消除了。第27题锁过期了但业务还没执行完怎么办这是分布式锁最经典的续期问题。锁设置30秒过期但业务执行了40秒锁已经失效另一个线程进来了等于锁形同虚设。手写方案是搞一个后台守护线程定期检查锁是否还在自己手里如果还在就执行PEXPIRE续期。但自己写这个逻辑容易出错所以生产上更建议直接用Redisson。Redisson的看门狗Watch Dog机制会自动处理续期默认锁超时时间30秒加锁成功后后台线程每10秒检查一次如果锁还在就自动续期到30秒。业务执行完释放锁看门狗线程也随之取消。源码层面主要看RedissonLock的renewExpiration和unlockAsync面试能把这个机制讲清楚基本上就能回应为什么引入Redisson。5.2 Redisson锁的实现与Redlock争议第28题Redisson是怎么实现分布式锁的Redlock在多节点场景下真的可靠吗Redisson的锁实现基于RedissonLock底层还是用Lua脚本保证原子性。获取锁的脚本会执行hset和pexpire而且它支持可重入同一个线程可以重复获取同一把锁内部会维护一个计数器。可重入在Java里是非常重要的特性因为一个带锁的方法里可能又调用了另一个带锁的方法。Redlock是Redis官方提出的多节点分布式锁算法向5个独立的Redis master依次加锁只有超过半数节点比如3个加锁成功并且加锁总耗时小于锁有效时间才算加锁成功。Redlock的争议一直很大尤其是分布式系统专家Martin Kleppmann撰文批评过它的安全性问题它依赖时钟假设在GC停顿、进程暂停场景下会产生两个客户端同时持有锁的情况。antirezRedis作者也专门反驳过双方至今没有完全达成一致。面到这个级别面试官其实不是在考标准答案而是看你能不能理解分布式锁在可用性和一致性之间的权衡。我的总结是单机或主从场景下基于Redisson的官方实现完全够用多节点Redlock一般用于对锁安全性极度敏感的场景但工程上引入Redlock的复杂度很高大部分业务是不需要的。5.3 分布式锁在秒杀与幂等场景中的应用第29题如何用Redis做秒杀场景的库存扣减怎样保证不超卖秒杀最核心的问题是高并发下库存只能减到0不能为负。用Java的synchronized或JVM锁解决不了分布式场景的问题得用Redis的原子操作。最推荐的方式是直接用Lua脚本把库存判断和扣减合在一起执行因为Redis执行Lua脚本是原子性的不存在并发间隙。-- KEYS[1] 是库存key例如 stock:1001 local stock tonumber(redis.call(get, KEYS[1])) if not stock or stock 0 then return -1 end redis.call(decr, KEYS[1]) return 1Java侧调用String script local stock tonumber(redis.call(get, KEYS[1])) if not stock or stock 0 then return -1 end redis.call(decr, KEYS[1]) return 1; DefaultRedisScriptLong redisScript new DefaultRedisScript(script, Long.class); Long result stringRedisTemplate.execute(redisScript, Arrays.asList(stock: skuId));当result 1时表示扣减成功接下来再异步落库生成订单。如果DB操作失败再用补偿脚本回滚库存。这里有一个高频追问既然Redis已经扣减成功了为什么还要DB库存答案是Redis只是一个前置校验和预扣最终的订单和库存数据必须落到数据库作为事实依据Redis扣减失败可以直接拦截但不能替代数据库。真正的秒杀系统还会加队列削峰、限流、抢购标记等设计这是另外一个很大的话题了。第30题如何用Redis做接口幂等重复请求怎么拦截接口幂等的经典实现是令牌Redis的SETNX。用户提交订单时服务端先为这个请求生成一个唯一的幂等ID比如UUID或分布式ID客户端提交请求时带着它。服务端处理前执行Boolean firstRequest stringRedisTemplate.opsForValue() .setIfAbsent(idempotent: idempotentKey, 1, 5, TimeUnit.MINUTES); if (!firstRequest) { // 之前已经处理过直接返回重复提交提示 return 请勿重复提交; }SETNX成功说明这个请求是第一次来后续同样的幂等ID会直接命中已存在key从而被拦截。这种方案在防止用户重复点击、接口重放时非常有效实现成本极低不需要额外的数据库表。需要提醒的是幂等ID的过期时间要结合业务最长处理时间来设置过期太短会出现第一次请求还没处理完第二次请求就穿进来了的情况。6. 面试追问与避坑技巧怎么把答案讲得不像背的6.1 Redis为什么单线程还能这么快这个问题经常脱离30道题单独被追问我建议你准备一个完整的回答链路。Redis的核心处理模型是单线程事件循环网络IO模块基于多路复用技术Linux上是epollmacOS上是kqueue能在一个线程里监听成千上万个socket连接做到有事件才处理没有事件就阻塞等待。单线程带来的最大优势是避免了多线程上下文切换和锁竞争同时执行命令本身就是原子性的。需要补充的是Redis 6.0之后引入了多线程IO主要用于处理网络读写但核心命令执行仍然是单线程的。面试官如果追问多线程IO解决了什么问题你要能答出解决的是大流量下网络带宽和内核socket读写对CPU的消耗而不是把命令执行并行化。6.2 排除问题时的加分技巧日志、INFO和可视化客户端面试聊项目时如果能顺带提一嘴排查Redis问题的具体工具链会显得很务实。我常用的是这几个手段INFO命令看内存、命中率、客户端的连接数。重点是INFO memory里的used_memory和mem_fragmentation_ratio内存碎片率碎片率大于1.5需要考虑重启或调整jemallocSLOWLOG查看慢命令。执行SLOWLOG GET 10如果发现大量KEYS *、HGETALL这种大key操作系统性能必然受影响MONITOR实时监控所有命令用来定位突发key访问可以但生产环境千万不要长时间开启它会让吞吐量断崖式下跌可视化客户端Redis Desktop Manager和Another Redis Desktop Manager都很常用我自己的偏好是后者界面更简洁支持连接池和JSON格式化选客户端唯一的标准就是支持SSH隧道方便查看key的TTL和序列化类型还有一个容易出现的问题Java项目里日志打出来的命令和实际Redis收到的命令不一致。常见原因是Spring Data Redis的RedisTemplate自带一些额外操作比如在opsForValue().set()的value是对象时没有配置序列化器就默认走JDK序列化。日志里看的是调用代码Redis看的是序列化后的字节流所以排查时要先确认序列化器配置。6.3 关于Redis复习规划的一点个人体会整理完这30道题之后我自己的一个强烈感受是Redis面试题早就不是靠背能过关的了。现在面试官更愿意从一个点切入比如你聊分布式锁他会一路追问到Lua脚本、原子性、续期、时钟、网络分区几十个问题顺着就出来了。所以建议大家在复习时不要按题号一个接一个背而是按应用场景来串线缓存一致性场景下面挂哪些题高可用场景下面挂哪些题分布式锁场景下面挂哪些题这样面试时思路才不会断层。最后再分享一个面试时的实操心得遇到不会的问题千万别直接说不知道试着从自己的经验出发推导。比如没接触过Redlock你可以说我知道它的基本思路是向多节点加锁但没在线上验证过所以不好评价它的可靠性。这种回答比你硬编一个答案要真诚得多面试官通常也愿意引导你继续往下聊。Redis的内容深不见底把自己的知识体系梳理成一条线比收藏100道题有用得多。

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询