《JAVA面经实录》- Redis面试题
Redis 最全最新面试题及答案(贴合深岗,含底层+踩坑+追问)
一、基础
1. 什么是 Redis?它的核心特点是什么?
答案:Redis(Remote Dictionary Server)是一款基于内存的高性能键值对数据库,支持多种数据结构,兼具持久化、高可用、分布式能力,核心用于缓存、会话存储、消息队列等场景。
核心特点(底层支撑+生产价值):
-
基于内存操作,读写速度极快(单机QPS可达10万+),底层依赖epoll/kqueue的I/O多路复用模型,避免阻塞IO开销;
-
支持多种数据结构(String、Hash、List等9种),灵活适配不同业务场景,底层每种结构对应专属编码(如String的int、embstr、raw编码);
-
支持持久化(RDB+AOF),解决内存数据易丢失问题,可根据业务选择合适的持久化组合;
-
支持主从复制、哨兵、集群,实现高可用和水平扩展,满足生产环境的稳定性需求;
-
支持事务、Lua脚本,保证操作的原子性,适配复杂业务场景;
-
单线程模型(核心操作单线程,IO多线程),避免线程切换开销,同时保证操作的原子性。
生产踩坑:误以为Redis单线程就不会卡顿——实际单线程下,若执行慢查询(如keys *、hgetall 大Hash),会阻塞所有请求,导致服务不可用;需避免慢查询,必要时用scan替代keys。
面试追问:Redis单线程为什么能支持高并发?(核心:内存操作本身很快,I/O多路复用解决IO阻塞,避免线程切换和锁竞争,单线程反而减少了开销;补充:Redis 6.0后引入IO多线程,仅负责读写IO,核心命令仍单线程,进一步提升吞吐量)。
2. Redis 和 Memcached 的区别?生产中为什么选 Redis?
答案:两者均为内存缓存工具,但核心差异集中在数据结构、持久化、高可用等维度,生产优先选Redis的核心原因是其功能更适配复杂业务场景,具体区别如下:
|
对比维度 |
Redis |
Memcached |
|---|---|---|
|
数据结构 |
支持String、Hash、List、Set等9种,灵活适配多场景 |
仅支持String,功能单一 |
|
持久化 |
支持RDB、AOF,可避免内存数据丢失 |
不支持持久化,重启后数据全部丢失 |
|
高可用 |
支持主从、哨兵、集群,可实现故障自动切换 |
不原生支持高可用,需依赖第三方工具(如Keepalived) |
|
并发能力 |
单线程核心+IO多线程,高并发支撑强(单机10万+QPS) |
多线程模型,并发能力较弱(单机万级QPS) |
|
数据过期 |
支持键过期,且有多种过期策略(惰性、定期、主动删除) |
仅支持键过期,且只有惰性删除,易造成内存泄漏 |
生产踩坑:早期项目用Memcached存储用户会话,因不支持持久化,服务器重启后所有用户会话丢失,引发大量投诉;后续替换为Redis,开启AOF持久化,解决该问题。
面试追问:什么场景下会选Memcached而不是Redis?(答案:简单String缓存场景,且对持久化、高可用无需求,追求极致轻量;比如静态页面缓存、临时验证码缓存,Memcached启动更快、资源占用更低)。
3. Redis 的默认端口、默认配置文件、核心配置参数(生产常用)?
答案:
-
默认端口:6379(单机)、6380(从机常用)、26379(哨兵默认端口)、7000-7005(集群节点常用);
-
默认配置文件:redis.conf(Linux)、redis.windows.conf(Windows),生产环境需修改默认配置,避免安全风险;
-
核心生产配置(必改):
-
bind 0.0.0.0:允许外部访问(默认bind 127.0.0.1,仅本地可访问);
-
protected-mode no:关闭保护模式(默认开启,禁止外部无密码访问);
-
requirepass 123456:设置密码,避免未授权访问(生产密码需复杂,定期更换);
-
daemonize yes:后台运行(默认no,前台运行,关闭终端则Redis停止);
-
dir /var/redis/data:指定持久化文件存储目录(默认当前目录,易丢失);
-
maxmemory:设置Redis最大可用内存(必须设置,避免内存溢出,导致服务器卡顿);
-
appendonly yes:开启AOF持久化(默认no,仅开启RDB,数据丢失风险高)。
-
生产踩坑:生产环境未设置maxmemory,Redis持续缓存数据,导致服务器内存被占满,触发OS的OOM killer,直接杀死Redis进程;后续设置maxmemory,并配置合理的内存淘汰策略,解决该问题。
面试追问:Redis密码设置后,如何在命令行登录?如何修改密码?(答案:登录:redis-cli -h 主机IP -p 端口 -a 密码;修改密码:config set requirepass 新密码,注意:该修改临时生效,重启Redis后失效,需修改配置文件永久生效)。
二、数据结构
1. Redis 支持哪些数据结构?每种数据结构的底层实现、核心用法及应用场景?
答案:Redis 核心支持9种数据结构,其中String、Hash、List、Set、Sorted Set(ZSet)为5种基础结构,Bitmaps、HyperLogLog、Geospatial、Stream为4种扩展结构,每种结构底层有专属编码,适配不同数据规模,具体如下:
(1)String(字符串)
底层实现:根据字符串长度和内容,采用3种编码:
-
int编码:字符串为纯数字(如"123"),长度≤20字节,底层存储为long型,节省内存;
-
embstr编码:字符串非纯数字,长度≤44字节,底层为一块连续内存(元数据+字符串内容),减少内存碎片;
-
raw编码:字符串长度>44字节,底层为SDS(简单动态字符串),可动态扩容,避免内存浪费(SDS对比C字符串,支持动态扩容、二进制安全、获取长度O(1))。
核心用法:set、get、incr、decr、append、strlen、setex(设置过期时间);
应用场景:缓存用户信息(JSON字符串)、验证码、计数器(文章阅读量、点赞数)、分布式ID生成。
生产踩坑:用String存储大JSON字符串(如10MB),执行get命令时会占用大量带宽,且单线程下会阻塞其他请求;建议拆分JSON为Hash结构,按需获取字段,减少带宽消耗。
(2)Hash(哈希)
底层实现:两种编码,根据哈希表中元素个数切换:
-
ziplist(压缩列表):元素个数≤512个,且每个元素的key和value长度≤64字节,底层为连续内存,节省空间;
-
hashtable(哈希表):元素个数>512个,或元素key/value长度>64字节,底层为数组+链表(解决哈希冲突),查询效率O(1)。
核心用法:hset、hget、hgetall、hdel、hkeys、hvals、hexists;
应用场景:缓存用户详情(如user:1001 的name、age、phone字段)、商品详情,按需获取字段,避免冗余数据。
面试追问:hgetall 和 hscan 的区别?生产中如何避免hgetall的坑?(答案:hgetall 会一次性获取Hash中所有元素,若Hash元素极多(如10万+),会阻塞单线程;hscan 是迭代式获取,每次获取指定数量元素,不会阻塞线程;生产中用hscan替代hgetall,或拆分大Hash为多个小Hash)。
(3)List(列表)
底层实现:两种编码,根据列表长度切换:
-
ziplist(压缩列表):列表长度≤512个,且每个元素长度≤64字节;
-
quicklist(快速列表):列表长度>512个,底层是ziplist的双向链表,兼顾空间和效率(Redis 3.2后替代了原来的linkedlist)。
核心用法:lpush、rpush、lpop、rpop、lrange(获取指定范围元素)、llen、lrem(删除指定元素);
应用场景:消息队列(简单FIFO队列)、最新消息列表(如朋友圈动态)、栈(lpush+lpop)。
生产踩坑:用List做消息队列时,消费者宕机后,已弹出(lpop/rpop)的消息会丢失;解决方案:用brpoplpush(弹出消息的同时,将消息存入备份列表),消费者处理完成后,删除备份列表中的消息。
(4)Set(集合)
底层实现:两种编码:
-
intset(整数集合):集合中所有元素都是整数,且元素个数≤512个,底层为有序数组,查询效率O(1);
-
hashtable(哈希表):元素包含非整数,或元素个数>512个,底层用哈希表存储,key为元素值,value为null,保证元素唯一。
核心用法:sadd、srem、smembers、sismember(判断元素是否存在)、sinter(交集)、sunion(并集)、sdiff(差集);
应用场景:用户标签(如用户1001的标签为"学生、男性")、好友去重、共同好友查询、抽奖活动(srandmember 随机取元素)。
(5)Sorted Set(ZSet,有序集合)
底层实现:两种编码:
-
ziplist(压缩列表):元素个数≤128个,且每个元素的member长度≤64字节,底层按score排序存储,节省空间;
-
skiplist(跳表)+ hashtable:元素个数>128个,或member长度>64字节;hashtable存储member到score的映射(查询score O(1)),skiplist按score排序存储(范围查询O(logN)),跳表是ZSet的核心底层结构。
核心用法:zadd、zrem、zscore(获取元素score)、zrank(获取元素排名)、zrange(按排名范围获取)、zrangebyscore(按score范围获取)、zincrby(增减score);
应用场景:排行榜(如游戏战力榜、文章阅读量榜)、带权重的消息队列、定时任务(按score存储时间戳,定期获取到期任务)。
面试追问:跳表的底层原理是什么?为什么ZSet不用红黑树而用跳表?(答案:跳表是多层有序链表,通过“跳跃”节点减少查询次数,查询、插入、删除效率均为O(logN);红黑树的范围查询效率不如跳表,且跳表实现更简单,Redis作者为了简化代码,选择跳表而非红黑树)。
(6)扩展数据结构(Bitmaps、HyperLogLog、Geospatial、Stream)
-
Bitmaps(位图):底层是String,按位存储(0/1),核心用法:setbit、getbit、bitcount(统计1的个数)、bitop(位运算);应用场景:用户签到(1天1位,365天仅需45字节)、用户在线状态(0离线,1在线)。
-
HyperLogLog(基数统计):底层是String,用于统计不重复元素的个数(基数),占用内存固定(约12KB),误差率约0.81%;核心用法:pfadd、pfcount、pfmerge;应用场景:网站UV统计(无需存储具体用户,仅统计个数)。
-
Geospatial(地理空间):底层是ZSet(score存储经纬度的编码值),核心用法:geoadd、geodist(计算距离)、georadius(根据坐标范围查询);应用场景:附近的人、外卖配送范围查询。
-
Stream(流):Redis 5.0新增,底层是链表,支持多消费者组、消息确认、消息回溯,是更完善的消息队列方案;核心用法:xadd、xread、xgroup、xack;应用场景:高可靠消息队列(如订单异步通知)。
生产踩坑:用HyperLogLog统计用户UV时,误以为其能获取具体用户列表,导致无法筛选特定用户;注意:HyperLogLog仅统计基数,不存储具体元素,无法获取单个用户信息。
2. SDS(简单动态字符串)的底层结构?和C字符串的区别?
答案:SDS是Redis自定义的字符串结构,用于替代C字符串,底层结构包含3个部分(Redis 3.2后):
-
len:当前字符串的长度(字节数),获取长度O(1);
-
alloc:当前字符串分配的内存长度(字节数),减去len即为空闲内存;
-
flags:标记SDS的编码类型(如int、embstr、raw),占1字节;
-
buf:存储字符串内容的字节数组,末尾自动添加'\0'(兼容C字符串函数)。
与C字符串的区别(核心优势):
-
获取长度:SDS O(1),C字符串O(n)(需遍历到'\0');
-
动态扩容:SDS扩容时会预留空闲内存(避免频繁扩容),C字符串需手动重新分配内存,易内存泄漏;
-
二进制安全:SDS允许存储'\0'(len标记长度,不依赖'\0'判断结束),可存储图片、视频等二进制数据;C字符串以'\0'为结束标志,无法存储含'\0'的内容;
-
避免缓冲区溢出:SDS扩容时会检查内存是否足够,C字符串若写入超出内存的内容,会导致缓冲区溢出。
面试追问:SDS的扩容策略是什么?(答案:当字符串长度小于1MB时,扩容后alloc=2*len;当长度≥1MB时,扩容后alloc=len+1MB,每次扩容预留空闲内存,减少后续写入的内存分配次数)。
3. Redis 数据结构的编码转换规则?(核心考点)
答案:Redis 每种数据结构的编码并非固定,会根据数据规模(元素个数、元素长度)自动切换,切换不可逆(仅从节省空间的编码切换到高效查询的编码,反之不切换),核心转换规则如下:
-
String:int(纯数字≤20字节)→ embstr(非纯数字≤44字节)→ raw(>44字节);
-
Hash:ziplist(元素≤512个,key/value≤64字节)→ hashtable(超出任一条件);
-
List:ziplist(元素≤512个,元素≤64字节)→ quicklist(超出任一条件);
-
Set:intset(全整数,元素≤512个)→ hashtable(超出任一条件);
-
ZSet:ziplist(元素≤128个,member≤64字节)→ skiplist+hashtable(超出任一条件)。
生产踩坑:Hash结构因元素个数超过512,从ziplist切换为hashtable,内存占用大幅增加(hashtable有额外的元数据开销);解决方案:拆分大Hash为多个小Hash(如user:1001:base、user:1001:ext),控制每个Hash的元素个数≤512。
三、持久化机制
1. Redis 有哪些持久化机制?各自的底层原理、优缺点及适用场景?
答案:Redis 核心有两种持久化机制:RDB(Redis Database)和AOF(Append Only File),两者可单独使用,也可组合使用,核心差异在于“快照存储”和“日志存储”,具体如下:
(1)RDB(快照持久化)
底层原理:在指定时间间隔内,将Redis内存中的所有数据生成一份快照(二进制文件,.rdb),存储到磁盘;恢复时,直接加载.rdb文件到内存,快速恢复数据。
触发方式(生产常用):
-
手动触发:save(同步触发,单线程执行,阻塞所有请求,生产禁用)、bgsave(异步触发,fork一个子进程生成快照,主进程继续处理请求,生产常用);
-
自动触发:通过redis.conf配置,如“save 900 1”(900秒内有1次写入操作,触发bgsave)、“save 300 10”(300秒内有10次写入)、“save 60 10000”(60秒内有10000次写入);
-
其他触发:Redis关闭(shutdown)时,会自动执行save,确保数据不丢失;主从复制时,从机同步数据前,主机会执行bgsave生成快照,发送给从机。
优点:
-
快照文件体积小,存储效率高,加载速度极快(适合大规模数据恢复);
-
异步触发(bgsave)时,不阻塞主进程,对服务影响小;
-
适合全量备份场景(如每天凌晨备份一次.rdb文件)。
缺点:
-
数据一致性差:若Redis宕机,两次快照之间的写入数据会丢失(如配置save 900 1,宕机时会丢失最多900秒的数据);
-
bgsave触发时,fork子进程会消耗内存(复制主进程的内存页表),若内存不足,会导致Redis卡顿;
-
不适合实时持久化场景(对数据丢失敏感的业务)。
(2)AOF(Append Only File)
底层原理:将Redis执行的每一条写命令(如set、hset),以日志的形式追加到.aof文件(文本格式),恢复时,重新执行.aof文件中的所有命令,还原数据。
核心配置(生产必改):
-
appendonly yes:开启AOF持久化(默认no);
-
appendfsync:AOF刷盘策略(核心,决定数据一致性和性能):
-
always:每执行一条写命令,立即刷盘(数据一致性最高,性能最差,IO开销大,生产不推荐);
-
everysec:每秒刷盘一次(默认,平衡一致性和性能,最多丢失1秒数据,生产推荐);
-
no:由操作系统决定何时刷盘(数据一致性最差,可能丢失大量数据,生产禁用)。
-
-
auto-aof-rewrite-min-size:AOF文件最小重写体积(默认64MB),当AOF文件超过该大小,才可能触发重写;
-
auto-aof-rewrite-percentage:AOF文件重写触发百分比(默认100%),当AOF文件体积比上次重写后增长100%,触发重写。
AOF重写(核心机制):
因AOF文件会不断追加写命令,体积会越来越大,重写机制会将多个冗余命令合并为一个(如set a 1、set a 2,重写为set a 2),生成一个更小的AOF文件,提升加载速度。
触发方式:手动触发(bgrewriteaof,异步执行)、自动触发(满足上述两个重写配置条件)。
优点:
-
数据一致性高:默认每秒刷盘,最多丢失1秒数据,可配置always实现零丢失(牺牲性能);
-
日志文件是文本格式,可手动修改(如误删数据,可编辑.aof文件删除误操作命令,再恢复);
-
适合对数据丢失敏感的场景(如金融、电商订单)。
缺点:
-
AOF文件体积比RDB大,存储效率低,加载速度比RDB慢(需重新执行所有命令);
-
每秒刷盘(everysec)会有一定IO开销,对Redis性能有轻微影响;
-
重写过程中,fork子进程会消耗内存,若AOF文件过大,重写时间会较长。
(3)RDB 和 AOF 对比及适用场景
|
对比维度 |
RDB |
AOF |
|---|---|---|
|
存储方式 |
二进制快照,存储完整数据 |
文本日志,存储写命令 |
|
数据一致性 |
差,丢失两次快照间的数据 |
高,最多丢失1秒(默认) |
|
文件体积 |
小,存储效率高 |
大,存储效率低 |
|
加载速度 |
快 |
慢 |
|
IO开销 |
低(仅快照时IO) |
高(每秒刷盘,写命令追加) |
|
适用场景 |
全量备份、大规模数据恢复、对数据一致性要求低的场景 |
对数据丢失敏感的场景(金融、电商)、需要手动恢复误操作数据的场景 |
生产推荐配置:开启AOF(appendfsync everysec)+ 开启RDB(每天凌晨手动执行bgsave备份),既保证数据一致性(最多丢失1秒),又有全量备份,应对极端情况(如AOF文件损坏)。
生产踩坑:仅开启AOF,未开启RDB,且AOF文件因磁盘故障损坏,导致数据无法恢复;后续增加RDB全量备份,同时定期校验AOF文件完整性(用redis-check-aof命令)。
面试追问:Redis 启动时,优先加载AOF还是RDB?为什么?(答案:优先加载AOF;因为AOF的数据一致性比RDB高,AOF文件中的数据更全,启动时会先检查AOF文件,若存在且完整,加载AOF;若AOF不存在或损坏,再加载RDB)。
2. AOF 重写的底层原理?重写过程中,新的写命令如何处理?
答案:AOF重写的核心是“合并冗余命令、生成最小指令集”,底层流程如下:
-
主进程收到重写请求(手动bgrewriteaof或自动触发),fork一个子进程(异步执行,不阻塞主进程);
-
子进程读取Redis内存中的所有数据,生成对应的写命令(如Hash的所有字段,生成一个hset命令,而非多个hset命令),写入临时AOF文件;
-
重写过程中,主进程继续处理客户端的写命令,这些新的写命令会同时追加到“原AOF文件”和“重写缓冲区”(避免重写期间的数据丢失);
-
子进程完成重写后,向主进程发送信号,主进程将重写缓冲区中的所有命令追加到临时AOF文件;
-
主进程替换原AOF文件为临时AOF文件,重写完成。
关键注意点:重写过程中,原AOF文件不会被修改,确保即使重写出错,原AOF文件仍可正常使用;重写缓冲区的作用是保存重写期间的新写命令,保证重写后的数据完整性。
面试追问:AOF重写和bgsave的区别?(答案:两者均是fork子进程异步执行,核心区别:1. 目的不同:AOF重写是压缩AOF文件,bgsave是生成RDB快照;2. 读取数据方式不同:AOF重写读取内存中所有数据,生成新的命令集;bgsave读取内存中所有数据,生成二进制快照;3. 对IO的影响不同:AOF重写写入的是文本命令,IO开销略大;bgsave写入的是二进制数据,IO开销略小)。
3. RDB 持久化中,fork 子进程的底层原理?为什么 fork 会消耗内存?
答案:Redis 的 bgsave(RDB异步快照)和 bgrewriteaof(AOF重写),均依赖 fork 子进程实现异步操作,底层基于操作系统的“写时复制(Copy On Write, COW)”机制。
fork 子进程的流程:
-
主进程执行 fork() 系统调用,创建一个子进程;
-
fork 瞬间,子进程会复制主进程的所有内存页表(仅复制页表,不复制实际内存数据),此时子进程和主进程共享同一块内存数据;
-
子进程执行快照(bgsave)或重写(bgrewriteaof),读取内存数据时,若数据未被修改,直接共享主进程的内存;
-
若主进程修改了某块内存数据(如执行set命令),操作系统会将该块内存数据复制一份,给主进程修改,子进程仍使用原来的内存数据,避免子进程读取的数据被篡改。
fork 消耗内存的原因:
fork 瞬间虽不复制实际内存数据,但会复制主进程的内存页表(每个内存页表项占用4字节),若Redis内存占用较大(如10GB),页表体积会很大(10GB / 4KB 每页 × 4字节 = 10MB),fork 时复制页表会消耗大量内存,甚至导致Redis卡顿;此外,若主进程在fork后大量修改数据,会触发大量内存复制,进一步消耗内存。
生产优化:1. 避免在Redis内存峰值时触发bgsave或AOF重写;2. 合理设置RDB快照间隔和AOF重写阈值;3. 给Redis服务器预留足够的空闲内存,避免fork时内存不足。
四、内存淘汰与过期策略
1. Redis 为什么需要内存淘汰策略?核心内存淘汰策略有哪些?(生产重点)
答案:Redis 是基于内存的数据库,内存空间有限,当Redis内存使用达到maxmemory(配置的最大内存)时,无法再写入新数据(若未配置淘汰策略,会返回错误);内存淘汰策略的作用是:当内存达到上限时,自动删除部分数据,腾出内存空间,保证Redis正常运行。
Redis 4.0+ 支持8种内存淘汰策略,分为三大类,核心常用策略4种:
(1)过期键淘汰策略(仅删除过期的键)
-
volatile-lru:淘汰过期键中,最近最少使用(LRU)的键(生产最常用,优先删除过期且不常用的数据,兼顾数据有效性和热点数据);
-
volatile-ttl:淘汰过期键中,剩余过期时间(TTL)最短的键(适合对过期时间敏感的场景);
-
volatile-random:随机淘汰过期键中的一个键(性能高,但可能淘汰热点数据,不推荐);
-
volatile-lfu:淘汰过期键中,最近最少频率使用(LFU)的键(Redis 4.0新增,比LRU更精准,适合高频访问场景)。
(2)全量键淘汰策略(删除所有键,无论是否过期)
-
allkeys-lru:淘汰所有键中,最近最少使用(LRU)的键(适合所有数据都可作为缓存,无明显过期时间的场景);
-
allkeys-random:随机淘汰所有键中的一个键(不推荐,易淘汰热点数据);
-
allkeys-lfu:淘汰所有键中,最近最少频率使用(LFU)的键(适合高频访问场景,比allkeys-lru更精准)。
(3)不淘汰策略
-
noeviction:不淘汰任何键,当内存达到maxmemory时,拒绝所有写请求(返回OOM错误),仅允许读请求(生产禁用,会导致服务不可用)。
生产推荐配置:优先使用 volatile-lru(有过期时间的缓存场景)或 allkeys-lru(无过期时间的缓存场景);若业务中高频访问的数据较多,可使用 volatile-lfu 或 allkeys-lfu,减少热点数据被淘汰的概率。
生产踩坑:配置了 noeviction 策略,且未设置 maxmemory,Redis 内存被占满,触发OS OOM killer,杀死Redis进程;后续修改为 volatile-lru 策略,并设置 maxmemory(如服务器内存的80%),解决该问题。
面试追问:LRU 和 LFU 的区别?Redis 是如何实现 LRU 的?(答案:1. 区别:LRU 基于“最近使用时间”,淘汰最久未使用的键;LFU 基于“使用频率”,淘汰使用次数最少的键,更能精准保留热点数据(如一个键很久未用,但之前使用频率极高,LRU会淘汰,LFU不会);2. Redis 实现 LRU:并未使用传统的LRU链表(内存开销大),而是采用“近似LRU”——给每个键添加一个“最后访问时间戳”,淘汰时,随机采样N个键(默认5个),淘汰其中时间戳最久的键,采样数量越多,越接近真实LRU,可通过配置 maxmemory-samples 调整采样数量)。
2. Redis 的过期键删除策略?为什么不用单一的删除策略?
答案:Redis 采用“三种删除策略结合”的方式,解决过期键删除的“一致性”和“性能”平衡问题,单一删除策略无法兼顾两者,具体三种策略如下:
(1)惰性删除(passive deletion)
原理:当客户端访问一个过期键时,Redis 才检查该键是否过期,若过期则删除,返回nil;若未过期则正常返回数据。
优点:不占用额外CPU资源,删除操作仅在访问时触发,对性能影响小;
缺点:过期键若长期不被访问,会一直占用内存,导致内存泄漏(如一个过期的缓存键,一直无人访问,会持续占用内存)。
(2)定期删除(active deletion)
原理:Redis 每隔一段时间(默认100毫秒),随机采样一部分过期键,删除其中已过期的键;采样数量和间隔可通过配置调整(如 hz 配置,默认10,代表每秒执行10次定期删除)。
优点:主动删除部分过期键,减少内存泄漏的概率;
缺点:采样数量过少,无法及时删除所有过期键;采样数量过多,会占用CPU资源,影响Redis性能。
(3)主动删除(内存淘汰时触发)
原理:当Redis内存达到maxmemory,且配置了内存淘汰策略时,会在写入新数据前,主动删除部分键(根据淘汰策略),腾出内存空间。
优点:保证Redis内存不溢出,确保服务正常运行;
缺点:若淘汰策略配置不当,可能淘汰热点数据,影响业务。
为什么不用单一策略?:
-
仅用惰性删除:内存泄漏严重,长期运行会导致内存被占满;
-
仅用定期删除:若采样频率过高,占用CPU过多;频率过低,无法及时清理过期键;
-
仅用主动删除:仅在内存满时才删除,过期键长期占用内存,且可能淘汰非过期的热点数据。
因此,Redis 结合三种策略,既保证了性能(惰性删除+定期删除,减少CPU开销),又避免了内存泄漏(定期删除+主动删除,及时清理过期键)。
面试追问:Redis 定期删除的采样机制是什么?(答案:Redis 每次定期删除时,会随机采样 maxmemory-samples 个过期键(默认5个),删除其中已过期的键;若采样的键中,过期键比例超过25%,则继续采样,直到过期键比例≤25%,避免一次性删除过多键,占用CPU资源)。
3. 生产中如何合理配置 maxmemory 和内存淘汰策略?(深岗重点)
答案:核心原则:根据服务器内存大小、业务场景(缓存/存储)、数据一致性要求,合理配置,避免内存溢出和热点数据被淘汰,具体配置方案如下:
-
maxmemory 配置:
-
服务器仅运行Redis:设置为服务器物理内存的80%(如16GB内存,设置为12.8GB),预留20%内存给操作系统和其他进程,避免OS OOM;
-
服务器运行多个进程(如Redis+MySQL):根据其他进程的内存占用,合理分配,确保Redis内存不超过服务器可用内存的50%,避免进程间内存竞争;
-
注意:若开启RDB/AOF持久化,需额外预留10%-20%内存,用于fork子进程时的内存复制。
-
-
内存淘汰策略配置(分场景):
-
场景1:缓存场景(如用户信息缓存、商品缓存),所有键都设置过期时间,优先用 volatile-lru(兼顾过期和热点数据);若高频访问数据多,用 volatile-lfu;
-
场景2:存储场景(如会话存储),部分键无过期时间,优先用 allkeys-lru(淘汰最不常用的键);
-
场景3:对过期时间敏感(如验证码、临时令牌),用 volatile-ttl(优先淘汰快过期的键);
-
禁止使用:noeviction、allkeys-random、volatile-random(要么导致服务不可用,要么易淘汰热点数据)。
-
-
补充优化:
-
给所有缓存键设置合理的过期时间(避免无过期时间的键长期占用内存);
-
定期清理过期键(可通过定时任务执行 scan 命令,删除过期键);
-
监控Redis内存使用情况(如用info memory命令),当内存使用率超过90%时,及时排查。
-
生产踩坑:将maxmemory设置为服务器物理内存的90%,且开启RDB持久化,fork子进程时,因内存不足,导致Redis卡顿,客户端请求超时;后续调整为70%,预留足够内存给fork子进程,解决卡顿问题。
五、缓存三大问题
1. 缓存穿透(Cache Penetration):原因、解决方案及生产踩坑?
答案:缓存穿透是指:客户端请求的数据,既不在Redis缓存中,也不在数据库中,导致每次请求都穿透缓存,直接访问数据库,大量请求会压垮数据库,甚至导致数据库宕机。
(1)核心原因
-
恶意攻击:攻击者故意请求不存在的数据(如用户ID为-1、随机无效ID),绕过缓存,攻击数据库;
-
业务误操作:请求了不存在的业务数据(如查询已删除的商品、无效的用户ID);
-
缓存未命中:新数据刚生成,还未写入缓存,此时请求该数据(偶发,影响较小)。
(2)解决方案(生产常用,按优先级排序)
-
缓存空值(最常用): 当数据库查询不到数据时,将空值(或特定标识,如"null")写入Redis缓存,并设置较短的过期时间(如5分钟),后续请求该数据时,直接从缓存返回空值,避免穿透到数据库。注意:空值的过期时间不能太长,避免真实数据生成后,缓存无法及时更新(如用户ID=-1的空值,若过期时间太长,当后续有该ID的用户时,缓存会返回空值)。
-
布隆过滤器(Bloom Filter,适合大数据量场景): 在缓存之前增加布隆过滤器,将数据库中所有存在的key(如用户ID、商品ID)提前存入布隆过滤器;当客户端请求时,先通过布隆过滤器判断该key是否存在:若不存在,直接返回空值,不访问缓存和数据库;若存在,再访问缓存和数据库。优点:占用内存小(如1亿个key,仅需约12MB内存),查询效率高(O(1));缺点:有误判率(无法100%判断key是否存在),且无法删除key(布隆过滤器的特性,删除会影响其他key的判断)。
-
接口层限流/校验(防恶意攻击): 在接口层增加参数校验(如用户ID必须为正整数、商品ID符合规范),拒绝无效参数;同时对接口进行限流(如每秒最多1000次请求),防止攻击者大量请求无效数据。
-
缓存预热(减少偶发穿透): 系统启动时,将热点数据提前写入缓存,避免新数据请求时,缓存未命中导致穿透。
(3)生产踩坑
采用“缓存空值”方案时,未设置过期时间,导致某无效ID的空值长期存在于缓存中,当后续该ID的真实数据生成后,客户端仍获取到空值,引发业务异常;后续给空值设置5分钟过期时间,并在真实数据生成时,主动更新缓存(删除空值或覆盖空值),解决该问题。
面试追问:布隆过滤器的底层原理是什么?误判率如何控制?(答案:1. 原理:布隆过滤器是一个bit数组,结合多个哈希函数,将一个key通过多个哈希函数映射到bit数组的多个位置,设置为1;判断key是否存在时,若所有映射位置都是1,则认为存在(可能误判),若有一个位置是0,则一定不存在;2. 误判率控制:误判率与bit数组长度、哈希函数个数相关,bit数组越长、哈希函数个数越多,误判率越低,但内存占用和计算开销越大;生产中可根据数据量,选择合适的bit数组长度和哈希函数个数,一般误判率控制在0.1%以内)。
2. 缓存击穿(Cache Breakdown):原因、解决方案及生产踩坑?
答案:缓存击穿是指:某个热点数据(如热门商品、热门用户)的缓存过期,此时大量客户端同时请求该数据,导致缓存未命中,所有请求都穿透到数据库,压垮数据库(与缓存穿透的区别:缓存击穿是“数据存在,但缓存过期”,缓存穿透是“数据不存在”)。
(1)核心原因
-
热点数据缓存过期:热门数据的缓存设置了过期时间,到期后,大量请求同时访问该数据;
-
缓存雪崩的前兆:单个热点数据过期,若多个热点数据同时过期,会引发缓存雪崩。
(2)解决方案(生产常用,按优先级排序)
-
热点数据永不过期(最简单,适合不变的热点数据): 对于不会变化的热点数据(如热门商品的基本信息、固定配置),不设置过期时间,避免缓存过期导致击穿;同时,当数据更新时,主动更新缓存(如商品信息修改后,调用set命令覆盖缓存)。
-
互斥锁(分布式锁,适合动态热点数据): 当缓存过期时,只有一个客户端能获取到分布式锁(如Redis的setnx命令),该客户端负责从数据库查询数据、更新缓存,其他客户端等待锁释放后,直接从缓存获取数据,避免大量请求穿透到数据库。 注意:锁的过期时间需合理设置(如3-5秒),避免锁未释放导致死锁;同时,查询数据库失败时,需释放锁,防止死锁。
-
缓存过期时间错开(避免批量过期): 对于多个热点数据,设置不同的过期时间(如在基础过期时间上增加0-300秒的随机值),避免多个热点数据同时过期,引发缓存击穿甚至缓存雪崩。
-
热点数据预热+定时更新(主动续命): 系统启动时,将热点数据提前写入缓存,并设置较短的过期时间;同时,通过定时任务(如每隔1分钟)查询热点数据,主动更新缓存的过期时间,避免缓存过期。
(3)生产踩坑
采用互斥锁方案时,未设置锁的过期时间,某客户端获取锁后因异常崩溃,导致锁长期未释放,其他客户端无法获取锁,全部穿透到数据库,压垮数据库;后续给锁设置3秒过期时间,同时在客户端查询数据库完成后,主动释放锁,解决死锁问题。
面试追问:Redis实现分布式锁的核心命令是什么?如何避免锁失效?(答案:1. 核心命令:setnx key value(不存在则设置,获取锁)+ expire key 过期时间(设置锁过期,避免死锁),Redis 2.6.12后可合并为set key value nx ex 过期时间(原子操作);2. 避免锁失效:① 合理设置锁过期时间,结合业务耗时调整;② 若业务耗时超过锁过期时间,可实现锁续期(如用Lua脚本定时延长锁过期时间);③ 客户端崩溃后,锁到期自动释放,避免死锁)。
3. 缓存雪崩(Cache Avalanche):原因、解决方案及生产踩坑?
答案:缓存雪崩是指:大量热点数据的缓存同时过期,或Redis服务宕机,导致所有客户端请求都穿透到数据库,数据库瞬间承受巨大压力,直接宕机,引发整个系统雪崩(比缓存击穿更严重,是批量的缓存击穿)。
(1)核心原因
-
批量缓存过期:多个热点数据设置了相同的过期时间(如凌晨0点统一过期),到期后大量请求同时穿透到数据库;
-
Redis服务宕机:Redis集群故障、服务器宕机,导致缓存全部不可用,所有请求直接访问数据库;
-
缓存穿透未解决:大量无效请求持续穿透,叠加缓存过期,进一步压垮数据库。
(2)解决方案(生产常用,按优先级排序)
-
缓存过期时间错开(核心,解决批量过期): 对所有热点数据,在基础过期时间上增加随机值(如0-300秒),确保不同热点数据的过期时间分散,避免同时过期;例如:热门商品缓存基础过期时间为1小时,随机增加0-5分钟,使过期时间分布在1小时到1小时5分钟之间。
-
Redis高可用部署(解决Redis宕机): 搭建Redis主从复制+哨兵模式,或Redis集群(如Redis Cluster),确保单个Redis节点宕机后,哨兵能自动切换主节点,集群能自动容错,保证缓存服务持续可用;同时,配置Redis持久化(RDB+AOF),避免Redis宕机后数据丢失,快速恢复服务。
-
热点数据永不过期(兜底,核心热点): 对核心热点数据(如首页热门商品、核心配置),不设置过期时间,同时通过定时任务主动更新缓存,避免缓存过期;非核心热点数据,采用过期时间错开方案。
-
多级缓存(降级兜底): 搭建多级缓存架构(本地缓存+Redis缓存+数据库),当Redis缓存宕机时,客户端先访问本地缓存(如Java的Caffeine缓存),避免直接穿透到数据库;本地缓存仅存储核心热点数据,且设置较短过期时间,避免数据不一致。
-
数据库限流/降级(终极兜底): 当数据库压力过大时,通过接口限流(如每秒最多500次请求)、服务降级(关闭非核心接口,优先保障核心接口),避免数据库宕机;同时,结合熔断机制(如Sentinel),当数据库响应超时,直接返回降级提示,不持续请求数据库。
(3)生产踩坑
某电商平台促销活动时,所有商品缓存统一设置为2小时过期,活动结束后缓存批量过期,大量请求穿透到数据库,导致数据库宕机,服务不可用;后续对商品缓存设置基础过期时间+随机值(2小时±10分钟),同时搭建Redis集群,避免批量过期和Redis宕机问题,后续活动未再出现雪崩。
面试追问:Redis集群和哨兵模式的区别?缓存雪崩和缓存击穿的核心差异是什么?(答案:1. 区别:哨兵模式主要解决Redis主从架构的高可用,实现主节点故障自动切换,适合中小规模场景;Redis集群不仅解决高可用,还实现水平扩展(分片存储),适合大规模数据场景,可分担单节点压力;2. 核心差异:缓存击穿是“单个热点数据过期”,导致大量请求穿透;缓存雪崩是“大量热点数据同时过期”或“Redis宕机”,导致所有请求穿透,影响范围更大)。
4. 缓存一致性(Cache Consistency):原因、解决方案及生产踩坑?(深岗高频)
答案:缓存一致性是指:Redis缓存中的数据与数据库中的数据保持一致,避免出现“缓存有数据但与数据库不一致”“缓存无数据但数据库有数据”的情况,核心解决“数据更新时,缓存和数据库如何同步”的问题。
(1)核心原因
-
数据更新顺序错误:先更新缓存、再更新数据库,若更新数据库失败,导致缓存数据正确、数据库数据错误;或先更新数据库、再删除缓存,若删除缓存失败,导致缓存数据旧、数据库数据新;
-
并发更新冲突:多个客户端同时更新同一数据,导致缓存和数据库数据不一致(如客户端A更新数据库,客户端B同时读取缓存,获取到旧数据);
-
缓存过期/淘汰:缓存过期或被淘汰后,未及时从数据库同步最新数据,导致缓存数据缺失;
-
Redis宕机:Redis宕机后,缓存数据丢失,恢复后未及时从数据库同步最新数据,导致缓存和数据库不一致。
(2)解决方案(生产常用,按业务优先级排序)
方案1:Cache-Aside Pattern(缓存旁路模式,最常用,适合大多数业务)
核心逻辑:读操作先查缓存,缓存未命中再查数据库,同时更新缓存;写操作先更数据库,再删除缓存(而非更新缓存)。
-
读操作流程:客户端请求 → 查Redis缓存 → 缓存命中,返回数据;缓存未命中,查数据库 → 将数据库数据写入缓存 → 返回数据;
-
写操作流程:客户端更新数据 → 先更新数据库 → 再删除Redis缓存(避免更新缓存失败导致数据不一致);
-
关键优化:删除缓存失败时,通过重试机制(如定时任务重试、消息队列重试)确保缓存删除成功;若业务允许,可设置缓存较短过期时间,兜底解决删除失败问题。
方案2:Write-Through Pattern(写透模式,适合对一致性要求高、写操作少的场景)
核心逻辑:写操作先更新缓存,再更新数据库,确保缓存和数据库同时更新;读操作正常查缓存,未命中查数据库并更新缓存。
优点:数据一致性高,不存在缓存和数据库不一致的情况;缺点:写操作性能低(需同时更新缓存和数据库),适合写操作少、读操作多的场景(如配置缓存)。
方案3:Write-Back Pattern(写回模式,适合对性能要求高、一致性要求低的场景)
核心逻辑:写操作只更新缓存,不立即更新数据库,缓存定期批量同步到数据库;读操作正常查缓存,未命中查数据库并更新缓存。
优点:写操作性能极高(仅更新缓存);缺点:数据一致性差,若Redis宕机,未同步到数据库的数据会丢失,适合非核心数据(如浏览记录)。
方案4:分布式事务(适合对一致性要求极高的场景,如金融、电商订单)
核心逻辑:通过分布式事务(如Redis的Lua脚本、Seata分布式事务),确保“更新数据库”和“删除/更新缓存”原子执行,要么同时成功,要么同时失败。
注意:分布式事务会降低系统性能,非必要不使用,优先用缓存旁路模式+重试机制。
(3)生产踩坑
采用“先更新数据库、再删除缓存”的缓存旁路模式时,未处理缓存删除失败的情况,某一次更新操作中,数据库更新成功,但缓存删除失败,导致后续请求获取到旧的缓存数据,引发业务异常;后续增加定时任务,每隔1分钟检查缓存和数据库数据一致性,对不一致的数据重新同步,同时实现缓存删除重试机制,解决该问题。
面试追问:为什么写操作要“删除缓存”而不是“更新缓存”?(答案:1. 避免并发更新冲突:若多个客户端同时更新同一数据,先更新数据库再更新缓存,可能出现“客户端A更新数据库→客户端B更新数据库→客户端A更新缓存”,导致缓存数据为A的旧数据,数据库为B的新数据;2. 减少无效更新:若数据更新频繁,但读操作少,更新缓存会造成资源浪费,删除缓存后,只有下次读操作才会同步数据,更高效;3. 避免更新失败:更新缓存比删除缓存更复杂(如Hash结构需更新指定字段),失败概率更高,删除缓存更简单、可靠)。
六、基础剩余高频考点(补充遗漏,贴合深岗面试)
1. 本地缓存和分布式缓存的区别及选择(核心遗漏,前文补充延伸)
答案:核心是“是否共享数据、是否依赖外部服务”,两者常结合使用(多级缓存),具体如下:
(1)核心定义
-
本地缓存:缓存数据存储在单个应用进程内(如Java的Caffeine、Guava Cache),仅当前应用实例可访问,不依赖外部服务,嵌入在应用中运行。
-
分布式缓存:缓存数据存储在独立的分布式服务中(如Redis、Memcached),多个应用实例、多台服务器可共享访问,依赖网络通信。
(2)核心区别(面试必背,分维度对比)
|
对比维度 |
本地缓存 |
分布式缓存 |
|
存储位置 |
应用进程内存中,嵌入应用 |
独立缓存服务(如Redis集群) |
|
访问速度 |
极快(无网络开销,直接访问内存) |
较快(需网络通信,有轻微延迟) |
|
数据共享 |
不共享,仅当前应用实例可用 |
共享,多应用、多实例可共用 |
|
容量限制 |
受应用内存限制,容量较小 |
可通过集群水平扩展,容量大 |
|
高可用性 |
差,应用重启后缓存全部丢失 |
高,支持主从、集群,故障可容错 |
|
运维成本 |
低,无需额外部署服务 |
高,需部署、维护缓存集群 |
(3)选择原则(生产落地,面试必问)
-
选本地缓存:数据仅单应用使用、对速度要求极高、数据量小(如应用内配置、本地限流规则),示例:Java用Caffeine缓存系统常量。
-
选分布式缓存:多应用共享数据、数据量大、要求高可用(如用户会话、商品缓存),示例:Redis集群缓存用户购物车、商品详情。
-
最优方案:多级缓存(本地缓存+分布式缓存),兼顾速度和共享,流程:请求→本地缓存→分布式缓存→数据库。
面试追问:本地缓存(Caffeine)的核心优势是什么?如何避免本地缓存数据不一致?(答案:1. 核心优势:无网络开销、访问速度极快,支持自动过期、内存淘汰,适配高频读场景;2. 避免不一致:设置较短过期时间(如1分钟),结合定时任务同步分布式缓存数据;应用重启后,重新加载数据到本地缓存)。
2. Redis 键的过期时间相关(基础高频,易遗漏)
答案:Redis支持给键设置过期时间(TTL),核心用于缓存场景,避免键长期占用内存,相关高频考点如下:
(1)核心命令(生产常用)
-
设置过期时间:expire key 秒数(如expire user:1001 3600,设置1小时过期);pexpire key 毫秒数(高精度过期);
-
设置键时直接指定过期时间:set key value ex 秒数(原子操作,如set user:1001 "zhangsan" ex 3600);
-
查看过期时间:ttl key(返回剩余秒数,-1表示永不过期,-2表示已过期);pttl key(返回剩余毫秒数);
-
移除过期时间:persist key(将过期键转为永不过期)。
(2)核心注意点(生产踩坑)
1. 过期时间精度:Redis的过期时间精度为毫秒级(Redis 2.6+),但定期删除策略是随机采样,无法做到毫秒级精准删除,存在轻微延迟(可忽略);
2. 过期键的影响:键过期后,不会立即被删除(结合三种删除策略),但访问时会返回nil,且不会占用内存淘汰的优先级(淘汰时优先淘汰过期键);
3. 批量设置过期时间:无直接批量设置命令,需通过Lua脚本(如遍历键,批量执行expire命令),避免循环执行expire导致Redis卡顿。
面试追问:Redis的ttl命令返回-1、-2分别代表什么?如何批量给一批键设置过期时间?(答案:1. 返回值含义:-1表示键存在且永不过期,-2表示键不存在或已过期;2. 批量设置:用Lua脚本,示例:for i,key in ipairs(KEYS) do expire(key, ARGV[1]) end,执行时传入批量键和过期秒数,原子性执行,避免循环阻塞)。
3. Redis 常用命令(基础必背,易遗漏高频)
除了数据结构相关命令,以下基础命令是面试高频提问,生产中高频使用,易遗漏:
(1)通用命令(所有数据结构适用)
-
keys *:查询所有键(生产禁用!单线程下阻塞所有请求,用scan替代);
-
scan cursor [match pattern] [count 数量]:迭代查询键(如scan 0 match user:* count 100,每次查询100个以user:开头的键);
-
exists key:判断键是否存在(返回1存在,0不存在);
-
del key1 key2:删除一个或多个键(原子操作);
-
type key:查看键的数据结构类型(如string、hash、list);
-
dbsize:查看当前Redis数据库的键总数;
-
flushdb:清空当前数据库的所有键(生产慎用!);
-
flushall:清空所有数据库的所有键(生产禁用!会导致全量数据丢失)。
(2)生产避坑重点
1. 禁止在生产环境执行keys *、flushdb、flushall命令:keys *会阻塞单线程,flush类命令会导致全量数据丢失,若需执行,需先备份数据,且在低峰期操作;
2. scan命令的cursor参数:cursor=0表示开始迭代,返回的cursor不为0表示还有未查询的键,需继续迭代,直到cursor=0结束;
3. del命令的原子性:del命令可批量删除多个键,且是原子操作,要么全部删除成功,要么全部失败(无部分删除)。
面试追问:keys命令和scan命令的区别?为什么生产不用keys?(答案:1. 区别:① 执行方式:keys是一次性查询所有键,scan是迭代式查询;② 阻塞情况:keys会阻塞单线程,导致所有请求超时,scan不会阻塞,每次查询少量键;③ 内存开销:keys一次性加载所有键到内存,内存开销大,scan分批加载,开销小;2. 生产不用keys:因为Redis是单线程,keys命令会遍历所有键,若键数量极多(如百万级),会阻塞线程数秒甚至分钟,导致服务不可用,用scan替代可避免阻塞)。
4. Redis 数据库相关(基础高频,易遗漏)
答案:Redis默认有16个数据库(编号0-15),核心用于数据隔离,相关高频考点如下:
(1)核心命令
-
select 数据库编号:切换数据库(如select 1,切换到1号数据库);
-
move key 数据库编号:将当前数据库的键移动到指定数据库(如move user:1001 1,将user:1001移动到1号库);
-
info databases:查看所有数据库的键数量;
-
config set databases 数量:修改默认数据库数量(临时生效,重启Redis后失效,需修改配置文件永久生效)。
(2)生产使用原则
1. 数据库隔离场景:不同业务、不同数据类型用不同数据库(如0号库存商品缓存,1号库存用户会话),避免键名冲突;
2. 不建议使用过多数据库:数据库过多(如超过16个)会增加Redis运维成本,且无法实现跨数据库的命令操作(如跨库查询);
3. 注意数据隔离:不同数据库的键相互独立,flushdb仅清空当前数据库,不会影响其他数据库。
面试追问:Redis不同数据库之间能共享数据吗?为什么不建议用多数据库实现数据隔离?(答案:1. 不能共享:不同数据库的键相互独立,无法跨数据库访问(如1号库无法直接获取0号库的键);2. 不建议多库隔离:① 无法实现精细化权限控制(Redis权限控制是针对整个Redis服务,而非单个数据库);② 跨数据库操作不便,需频繁切换数据库;③ 数据库过多,不利于监控和维护,建议用键名前缀(如user:、goods:)实现数据隔离,而非多数据库)。
5. Redis 与数据库的同步方式(基础高频,衔接缓存一致性)
答案:核心解决“缓存数据与数据库数据同步”,生产中常用3种方式,面试常问区别:
(1)三种同步方式(按优先级排序)
-
主动更新(最常用,缓存旁路模式):写操作先更新数据库,再删除缓存,读操作缓存未命中时,从数据库查询并更新缓存(前文缓存一致性已提,补充细节); 注意:删除缓存失败时,需增加重试机制(如消息队列重试、定时任务重试),避免数据不一致。
-
定时同步(兜底方案):通过定时任务(如每隔1分钟),批量查询数据库数据,对比缓存数据,对不一致的数据重新同步到缓存; 适用场景:缓存删除失败、Redis宕机恢复后,快速同步数据,兜底保证一致性。
-
数据库binlog同步(高级方案):监听数据库binlog日志(如MySQL的binlog),当数据库数据更新时,通过binlog解析,自动同步到Redis缓存; 优点:实时性高,无需业务代码干预,减少开发成本;缺点:需部署binlog解析服务(如Canal),运维成本高,适合大数据量、高实时性场景(如电商商品实时更新)。
(2)生产踩坑
采用定时同步方案时,定时任务间隔设置过长(如10分钟),导致缓存和数据库数据不一致的时间窗口过大,引发业务异常;后续将间隔调整为1分钟,同时增加缓存删除重试机制,减少不一致时间窗口。
面试追问:binlog同步方案和主动更新方案的区别?如何选择?(答案:1. 区别:① 实时性:binlog同步实时性高(数据更新立即同步),主动更新实时性依赖读操作(删除缓存后,需下次读才同步);② 开发成本:binlog同步需部署解析服务,开发、运维成本高,主动更新仅需业务代码处理,成本低;③ 适用场景:binlog适合大数据量、高实时性场景,主动更新适合中小规模、实时性要求不极致的场景;2. 选择:优先用主动更新(成本低、易维护),大数据量、高实时性场景,结合binlog同步兜底)。
七、主从复制与高可用
1. 什么是Redis主从复制?核心作用是什么?(基础高频)
答案:Redis主从复制是指:将一台Redis服务器(主节点,master)的数据,同步到多台Redis服务器(从节点,slave)的机制,主节点负责读写操作,从节点仅负责读操作(默认只读),形成“一主多从”的架构,核心作用是解决Redis的高可用和读写分离,具体如下:
-
读写分离:主节点承担写操作,从节点承担读操作,分担主节点压力(如电商场景,读请求远多于写请求,从节点可分流80%以上读请求);
-
数据备份:从节点实时同步主节点数据,相当于热备份,主节点宕机后,从节点可作为备用节点,避免数据丢失;
-
高可用基础:主从复制是哨兵模式、集群模式的基础,没有主从复制,无法实现故障自动切换和高可用。
核心注意点:主从复制默认是“异步同步”,主节点执行完写命令后,立即返回客户端结果,再异步将命令同步到从节点,可能存在极短时间的数据不一致(毫秒级,生产可接受);Redis 2.8+ 支持“部分同步”,解决了旧版本“全量同步”的性能问题。
面试追问:主从复制为什么默认异步同步?同步同步有什么问题?(答案:1. 异步同步原因:优先保证主节点的写性能,若采用同步同步,主节点需等待所有从节点同步完成才能返回结果,会严重降低主节点吞吐量,无法支撑高并发;2. 同步同步问题:主节点阻塞,写性能暴跌,且某一个从节点同步缓慢会拖累所有客户端请求,生产环境绝对不适用)。
2. 主从复制的底层原理(核心考点,深岗必问)
答案:主从复制的核心流程分为“全量同步”和“部分同步”,两种同步方式结合,兼顾数据完整性和性能,具体流程如下:
(1)全量同步(首次同步/从节点断线重连后差距过大)
当从节点首次连接主节点,或从节点断线后重连,主从数据差距过大(超过复制积压缓冲区大小)时,触发全量同步,流程如下:
-
从节点发送psync命令(Redis 2.8+),向主节点请求同步,携带自己的runid(从节点唯一标识)和offset(已同步的偏移量,首次为-1);
-
主节点收到请求后,判断从节点是首次连接(runid不存在或offset为-1),则触发全量同步,执行bgsave命令,生成RDB快照文件;
-
主节点在生成RDB的过程中,将所有写命令记录到“复制积压缓冲区”(环形缓冲区,默认1MB),避免同步期间的写命令丢失;
-
主节点生成RDB完成后,将RDB文件发送给从节点,从节点接收后,清空本地内存数据,加载RDB文件,同步主节点的基础数据;
-
主节点将复制积压缓冲区中的所有写命令,发送给从节点,从节点执行这些命令,最终实现主从数据一致;
-
同步完成后,主节点后续的写命令,会实时异步发送给从节点,从节点执行命令,保持数据同步。
(2)部分同步(从节点断线重连后差距较小)
当从节点断线时间较短,重新连接主节点时,主从数据差距较小(未超过复制积压缓冲区大小),触发部分同步,流程如下:
-
从节点重连后,发送psync命令,携带自己的runid和断线前的offset;
-
主节点判断runid与自己保存的从节点runid一致,且offset在复制积压缓冲区的范围内,说明可进行部分同步;
-
主节点从offset位置开始,将复制积压缓冲区中未同步的写命令,一次性发送给从节点;
-
从节点执行这些写命令,快速追上主节点的数据,完成同步,无需重新加载全量RDB,性能更高。
(3)核心关键组件
-
runid:每个Redis节点启动时生成的唯一标识,主节点会记录所有从节点的runid,用于判断从节点是否是之前连接过的节点;
-
offset:主从节点各自维护的偏移量,主节点每执行一条写命令,offset+1;从节点同步一条命令,offset也+1,通过offset判断主从数据是否一致;
-
复制积压缓冲区:主节点维护的环形缓冲区,用于存储近期的写命令,默认大小1MB,可通过配置repl-backlog-size调整,越大,支持从节点断线重连的时间越长。
生产踩坑:复制积压缓冲区设置过小(默认1MB),某从节点断线5分钟后重连,写命令超过缓冲区大小,触发全量同步,导致主节点fork子进程生成RDB,占用大量内存和IO,拖累主节点性能;后续将repl-backlog-size调整为10MB,支持从节点断线10分钟内重连无需全量同步。
3. 主从复制的配置(生产落地,面试必问)
答案:主从复制配置简单,无需修改主节点配置(默认支持主从复制),仅需修改从节点配置,核心有两种配置方式,生产常用第二种:
(1)配置文件配置(永久生效,生产推荐)
修改从节点的redis.conf配置文件,添加以下配置,重启Redis生效:
# 从节点配置(核心3行) slaveof 主节点IP 主节点端口 # 指定主节点的IP和端口(如slaveof 192.168.1.100 6379) slave-read-only yes # 从节点只读(默认yes,禁止从节点写操作,避免数据混乱) masterauth 主节点密码 # 若主节点设置了密码,必须配置该参数,否则无法同步(如masterauth 123456) # 可选优化配置 repl-backlog-size 10mb # 复制积压缓冲区大小,调整为10MB,支持更长时间断线重连 repl-ping-slave-period 10 # 从节点每隔10秒向主节点发送ping请求,检测主从连接状态 slave-priority 100 # 从节点优先级(值越小,优先级越高),哨兵模式下,优先级高的从节点优先被选为新主节点
(2)命令行配置(临时生效,适合测试)
登录从节点的redis-cli,执行以下命令,无需重启Redis,但重启后配置失效:
-
slaveof 主节点IP 主节点端口 # 绑定主节点(如slaveof 192.168.1.100 6379)
-
config set masterauth 主节点密码 # 若主节点有密码,设置密码
-
config set slave-read-only yes # 设置从节点只读
-
slaveof no one # 取消主从复制,将从节点转为独立节点
(3)生产配置注意事项
-
主节点必须设置密码(requirepass),从节点配置masterauth,避免未授权访问和数据泄露;
-
从节点建议关闭持久化(RDB+AOF),数据从主节点同步,减少从节点IO开销;若需开启,可开启RDB,关闭AOF,避免AOF刷盘影响性能;
-
主从节点网络延迟尽量控制在10ms以内,延迟过高会导致主从数据不一致,影响业务;
-
从节点数量不宜过多(建议不超过5个),过多从节点会增加主节点的同步压力,若需更多从节点,可采用“主-从-从”的级联架构(主节点同步给1个从节点,该从节点再同步给其他从节点)。
面试追问:主从复制中,从节点为什么默认只读?如何让从节点可写?(答案:1. 只读原因:避免多个从节点写数据,导致主从数据不一致,破坏主从复制架构;2. 让从节点可写:修改配置slave-read-only no,或命令行执行config set slave-read-only no;但生产绝对不建议,会导致主从数据混乱,引发业务异常)。
4. 主从复制的常见问题及解决方案(生产踩坑,深岗高频)
答案:主从复制落地时,常遇到数据不一致、从节点同步失败、主节点压力过大等问题,核心解决方案如下:
(1)问题1:主从数据不一致
核心原因:主从异步同步导致的短暂延迟、网络中断、从节点执行了写操作、主节点复制积压缓冲区过小。
解决方案:
-
合理设置复制积压缓冲区大小(如10-50MB),减少断线重连时的全量同步概率;
-
禁止从节点写操作(保持slave-read-only yes),避免从节点数据篡改;
-
定期检查主从offset是否一致(用info replication命令),若不一致,手动触发全量同步(从节点执行slaveof no one,再执行slaveof 主节点IP 端口);
-
优化网络,减少主从节点之间的延迟,缩短数据不一致的时间窗口。
(2)问题2:从节点同步失败
核心原因:主节点密码错误、主从网络不通、主节点拒绝从节点连接、从节点版本低于主节点(Redis主从版本需一致或从节点版本高于主节点)。
解决方案:
-
检查主从网络:ping主节点IP,确保端口开放(6379),关闭防火墙或开放对应端口;
-
核对主从密码:确保从节点masterauth与主节点requirepass一致;
-
检查主节点配置:主节点默认允许从节点连接,若配置了bind 127.0.0.1,需改为bind 0.0.0.0,允许外部从节点连接;
-
统一主从版本:确保所有节点Redis版本一致(如均为6.2.x),避免版本兼容问题。
(3)问题3:主节点压力过大(写请求多、同步压力大)
核心原因:主节点承担所有写操作,同时需向多个从节点同步数据,IO和CPU开销过大。
解决方案:
-
读写分离:所有读请求路由到从节点,写请求路由到主节点,分流主节点读压力;
-
级联主从:采用“主-从-从”架构,主节点仅同步给1个从节点(一级从),其他从节点(二级从)从一级从同步,减少主节点同步压力;
-
优化主节点性能:关闭主节点不必要的持久化(如关闭AOF,仅开启RDB),减少IO开销;合理设置maxmemory和淘汰策略,避免主节点内存溢出;
-
限制从节点数量:主节点直接同步的从节点不超过5个,多余从节点通过级联方式同步。
(4)问题4:从节点重启后,数据丢失
核心原因:从节点未开启持久化,重启后本地数据清空,需重新从主节点同步,若主节点宕机,从节点无数据。
解决方案:
-
从节点开启RDB持久化(关闭AOF),定期生成RDB快照,重启后加载本地RDB,减少重新同步的时间和主节点压力;
-
结合哨兵模式,主节点宕机后,哨兵快速将从节点切换为主节点,避免数据丢失。
5. 哨兵模式(Sentinel):原理、作用及配置(高可用核心,深岗必问)
答案:哨兵模式是Redis实现高可用的核心方案,基于主从复制,通过多个哨兵节点(Sentinel)监控主从节点的状态,当主节点宕机时,自动将一个从节点切换为新主节点,实现故障自动切换,无需人工干预,核心作用是“监控、自动切换、通知”。
(1)哨兵模式的核心作用
-
监控(Monitoring):哨兵节点实时监控主节点和从节点的运行状态(如是否在线、是否正常响应),每隔1秒向所有节点发送ping请求;
-
自动故障切换(Automatic Failover):当主节点宕机(哨兵检测到主节点失联超过指定时间),自动从从节点中选举一个最优的节点,切换为新主节点,同时让其他从节点同步新主节点的数据;
-
通知(Notification):当主从节点状态发生变化(如主节点宕机、切换新主节点),哨兵会通过配置的脚本,通知管理员或其他服务(如发送邮件、调用接口)。
(2)哨兵模式的底层原理(故障切换流程)
哨兵模式的核心是“多哨兵协作”,避免单哨兵故障导致监控失效,故障切换流程如下:
-
哨兵集群实时监控主节点,当某一个哨兵节点检测到主节点无响应(ping请求超时),会标记主节点为“主观下线”(仅该哨兵认为主节点宕机);
-
该哨兵节点向其他哨兵节点发送请求,询问它们是否也认为主节点宕机;
-
当超过“quorum”(法定人数,默认1)个哨兵节点都认为主节点宕机,主节点被标记为“客观下线”(所有哨兵都确认主节点宕机);
-
哨兵集群从所有从节点中,选举一个最优的从节点作为新主节点(选举规则:优先级最高→offset最大→runid最小);
-
哨兵集群向被选举的从节点发送slaveof no one命令,将其转为新主节点;
-
哨兵集群向其他从节点发送slaveof 新主节点IP 端口命令,让它们同步新主节点的数据;
-
当原主节点恢复后,哨兵会将其转为新主节点的从节点,避免原主节点再次成为主节点,导致数据混乱。
(3)哨兵模式的核心配置(生产落地)
哨兵节点需单独部署(不与主从节点部署在同一服务器),最少部署3个哨兵节点(避免单哨兵故障),核心配置(sentinel.conf)如下:
# 哨兵核心配置(每个哨兵节点配置一致) port 26379 # 哨兵默认端口(必须修改,避免与主从节点端口冲突) daemonize yes # 后台运行 dir /var/redis/sentinel # 哨兵日志和数据存储目录 logfile "/var/redis/sentinel/sentinel.log" # 日志文件,便于排查问题 # 监控主节点(核心配置) # 格式:sentinel monitor 主节点名称 主节点IP 主节点端口 法定人数(quorum) sentinel monitor mymaster 192.168.1.100 6379 2 # 监控名为mymaster的主节点,法定人数2(超过2个哨兵认为主节点下线,才触发故障切换) # 主节点密码(若主节点设置了密码,必须配置) sentinel auth-pass mymaster 123456 # 主节点失联超时时间(默认30000毫秒,30秒) # 超过该时间,哨兵认为主节点主观下线 sentinel down-after-milliseconds mymaster 30000 # 故障切换超时时间(默认180000毫秒,3分钟) # 超过该时间,故障切换失败,重新选举 sentinel failover-timeout mymaster 180000 # 每次故障切换,最多允许多少个从节点同时同步新主节点(默认1) # 设为1,避免多个从节点同时同步,拖累新主节点性能 sentinel parallel-syncs mymaster 1
(4)生产配置注意事项
-
哨兵节点数量:最少3个,且为奇数(避免投票平局,如3个、5个),确保故障切换时能形成多数票;
-
部署隔离:哨兵节点、主节点、从节点尽量部署在不同服务器,避免单服务器宕机导致整个高可用架构失效;
-
法定人数(quorum):建议设置为“哨兵节点数/2 + 1”(如3个哨兵,quorum设为2),确保多数哨兵确认主节点下线,避免误判;
-
超时时间:down-after-milliseconds建议设置为30秒,避免网络波动导致误判;failover-timeout设置为3分钟,给故障切换足够的时间。
6. 哨兵模式与Redis集群的区别(深岗高频追问)
答案:两者均是Redis高可用方案,但适用场景不同,核心区别如下,面试需精准区分:
|
对比维度 |
哨兵模式(Sentinel) |
Redis集群(Redis Cluster) |
|
核心作用 |
解决高可用(故障自动切换),基于主从复制,无分片能力 |
既解决高可用,又实现水平扩展(分片存储),分担单节点压力 |
|
数据存储 |
所有节点存储全量数据(主节点写,从节点读),内存开销大 |
数据分片存储(16384个哈希槽),每个节点存储部分数据,内存可水平扩展 |
|
适用场景 |
中小规模数据(如10GB以内),高可用要求高,读写分离需求明确 |
大规模数据(如10GB以上),需要水平扩展,分担单节点压力 |
|
运维成本 |
低,仅需部署主从节点+哨兵节点,配置简单 |
高,需部署集群节点(最少6个:3主3从),涉及分片、槽位分配等运维操作 |
|
故障切换 |
主节点宕机,哨兵切换从节点为新主节点,全量数据同步 |
主节点宕机,集群自动切换从节点为新主节点,仅同步该节点的分片数据,切换更快 |
生产选择原则:中小规模数据(10GB以内),优先用“主从复制+哨兵模式”(运维简单、成本低);大规模数据(10GB以上),用Redis集群(水平扩展、分担压力);核心业务可结合两者,实现高可用+高性能。
面试追问:Redis集群的哈希槽机制是什么?为什么用哈希槽而不是直接哈希?(答案:1. 哈希槽机制:Redis集群将所有数据分为16384个哈希槽,每个节点分配一定数量的槽位(如3主节点,每个节点分配5461个槽);客户端请求时,先对key进行CRC16哈希,再对16384取模,得到对应的哈希槽,路由到对应节点;2. 用哈希槽的原因:直接哈希会导致节点扩容/缩容时,所有key需重新哈希,成本极高;哈希槽可实现“槽位迁移”,扩容时仅迁移部分槽位的数据,无需全量迁移,降低运维成本)。
7. 主从复制与高可用的生产最佳实践(深岗落地题)
答案:结合生产场景,主从复制与高可用的最佳实践的核心是“稳定、高效、可运维”,具体方案如下:
-
架构选择: 中小规模(数据≤10GB):1主2从+3哨兵(奇数),主节点负责写,2个从节点负责读,3个哨兵实现故障自动切换;
-
大规模(数据>10GB):Redis集群(3主3从),每个主节点对应1个从节点,实现分片存储和高可用;
-
核心业务:多级缓存(本地缓存+Caffeine+Redis集群),兼顾速度、高可用和水平扩展。
-
配置优化: 主节点:开启AOF+RDB持久化(AOF everysec,RDB每天凌晨备份),设置maxmemory(服务器内存80%),配置volatile-lru淘汰策略;
-
从节点:开启RDB持久化(关闭AOF),设置slave-read-only yes,调整repl-backlog-size为10-50MB,配置级联同步(若从节点数量>2);
-
哨兵节点:部署在不同服务器,quorum设为哨兵数/2+1,超时时间合理设置(down-after-milliseconds=30000ms)。
-
监控与运维:实时监控:用Prometheus+Grafana监控主从同步状态、哨兵状态、内存使用率、QPS等指标,设置告警(如主从offset差距过大、哨兵节点下线);
-
定期备份:主节点每天凌晨执行bgsave备份RDB文件,异地存储,避免数据丢失;
-
故障演练:每月演练一次主节点宕机,验证哨兵故障切换是否正常,确保高可用架构有效;
-
版本统一:所有Redis节点(主从、哨兵、集群)版本一致,避免版本兼容问题。
生产踩坑总结:曾因哨兵节点与主节点部署在同一服务器,服务器宕机后,哨兵和主节点同时失效,导致高可用架构崩溃;后续将哨兵节点单独部署在3台不同服务器,结合监控告警,彻底解决该问题。
八、事务与 Lua 脚本
1. Redis 事务的核心概念、原理及常用命令(基础高频)
答案:Redis 事务是一组命令的集合,核心作用是“将多个命令打包,一次性、原子性执行”——要么所有命令都执行成功,要么所有命令都执行失败(不存在部分执行的情况),但需注意:Redis 事务不支持回滚(与关系型数据库事务不同),核心用于保证多命令执行的原子性,避免并发场景下的数据混乱。
(1)核心原理
Redis 事务通过“三个阶段”实现原子性,底层无锁机制,依赖队列和标记位实现:
-
开启事务(multi):客户端发送 multi 命令后,Redis 进入事务模式,后续所有命令不会立即执行,而是加入到事务队列中,返回“QUEUED”表示命令入队成功;
-
命令入队(queue):客户端发送的所有写命令(set、hset等)、读命令(get、hget等),均依次加入事务队列,Redis 仅记录命令,不执行;
-
执行事务(exec):客户端发送 exec 命令后,Redis 依次执行事务队列中的所有命令,执行完成后,返回所有命令的执行结果;若队列中存在命令错误(如语法错误),则整个事务取消,所有命令均不执行;若执行过程中出现运行时错误(如对 String 执行 hset 命令),仅该错误命令不执行,其他命令正常执行(无回滚)。
(2)核心常用命令(面试必背、生产常用)
-
multi:开启事务,进入事务模式;
-
exec:执行事务队列中的所有命令,结束事务模式;
-
discard:取消事务,清空事务队列,结束事务模式(未执行 exec 前可用);
-
watch:监控一个或多个键,若在事务执行前,被监控的键被其他客户端修改,则事务取消(exec 执行失败,返回 nil),用于实现乐观锁;
-
unwatch:取消对所有键的监控,若已执行 exec 或 discard,无需手动执行 unwatch。
(3)核心注意点(面试高频追问)
1. Redis 事务不支持回滚:与 MySQL 事务不同,Redis 没有回滚机制,即使事务中某条命令执行失败,其他命令仍会继续执行(语法错误除外);原因:Redis 为了追求高性能,简化了事务实现,避免回滚带来的性能开销;
2. 两种错误对事务的影响:
-
语法错误(如命令拼写错误、参数错误):命令入队时就会报错,Redis 会标记事务为“无效”,执行 exec 时,整个事务取消,所有命令均不执行;
-
运行时错误(如对 String 类型执行 hset、incr 非数字字符串):命令入队时无报错,执行 exec 时,该错误命令不执行,其他命令正常执行,不会回滚。
3. watch 命令的乐观锁机制:watch 监控的键,若在事务执行前被其他客户端修改,exec 会返回 nil,事务取消;适合并发修改同一数据的场景(如秒杀、库存扣减),避免超卖。
生产踩坑:误以为 Redis 事务支持回滚,在库存扣减场景中,某条命令执行失败后,未做补偿处理,导致数据不一致;后续通过“watch 监控库存键 + 事务执行 + 失败重试”的方案,解决该问题。
面试追问:Redis 事务为什么不支持回滚?如何实现 Redis 事务的“回滚效果”?(答案:1. 不支持回滚原因:Redis 设计初衷是追求高性能,回滚机制需要记录事务执行前的状态,会增加内存开销和执行延迟,且实际业务中,Redis 事务的命令错误多为语法错误,可在入队时发现,运行时错误较少;2. 实现回滚效果:手动补偿——事务执行失败后,执行反向命令(如 set 对应 del、incr 对应 decr),恢复数据;或通过 watch 监控键,避免并发修改导致的数据不一致)。
2. Redis 事务的应用场景及生产最佳实践
答案:Redis 事务适合“多命令需原子执行”的场景,核心应用场景集中在并发控制、数据一致性保障,生产中需结合 watch 命令和重试机制,避免数据混乱,具体如下:
(1)核心应用场景
-
库存扣减(高频场景):如秒杀活动,需原子执行“查询库存→扣减库存→记录订单”三个命令,避免超卖(结合 watch 监控库存键);
-
用户积分操作:原子执行“查询积分→扣减积分→增加消费记录”,避免积分扣减后未记录,或记录后未扣减积分;
-
批量操作原子化:如批量删除多个键、批量更新多个字段,确保所有操作要么全部成功,要么全部失败,避免部分操作成功导致的数据不一致。
(2)生产最佳实践(避坑重点)
-
结合 watch 实现乐观锁:并发修改同一数据时,先用 watch 监控目标键,再开启事务,执行命令后 exec;若 exec 失败(返回 nil),说明键被修改,重试整个流程(最多重试3次);
-
避免事务中包含大量命令:Redis 事务执行时,会阻塞单线程(所有命令一次性执行,期间无法处理其他请求),若事务中命令过多(如1000+),会导致服务卡顿,建议拆分事务,每次事务包含不超过10条命令;
-
提前校验命令语法:事务执行前,先校验所有命令的语法和参数,避免语法错误导致整个事务取消;
-
避免事务中包含读命令:读命令(如 get)在事务队列中不会立即执行,无法获取实时数据,若需读取数据用于后续命令,建议在开启事务前读取,或使用 Lua 脚本替代;
-
失败补偿机制:事务执行失败后,手动执行反向命令恢复数据,或记录日志,后续通过定时任务补偿,避免数据不一致。
生产踩坑:在秒杀场景中,未使用 watch 监控库存键,多个客户端同时执行事务扣减库存,导致超卖(库存变为负数);后续优化为“watch 库存键 + 事务扣减 + 失败重试”,同时设置库存下限校验(扣减后库存≥0),彻底解决超卖问题。
3. Lua 脚本的核心作用、原理及优势(深岗高频)
答案:Lua 脚本是 Redis 支持的轻量级脚本语言,可将多个 Redis 命令编写成一个 Lua 脚本,提交给 Redis 一次性执行,核心作用是“实现复杂的原子操作”,弥补 Redis 事务的不足,同时提升性能,是生产中实现高并发、数据一致性的核心方案。
(1)核心原理
Redis 内置 Lua 解释器,客户端将编写好的 Lua 脚本发送给 Redis 后,Redis 会原子性执行脚本中的所有命令,执行过程中不会被其他客户端的请求打断(单线程特性保障);脚本执行完成后,返回脚本的执行结果。
关键特性:
-
原子性:脚本中的所有命令一次性执行,不会被中断,等同于“强化版事务”;
-
复用性:Lua 脚本可存储在 Redis 中(用 script load 命令),后续通过脚本 SHA1 值调用,避免重复传输脚本,提升性能;
-
灵活性:可编写复杂逻辑(如条件判断、循环),实现 Redis 原生命令无法实现的功能(如库存扣减+限流)。
(2)核心优势(对比 Redis 事务)
|
对比维度 |
Redis 事务 |
Lua 脚本 |
|
原子性 |
原子执行,但运行时错误无法回滚,仅语法错误取消事务 |
完全原子性,脚本中所有命令要么全部执行,要么全部不执行(执行中报错,脚本终止) |
|
逻辑复杂度 |
仅支持简单的命令队列,无条件判断、循环等逻辑 |
支持条件判断、循环、函数等复杂逻辑,可实现复杂业务场景 |
|
性能 |
多命令需多次网络传输,有网络开销 |
一次网络传输,脚本在 Redis 端执行,减少网络开销,性能更高 |
|
复用性 |
无复用性,每次需重新发送所有命令 |
可存储在 Redis 中,通过 SHA1 值调用,复用性强 |
(3)核心常用命令(Lua 脚本操作)
-
eval 脚本 键数 key1 key2 ... arg1 arg2 ...:执行 Lua 脚本,key 是脚本中需要操作的键,arg 是脚本中的参数(如 eval "return redis.call('get', KEYS[1])" 1 user:1001,获取 user:1001 的值);
-
script load 脚本:将 Lua 脚本加载到 Redis 中,返回脚本的 SHA1 值(如 script load "return redis.call('incr', KEYS[1])");
-
evalsha SHA1值 键数 key1 key2 ... arg1 arg2 ...:通过脚本 SHA1 值调用已加载的脚本,避免重复传输脚本(生产推荐);
-
script exists SHA1值:判断脚本是否已加载到 Redis 中;
-
script flush:清空 Redis 中所有已加载的 Lua 脚本;
-
script kill:终止当前正在执行的 Lua 脚本(仅当脚本执行时间过长,阻塞服务时使用)。
面试追问:Redis 为什么推荐用 Lua 脚本替代事务?(答案:1. 原子性更强:Lua 脚本执行过程中不会被中断,即使出现运行时错误,脚本会立即终止,不会执行后续命令,避免部分执行导致的数据不一致;2. 逻辑更灵活:支持条件判断、循环等复杂逻辑,可实现事务无法实现的业务场景(如库存扣减+限流、多键关联操作);3. 性能更高:一次网络传输,减少网络开销,且脚本在 Redis 端执行,无需多次交互;4. 复用性强:可存储在 Redis 中,重复调用,提升开发效率)。
4. Lua 脚本的生产应用场景、编写规范及踩坑点(深岗落地)
(1)核心生产应用场景
-
秒杀/库存扣减(核心场景):结合条件判断,实现“库存校验→扣减库存→记录日志”的原子操作,避免超卖、漏卖(比事务更可靠);
-
分布式限流:如基于滑动窗口限流,用 Lua 脚本实现“判断当前窗口内请求数→是否允许请求→更新窗口请求数”的原子逻辑;
-
多键原子操作:如同时操作多个 Hash 字段、多个 String 键,确保所有操作原子执行(如用户注册时,同时设置用户信息、用户标签、积分);
-
自定义复杂逻辑:如实现分布式锁(比 setnx 更可靠)、数据批量处理(循环处理多个键)等。
(2)Lua 脚本编写规范(生产避坑)
-
使用 redis.call() 执行 Redis 命令:脚本中操作 Redis 必须使用 redis.call()(如 redis.call('set', KEYS[1], ARGV[1])),若命令执行失败,会抛出异常,终止脚本;
-
避免使用 redis.pcall():redis.pcall() 与 redis.call() 功能类似,但命令执行失败时不会抛出异常,会返回错误信息,继续执行后续命令,易导致数据不一致,生产不推荐;
-
脚本中必须使用 KEYS 和 ARGV 传递参数:禁止在脚本中硬编码键名和参数(如直接写 'user:1001'),否则脚本无法复用,且无法通过 Redis 集群的槽位路由(集群模式下,KEYS 必须属于同一槽位);
-
控制脚本执行时间:Redis 单线程执行脚本,若脚本执行时间过长(超过100ms),会阻塞所有客户端请求,建议脚本执行时间控制在10ms以内,避免复杂循环;
-
避免脚本中包含读命令后执行写命令:读命令(如 get)获取的数据可能被其他客户端修改,导致写命令执行基于旧数据,建议通过 KEYS 监控相关键,或确保读、写命令的原子性。
(3)生产踩坑及解决方案
踩坑1:Lua 脚本执行时间过长(如包含1000次循环),导致 Redis 单线程阻塞,客户端请求超时;
解决方案:拆分脚本,将复杂逻辑拆分为多个短脚本,每个脚本执行时间控制在10ms以内;避免循环遍历大量键,改用 scan 命令分批处理。
踩坑2:脚本中硬编码键名,部署到 Redis 集群后,键不属于同一槽位,导致脚本执行失败;
解决方案:所有键通过 KEYS 传递,且确保所有 KEYS 属于同一槽位(可通过键名前缀+哈希标签,如 {user}:1001、{user}:1002,确保同一前缀的键分配到同一槽位)。
踩坑3:使用 redis.pcall() 执行命令,某条命令执行失败后,脚本继续执行后续命令,导致数据不一致;
解决方案:统一使用 redis.call(),脚本执行失败时立即终止,同时在客户端添加重试机制,确保脚本执行成功。
踩坑4:未加载脚本直接使用 evalsha 调用,导致报错“NOSCRIPT No matching script”;
解决方案:生产中,先通过 script load 加载脚本,获取 SHA1 值后再调用;若脚本未加载,客户端捕获异常,先加载脚本再重试。
5. 面试高频 Lua 脚本示例(必背)
以下3个示例是面试高频提问,需熟练掌握,能写出脚本并解释逻辑:
(1)示例1:库存扣减(秒杀场景,避免超卖)
需求:校验库存是否充足,若充足则扣减库存,返回1;若库存不足,返回0;若库存为负数,返回-1。
-- 脚本逻辑:KEYS[1] = 库存键名,ARGV[1] = 扣减数量 local stock = redis.call('get', KEYS[1]) if not stock then return -1 -- 库存键不存在 end local stock_num = tonumber(stock) local decr_num = tonumber(ARGV[1]) if stock_num < decr_num then return 0 -- 库存不足 end -- 扣减库存 redis.call('decrby', KEYS[1], decr_num) return 1 -- 扣减成功
调用方式:eval 脚本 1 stock:1001 1(扣减商品1001的库存1个);或加载脚本后用 evalsha 调用。
(2)示例2:分布式锁(基础版,避免死锁)
需求:实现分布式锁,key 为锁标识,value 为唯一标识(如客户端ID),设置过期时间,避免死锁;获取锁成功返回1,失败返回0。
-- 脚本逻辑:KEYS[1] = 锁键名,ARGV[1] = 客户端唯一标识,ARGV[2] = 过期时间(秒) local lock = redis.call('setnx', KEYS[1], ARGV[1]) if lock == 1 then redis.call('expire', KEYS[1], ARGV[2]) -- 设置过期时间,避免死锁 return 1 -- 获取锁成功 end return 0 -- 获取锁失败
(3)示例3:滑动窗口限流(1分钟内最多100次请求)
需求:限制某个接口(key)1分钟内最多100次请求,返回当前请求次数,超过限制返回-1。
-- 脚本逻辑:KEYS[1] = 限流键名(如 api:limit:user1001),ARGV[1] = 窗口时间(秒),ARGV[2] = 最大请求数 local current = redis.call('incr', KEYS[1]) if current == 1 then redis.call('expire', KEYS[1], ARGV[1]) -- 第一次请求,设置窗口时间 end if current > tonumber(ARGV[2]) then return -1 -- 超过限流 end return current -- 返回当前请求次数
6. 事务与 Lua 脚本的核心区别(面试必背)
两者均用于保证多命令的原子性,但适用场景不同,核心区别如下,需精准区分:
-
原子性细节:事务仅保证“命令队列原子执行”,运行时错误无法回滚,部分命令可能执行;Lua 脚本保证“完全原子性”,执行中报错立即终止,所有命令要么全部成功,要么全部失败;
-
逻辑复杂度:事务仅支持简单的命令队列,无复杂逻辑;Lua 脚本支持条件判断、循环、函数,可实现复杂业务逻辑;
-
性能:事务多命令需多次网络传输,性能一般;Lua 脚本一次网络传输,性能更高;
-
复用性:事务无复用性,每次需重新发送命令;Lua 脚本可存储在 Redis 中,重复调用,复用性强;
-
适用场景:事务适合简单的多命令原子操作(如批量删除、批量更新);Lua 脚本适合复杂的原子操作(如秒杀、限流、多键关联操作)。
面试追问:生产中,什么时候用事务?什么时候用 Lua 脚本?(答案:1. 用事务:简单的多命令原子操作,逻辑简单,无需条件判断、循环,且能接受“运行时错误不回滚”(如批量删除多个无关键、批量设置过期时间);2. 用 Lua 脚本:复杂的原子操作,需要条件判断、循环,或对数据一致性要求高(如秒杀、限流、库存扣减),且希望减少网络开销、提升性能)。
九、分布式锁(面试高频,深岗必问)
分布式锁是分布式系统中解决“并发竞争资源”的核心方案,Redis因高性能、易用性,成为分布式锁的首选实现方式(替代数据库锁)。核心考点集中在“实现方案、优缺点、避坑点、优化方案”,贴合生产落地,以下是完整高频考点,覆盖面试全场景。
1. 分布式锁的核心定义与核心要求(基础必背)
答案:分布式锁是指在分布式系统中,多个节点(或服务)通过共享存储(如Redis、ZooKeeper),实现对同一资源的互斥访问,确保同一时间只有一个节点能操作资源,避免并发修改导致的数据不一致(如超卖、库存负数)。
分布式锁必须满足的4个核心要求(面试必答):
-
互斥性:核心要求,同一时间只有一个客户端能获取到锁,其他客户端无法获取;
-
安全性:避免死锁(锁必须有过期时间,防止客户端崩溃后锁无法释放);避免误释放(客户端只能释放自己持有的锁,不能释放其他客户端的锁);
-
可用性:锁服务需高可用,Redis宕机后,锁仍能正常获取和释放(结合主从、哨兵或集群);
-
一致性:锁的获取和释放需原子操作,避免并发场景下的锁状态混乱。
面试追问:为什么不用数据库锁(如MySQL行锁)实现分布式锁?(答案:1. 性能差:数据库锁依赖数据库事务,并发高时会导致锁等待、事务阻塞,吞吐量低;2. 可用性差:数据库宕机后,锁服务完全不可用;3. 扩展性差:无法应对大规模分布式系统的并发需求,而Redis性能高、支持高可用,更适合分布式锁场景)。
2. Redis分布式锁的3种实现方案(从基础到高级,面试层层递进)
Redis实现分布式锁有3种核心方案,难度递增,生产中优先使用高级方案,面试需掌握每种方案的原理、优缺点及适用场景。
(1)方案1:基础版(setnx + expire,面试入门)
核心原理:利用setnx命令的“不存在则设置”特性实现互斥,结合expire命令设置过期时间,避免死锁。
核心命令:
# 1. 获取锁:key=lock:stock(锁标识),value=客户端唯一标识(如UUID) setnx lock:stock uuid123 # 2. 设置过期时间(避免死锁),单位秒 expire lock:stock 3 # 3. 释放锁:先判断锁是自己的,再删除(非原子操作,有问题) if redis.call('get', 'lock:stock') == 'uuid123' then return redis.call('del', 'lock:stock') end
优点:实现简单,入门级方案,适合面试基础提问;
缺点(致命,生产禁用):
-
原子性问题:setnx和expire是两个独立命令,若setnx执行成功后,客户端崩溃,expire未执行,锁会永久存在,导致死锁;
-
释放锁非原子:判断锁和删除锁是两个命令,若判断完成后,锁过期自动释放,其他客户端已获取锁,此时删除锁会误释放他人的锁。
(2)方案2:进阶版(set nx ex 原子命令,生产基础方案)
核心原理:Redis 2.6.12后,支持set命令的组合参数,将setnx和expire合并为一个原子命令,解决基础版的原子性问题。
核心命令:
# 获取锁:nx=不存在则设置,ex=设置过期时间(3秒),key=锁标识,value=客户端唯一标识 set lock:stock uuid123 nx ex 3 # 释放锁:用Lua脚本实现“判断+删除”原子操作(解决误释放问题) eval "local lockVal = redis.call('get', KEYS[1]); if lockVal == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end" 1 lock:stock uuid123
优点:解决了基础版的原子性问题,实现简单,无死锁风险(有过期时间),无锁误释放问题(Lua脚本原子判断+删除);
缺点(生产需优化):
-
锁过期问题:若业务执行时间超过锁过期时间,锁会自动释放,其他客户端获取锁,导致并发问题(如库存扣减时,第一个客户端未执行完,第二个客户端已获取锁);
-
高可用问题:若Redis主节点宕机,从节点未同步锁数据,哨兵切换后,新主节点无锁信息,导致多个客户端同时获取锁。
生产优化:给锁添加“续期机制”(如用Lua脚本定时延长锁过期时间),结合Redis主从+哨兵,保证高可用。
(3)方案3:高级版(Redisson分布式锁,生产首选)
核心原理:Redisson是Redis的Java客户端,封装了分布式锁的完整实现,解决了进阶版的所有缺点,支持自动续期、高可用、公平锁/非公平锁、可重入锁等特性,无需手动编写Lua脚本和续期逻辑。
核心特性(面试必背):
-
自动续期:Redisson会启动一个后台线程(Watch Dog,看门狗),每隔1/3锁过期时间,自动延长锁的过期时间,避免业务未执行完锁过期;
-
原子性:底层用Lua脚本实现锁的获取、释放、续期,确保原子操作;
-
高可用:支持Redis主从、哨兵、集群模式,主节点宕机后,自动切换到从节点,锁服务不中断;
-
可重入锁:支持同一客户端多次获取同一把锁(如递归调用场景),避免死锁;
-
公平/非公平锁:默认非公平锁,可配置为公平锁(按请求顺序获取锁),避免饥饿问题。
核心代码示例(Java,面试必提):
// 1. 获取Redisson客户端 RedissonClient redisson = Redisson.create(config); // 2. 获取分布式锁(可重入锁) RLock lock = redisson.getLock("lock:stock"); try { // 3. 获取锁:waitTime=10秒(等待锁的时间),leaseTime=-1(自动续期,默认30秒) boolean acquired = lock.tryLock(10, -1, TimeUnit.SECONDS); if (acquired) { // 执行业务逻辑(如库存扣减) } } finally { // 4. 释放锁(确保最终释放,避免死锁) if (lock.isHeldByCurrentThread()) { lock.unlock(); } }
优点:封装完善,无需手动处理锁过期、续期、高可用问题,生产落地成本低,稳定性高;
缺点:需引入Redisson依赖,增加少量依赖成本(可忽略,生产普遍使用)。
面试追问:Redisson的看门狗(Watch Dog)原理是什么?(答案:1. 当leaseTime=-1时,Redisson会启动看门狗线程,默认锁过期时间30秒;2. 看门狗每隔10秒(30秒的1/3),会执行Lua脚本,将锁的过期时间重新设置为30秒;3. 若客户端崩溃,看门狗线程终止,锁会在30秒后自动释放,避免死锁;4. 业务执行完成后,手动unlock,看门狗线程自动终止)。
3. Redis分布式锁的生产踩坑及解决方案(深岗高频)
生产中使用Redis分布式锁,最容易踩4个坑,面试常问“如何避免”,需结合实际场景记忆。
(1)踩坑1:锁过期导致并发问题
场景:业务执行时间(如5秒)超过锁过期时间(如3秒),锁自动释放,其他客户端获取锁,导致同一资源被同时操作(如超卖)。
解决方案:
-
使用Redisson分布式锁,开启自动续期(leaseTime=-1),由看门狗自动延长锁过期时间;
-
手动续期:若不使用Redisson,可在业务执行中,定时(如每隔1秒)执行Lua脚本,延长锁过期时间;
-
合理设置锁过期时间:根据业务最大执行时间,设置过期时间(如业务最长5秒,设置10秒),预留足够缓冲。
(2)踩坑2:主从切换导致锁丢失
场景:Redis主节点宕机,从节点未同步锁数据(异步同步),哨兵切换新主节点后,新主节点无锁信息,多个客户端同时获取锁。
解决方案:
-
使用Redis集群模式(Redis Cluster),确保锁数据分布在不同节点,避免单节点宕机导致锁丢失;
-
使用Redisson的“红锁”(RedLock):同时向多个Redis节点(至少3个)获取锁,只有超过半数节点获取成功,才认为锁获取成功,避免单节点故障导致锁失效;
-
优化主从同步:增大复制积压缓冲区,减少主从同步延迟,降低切换时锁丢失的概率。
(3)踩坑3:误释放他人的锁
场景:客户端A获取锁后,锁过期自动释放,客户端B获取锁;此时客户端A执行完成,误删除客户端B的锁,导致锁失效。
解决方案:
-
锁value必须设置为“客户端唯一标识”(如UUID、客户端ID+时间戳);
-
释放锁时,必须用Lua脚本实现“判断value是否匹配+删除锁”的原子操作,避免判断和删除分离。
(4)踩坑4:锁等待时间过长,导致服务超时
场景:高并发场景下,大量客户端竞争同一把锁,部分客户端等待锁的时间过长(如超过接口超时时间),导致接口报错。
解决方案:
-
设置合理的锁等待时间(tryLock的waitTime),如10秒,超过等待时间则放弃获取锁,返回失败(避免无限等待);
-
使用公平锁:Redisson配置公平锁,按请求顺序获取锁,避免部分客户端长期无法获取锁(饥饿问题);
-
拆分锁粒度:将大锁拆分为小锁(如库存锁拆分为stock:1001、stock:1002),减少并发竞争。
4. 分布式锁的其他实现方案(面试对比)
面试常问“Redis分布式锁与其他分布式锁的区别”,需掌握3种主流方案的对比,突出Redis的优势。
|
实现方案 |
核心优势 |
核心缺点 |
适用场景 |
|
Redis分布式锁 |
性能高、实现简单、支持高可用、生态完善(Redisson) |
主从切换可能丢失锁、需处理锁续期 |
高并发、高性能需求场景(如秒杀、库存) |
|
ZooKeeper分布式锁 |
一致性强、无锁丢失问题、支持公平锁 |
性能低、实现复杂、集群部署运维成本高 |
一致性要求极高、并发不高的场景 |
|
数据库锁(MySQL) |
无需额外组件、实现简单 |
性能差、可用性低、易阻塞 |
并发极低、小型分布式系统 |
面试追问:为什么生产中优先用Redis分布式锁,而不用ZooKeeper?(答案:1. 性能:Redis是内存数据库,锁操作是内存操作,性能远高于ZooKeeper(磁盘操作);2. 易用性:Redis分布式锁实现简单,Redisson封装完善,无需复杂开发;3. 运维成本:Redis集群部署、运维更简单,ZooKeeper集群运维复杂,易出现脑裂等问题;4. 适配场景:分布式锁多用于高并发场景,Redis的高性能更贴合需求)。
5. 分布式锁的生产最佳实践(深岗落地题)
结合前文踩坑点和优化方案,总结生产落地的核心最佳实践,面试时可直接套用,体现生产经验。
-
锁实现方案:优先使用Redisson分布式锁,开启自动续期(leaseTime=-1),避免手动处理续期和原子性问题;
-
锁key设计:锁key需具备唯一性和可读性,格式为“lock:业务模块:资源标识”(如lock:stock:1001、lock:order:2026),避免锁冲突;
-
锁粒度:尽量拆分锁粒度,避免大锁(如库存锁按商品ID拆分,而非全局库存锁),减少并发竞争;
-
高可用保障:Redis部署主从+哨兵或集群模式,确保锁服务高可用;高一致性场景,使用Redisson红锁;
-
异常处理: 获取锁时,设置合理的等待时间(如10秒),超过等待时间返回失败,避免无限等待;
-
释放锁时,必须在finally中释放,确保即使业务报错,锁也能正常释放;
-
捕获锁相关异常(如Redis连接异常),做降级处理(如返回失败,提示用户重试)。
-
监控告警:监控锁的获取成功率、等待时间、释放情况,设置告警(如锁等待时间超过5秒、获取成功率低于90%),及时排查问题;
-
避免过度使用:非并发竞争场景,无需使用分布式锁(如查询操作),避免增加系统复杂度和性能开销。
6. 面试高频追问(必背)
以下追问是面试中高频出现的,需熟练掌握,精准回答,体现专业性:
-
问:Redis分布式锁的可重入性怎么实现?(答案:Redisson的可重入锁,底层通过“计数器”实现:客户端第一次获取锁,计数器设为1;同一客户端再次获取锁,计数器+1;释放锁时,计数器-1,计数器为0时,删除锁);
-
问:Redisson红锁的原理是什么?什么时候用?(答案:1. 原理:向至少3个独立的Redis节点(非主从关系)发送获取锁请求,只有超过半数(≥2个)节点获取成功,才认为锁获取成功;2. 适用场景:对锁一致性要求极高,不允许锁丢失的场景(如金融交易),普通场景无需使用,性能略低);
-
问:锁的过期时间设置过长或过短,有什么问题?(答案:① 过长:锁释放慢,若客户端崩溃,锁长期占用,导致其他客户端无法操作资源;② 过短:业务未执行完,锁自动释放,导致并发问题);
-
问:Redis集群模式下,分布式锁如何保证槽位一致性?(答案:锁key需通过“哈希标签”(如{lock:stock}:1001),确保锁key落在同一槽位,避免跨槽操作导致锁失效;Redisson会自动处理槽位路由,无需手动干预)。
十、性能优化(面试高频,生产必懂)
Redis性能优化是面试深岗(中级/高级)的核心考点,核心围绕“内存优化、IO优化、并发优化、部署优化”四大维度,结合生产踩坑点和调优实践,以下是完整高频考点,覆盖面试全场景,同时贴合实际工作落地。
1. 性能优化的核心指标(基础必背)
答案:衡量Redis性能的4个核心指标,优化的目标是“提升吞吐量、降低延迟、减少内存占用、保证高可用”,面试需先明确优化目标:
-
吞吐量(QPS):单位时间内Redis处理的请求数,Redis单机QPS可达10万+,优化目标是根据业务需求提升QPS,避免瓶颈;
-
响应延迟(Latency):Redis处理单个请求的时间,理想延迟在1ms以内,优化目标是降低延迟,避免客户端请求超时;
-
内存使用率:控制Redis内存占用在合理范围(如服务器内存80%以内),避免内存溢出、swap使用,减少内存浪费;
-
可用性:通过高可用部署,确保Redis服务持续可用,优化目标是减少宕机时间,故障切换快速完成。
面试追问:如何监控Redis的性能指标?(答案:1. 原生命令:info stats(查看QPS、延迟、连接数)、info memory(查看内存使用)、info persistence(查看持久化状态);2. 监控工具:Prometheus+Grafana(可视化监控,设置告警)、RedisInsight(官方可视化工具);3. 日志分析:查看Redis日志,排查慢查询、错误信息)。
2. 内存优化(核心高频,生产重点)
答案:Redis是内存数据库,内存优化是性能优化的基础,核心目标是“减少内存占用、避免内存浪费、降低内存淘汰频率”,常用优化方案如下(按优先级排序):
(1)合理选择数据结构,避免内存浪费
-
优先使用高效数据结构:如用Hash存储对象(比多个String节省内存)、用ZSet存储有序数据(比List+Sort高效)、用Bitmaps存储布尔型数据(如签到)、用HyperLogLog统计基数(如UV);
-
控制数据结构编码:利用Redis的编码转换规则,控制数据规模,避免编码切换导致内存暴涨(如Hash元素≤512个、key/value≤64字节,使用ziplist编码,内存更节省);
-
避免大key:大key(如超过100MB的String、包含10万+元素的List)会导致内存占用过高、删除/序列化耗时过长、网络传输缓慢,优化方案:拆分大key(如大Hash拆分为多个小Hash,大List拆分为多个小List)、压缩大key(如用gzip压缩String类型的大文本)。
(2)开启内存淘汰策略,合理设置maxmemory
-
核心配置:设置maxmemory(如服务器内存80%),避免内存溢出;优先选择volatile-lru(有过期时间的缓存场景)或allkeys-lru(无过期时间的缓存场景),避免使用noeviction;
-
优化细节:给所有缓存键设置合理的过期时间,避免无过期时间的键长期占用内存;定期清理过期键(用scan命令批量删除,避免keys命令阻塞)。
(3)内存碎片优化
Redis长期运行后,频繁的键删除、修改会导致内存碎片(内存空间碎片化,可用内存不足但总内存未达maxmemory),导致内存使用率虚高。
-
查看内存碎片率:info memory 中的 mem_fragmentation_ratio(碎片率),正常范围1.0-1.5,超过1.5说明碎片严重;
-
优化方案:① 开启自动内存整理(Redis 4.0+,配置activedefrag yes),Redis会后台整理内存碎片;② 手动重启Redis(适合非核心场景,重启后碎片率重置,但会丢失未持久化数据,需提前备份);③ 避免频繁删除大key,减少碎片产生。
(4)其他内存优化技巧
-
关闭无用功能:如关闭Redis的持久化(非核心场景,不推荐)、关闭保护模式(仅内网环境),减少内存占用;
-
合理设置过期时间:避免所有键同时过期(错开过期时间),减少内存淘汰时的CPU开销;
-
使用共享对象池:Redis对小整数(0-9999)、空字符串等会使用共享对象池,避免重复创建对象,节省内存(无需手动配置,默认开启)。
生产踩坑:某业务使用大key存储商品详情(String类型,约50MB),导致Redis删除该key时阻塞100ms+,客户端请求超时;后续拆分大key为“商品基本信息”“商品详情”“商品图片”3个小key,同时压缩文本内容,解决阻塞问题,内存占用也降低了40%。
3. IO优化(降低延迟,提升吞吐量)
答案:Redis的IO瓶颈主要来自“持久化IO”“网络IO”,优化核心是“减少IO开销、提升IO效率”,常用方案如下:
(1)持久化IO优化(核心,避免IO阻塞)
-
AOF优化:① 选择合适的刷盘策略(everysec,平衡一致性和性能,生产推荐),避免always(IO开销过大)和no(数据一致性差);② 合理设置AOF重写阈值(auto-aof-rewrite-min-size=64MB,auto-aof-rewrite-percentage=100%),避免频繁重写;③ 重写时预留足够内存,避免fork子进程导致内存不足;
-
RDB优化:① 避免在业务高峰期触发bgsave(手动触发在凌晨低峰期);② 合理设置RDB快照间隔(如每天1次全量备份),结合AOF使用,减少RDB的IO压力;③ 存储RDB/AOF文件到独立磁盘(如SSD),提升IO读写速度;
-
禁用不必要的持久化:非核心缓存场景(如临时会话),可关闭持久化,彻底消除持久化IO开销。
(2)网络IO优化
-
减少网络传输次数:① 用Pipeline批量执行命令(如批量set、批量get),减少TCP连接往返次数(单次Pipeline可执行100-1000条命令,避免过多导致阻塞);② 用Lua脚本替代多命令执行,一次网络传输完成复杂操作;
-
减少网络传输体积:① 压缩数据(如用gzip压缩String类型数据);② 避免返回大量数据(如用scan命令分批获取数据,避免keys、hgetall命令返回全量数据);③ 合理使用序列化方式(如Protocol Buffers替代JSON,体积更小);
-
优化网络配置:① 开启TCP_NODELAY(禁用Nagle算法),减少网络延迟(Redis默认开启);② 增加Redis的TCP连接队列(tcp-backlog),避免连接被拒绝;③ 部署Redis和应用服务在同一内网,减少跨网传输延迟。
面试追问:Pipeline和事务的区别?(答案:1. 原子性:事务保证所有命令原子执行,Pipeline不保证原子性(某条命令失败,其他命令仍会执行);2. 用途:事务用于多命令原子操作,Pipeline用于批量执行命令,提升网络效率;3. 执行方式:事务是“命令入队→统一执行”,Pipeline是“批量发送命令→批量返回结果”,无事务队列的标记过程)。
4. 并发优化(提升吞吐量,避免阻塞)
答案:Redis是单线程模型,并发优化的核心是“避免单线程阻塞、充分利用CPU资源、减少并发竞争”,常用方案如下:
(1)避免单线程阻塞(核心重点)
-
禁止执行慢命令:慢命令(如keys *、hgetall、sort、union)会阻塞单线程,导致所有请求排队,优化方案:① 用scan替代keys(分批获取键);② 用hscan、zscan替代hgetall、zrange(分批获取元素);③ 避免复杂排序、聚合操作,移至应用层处理;
-
控制Lua脚本执行时间:Lua脚本执行时间控制在10ms以内,避免复杂循环,防止阻塞单线程;
-
优化客户端连接:① 限制客户端最大连接数(maxclients,默认10000),避免连接过多导致Redis卡顿;② 使用连接池(如Java的JedisPool、Redisson连接池),减少TCP连接建立/关闭的开销;③ 及时关闭闲置连接(配置timeout,如300秒),释放资源。
(2)利用多线程/多实例提升并发能力
-
Redis 6.0+ 多线程优化:开启IO多线程(配置io-threads 4,线程数建议为CPU核心数的1/2),IO多线程仅处理“网络IO”(接收请求、返回结果),核心命令执行仍为单线程,避免线程安全问题,可提升2-3倍吞吐量;
-
多实例部署:单Redis实例性能有限,可部署多个Redis实例(如按业务模块拆分),分担并发压力(如商品缓存一个实例、用户缓存一个实例);
-
读写分离:主节点负责写,从节点负责读,将读请求分流到从节点,提升读吞吐量(结合哨兵模式,确保从节点高可用)。
(3)减少并发竞争
-
拆分锁粒度:分布式锁按资源标识拆分(如库存锁按商品ID拆分),减少多个客户端竞争同一把锁;
-
避免热点key并发访问:热点key(如首页热门商品)会导致大量请求集中在单节点,优化方案:① 热点key缓存到本地(如应用层Caffeine缓存),减少Redis访问;② 热点key分片(如将一个热点key拆分为多个子key,分散到不同实例);③ 延迟更新热点key(如定时更新,避免高频写入)。
生产踩坑:某业务在高峰期执行keys * 命令,导致Redis单线程阻塞5秒,所有客户端请求超时;后续禁用keys命令,改用scan命令分批获取键,同时在监控中添加慢命令告警(配置slowlog-log-slower-than=1000,记录延迟超过1ms的命令),彻底解决阻塞问题。
5. 部署优化(高可用+高性能,生产落地)
答案:部署优化是性能优化的兜底保障,核心是“高可用部署、合理硬件配置、避免单点故障”,结合业务场景选择合适的部署架构:
(1)部署架构选择
-
中小规模(数据≤10GB,QPS≤1万):1主2从+3哨兵,主节点写,从节点读,哨兵实现故障自动切换,部署简单、运维成本低;
-
大规模(数据>10GB,QPS>1万):Redis集群(3主3从),分片存储,分担内存和并发压力,支持水平扩展,结合哨兵实现高可用;
-
核心业务:多级缓存(本地缓存+Caffeine+Redis),减少Redis访问压力,提升响应速度。
(2)硬件配置优化
-
CPU:选择多核CPU(如8核16线程),Redis 6.0+ 多线程可充分利用多核资源;避免CPU超频,保证稳定性;
-
内存:优先使用DDR4内存,容量根据业务需求配置(如16GB、32GB),预留20%-30%空闲内存,避免swap使用(swap会导致延迟暴涨);
-
磁盘:持久化文件(RDB/AOF)存储在SSD磁盘,SSD读写速度远高于HDD,减少持久化IO延迟;
-
网络:使用千兆网卡,避免网络带宽瓶颈;部署在同一内网,减少跨网传输延迟。
(3)系统配置优化
-
关闭swap:修改/etc/sysctl.conf,设置vm.swappiness=0,禁止Redis使用swap,避免内存交换导致延迟;
-
调整TCP配置:① 增大TCP连接队列(net.core.somaxconn=1024);② 开启TCP_NODELAY(echo 1 > /proc/sys/net/ipv4/tcp_nodelay);③ 调整TCP超时时间(net.ipv4.tcp_keepalive_time=600);
-
关闭大页内存:大页内存会导致Redis内存碎片增加,关闭命令:echo never > /sys/kernel/mm/transparent_hugepage/enabled;
-
设置Redis后台运行(daemonize yes),避免终端关闭导致Redis进程退出。
6. 性能优化的生产排查流程(面试高频,深岗必问)
答案:生产中Redis性能异常(延迟高、QPS低、内存暴涨),需按“排查→定位→优化→验证”的流程处理,面试需熟练掌握,体现生产经验:
-
第一步:排查异常现象:通过监控工具(Prometheus+Grafana)、原生命令,确认异常指标(如延迟超过10ms、QPS下降、内存使用率超过90%、出现慢命令);
-
第二步:定位异常原因: 延迟高:查看慢命令日志(slowlog get),排查是否有慢命令;查看内存碎片率,排查是否碎片严重;查看网络状态,排查是否网络延迟;
-
QPS低:查看客户端连接数,排查是否连接过多;查看IO状态,排查是否持久化IO阻塞;查看是否有热点key并发竞争;
-
内存暴涨:查看大key(redis-cli --bigkeys),排查是否有大key未拆分;查看数据编码,排查是否编码切换导致内存增加;查看过期键,排查是否有大量无过期时间的键。
-
第三步:执行优化方案:根据异常原因,执行对应优化(如拆分大key、禁用慢命令、开启IO多线程、优化持久化配置);
-
第四步:验证优化效果:监控优化后的指标(延迟、QPS、内存使用率),确认异常是否解决;若未解决,重复排查流程,调整优化方案。
7. 面试高频追问(必背)
-
问:Redis单线程模型为什么性能还能这么高?(答案:1. 内存操作:所有命令都是内存操作,无磁盘IO开销;2. 单线程避免上下文切换:无需线程切换和锁竞争,减少性能损耗;3. IO多路复用:Redis使用epoll/kqueue等IO多路复用机制,同时处理多个客户端连接,提升并发能力;4. 高效编码:数据结构编码优化(如ziplist、intset),减少内存占用和命令执行时间);
-
问:Redis 6.0的多线程和单线程的区别?为什么不采用多线程执行命令?(答案:1. 区别:多线程仅处理网络IO(接收请求、返回结果),核心命令执行仍为单线程;2. 不采用多线程执行命令的原因:避免多线程锁竞争,简化代码实现,同时Redis命令执行速度极快(内存操作),单线程足够支撑高并发,多线程反而会增加上下文切换开销);
-
问:如何排查Redis的慢命令?怎么优化?(答案:1. 排查:开启慢命令日志(配置slowlog-log-slower-than=1000,slowlog-max-len=1000),用slowlog get命令查看慢命令;2. 优化:禁用keys、hgetall等慢命令,用scan、hscan替代;将复杂排序、聚合操作移至应用层;优化大key,减少命令执行时间);
-
问:Redis内存碎片率过高怎么办?(答案:1. 开启自动内存整理(activedefrag yes);2. 手动重启Redis(提前备份数据);3. 优化数据操作,避免频繁删除大key,减少碎片产生;4. 合理选择数据结构,避免编码切换);
-
问:热点key的危害及优化方案?(答案:1. 危害:大量请求集中在单Redis节点,导致该节点CPU、内存、网络压力过大,甚至宕机,影响整个服务;2. 优化方案:本地缓存+Redis缓存(多级缓存)、热点key分片、延迟更新热点key、避免高频写入热点key)。
总结:Redis性能优化的核心逻辑是“先定位瓶颈,再针对性优化”,优先解决“单线程阻塞、大key、IO开销、内存浪费”四大问题,结合部署优化和监控告警,确保Redis高性能、高可用运行,面试中需结合生产踩坑点,体现落地能力。

600

被折叠的 条评论
为什么被折叠?



