以下是100道对标阿里P6级别的Redis面试题,覆盖底层原理、数据结构、持久化、高可用、分布式、性能优化、生产运维七大维度。P6级别要求不仅能"用",更要"懂原理、能排查、会设计"。
一、底层原理与线程模型(12题)
- Redis为什么这么快?核心原因有哪些?
1.纯内存操作,数据全在内存读写耗时少
2.单线程+网络IO多路复用,避免多线程上下文切换,cpu缓存失效和锁竞争的开销,
3.高效的数据结构,sds(简单动态字符串)预分配内存减少重分配,跳表(skiplist)实现快速范围查找,压缩列表(ziplist)节约内存, - Redis为什么最初被设计成单线程的?
1.redis核心瓶颈在于内存带宽和网络延迟,不在于核心数,单核处理内存速度远快于网络发包速度,
2.redis内部数据结构(hash,跳表)极其复杂,引入多线程之后并发控制反而性能下降,代码维护难度高
3.多线程切换时要保存上下文,寄存器,程序计数器,还会污染cpu缓存,代价大 - Redis 6.0为什么引入多线程?多线程只负责什么?
1.负责网络层IO处理,随着带宽增大,网络层io处理开始占用cpu比重变大,需要增加对应能力
2.网络层的io多路复用只负责Socket的读写(i/o)和协议解析 - Redis的单线程模型具体是怎样的?IO多路复用如何工作?
概括为一个循环三个阶段,
1.监听注册,主线程将多个客户端socket fd注册道epoll(Linux内核事件表)上
2.等待事件,调用epoll_wait 进入阻塞/休眠状态,内核检查道某个fd可读写/可写时唤醒主线程,
3.分发通知,主线程将就绪的事件放入队列,按顺序取出命令执行,执行完成后将响应数据写入输出缓冲区,再注册写事件,由内核将数据发回客户端 - Redis和Memcached有什么区别?
1.redis数据结构丰富,它只支持k/v(string)结构
2.redis支持rdb+aof,可持久化,它仅为内存操作
3.redis支持集群,主从,哨兵,分片,它无原生集群,需要客户端一致性哈希实现 - 一条Redis命令从客户端到服务端执行的完整流程是怎样的?
1.客户端发送,客户端将命令(如set key val)编码为resp协议数组,通过socket发送到网卡
2.内核接收,数据进入linux讷河的socket接收缓冲区
3.事件触发,epoll检测道该fd可读,触发epollin事件,唤醒redis主线程
4.读取解析,主线程调用read()函数将数据从内核缓冲区读取到用户态内存。并根据resp协议解析出命令参数
5.执行命令,主线程查找命令表,调用对应的处理函数执行命令,操作对应的数据结构
6.返回响应,将执行结果编码为resp格式下入输出缓冲区,主线程注册epollout事件,内核异步将数据从内核缓冲区通过网卡发辉和客户端 - Redis的通信协议是什么?RESP协议的格式了解吗?
1.resp(redis serialization protocol)协议,序列化协议 - Redis的IO线程模型在6.0前后有什么变化?
1.在6.0之前主线程包揽全部活,6.0之后包含6.0将已建立连接的read任务分发给io线程池,并行处理read和resp协议解析,解析完之后,再放回队列交给主线程执行 - Redis为什么使用单核就能支撑高并发?
1.redis的qps瓶颈在于网络收发包速度和内存访问延迟,cpu操作速度远高于这两部分 - Redis的epoll模型是如何实现的?
1.主要依赖于内核事件表,将客户端socket fd和关注的事件epollin,epollout等事件注册入内核事件表,配合内核事件监听,将对应事件投入到对应的缓冲区,没有事件就休眠,只需关注对应事件触发就行了 - Redis如何处理慢查询?慢查询日志怎么配置和分析?
1.redis维护了一个固定长度的先入先出队列,专门存储超过预设阈值的命令执行记录,
2.配置在redis.conf文件中配置slowlog-log-slower- than 10000(默认10ms,单位微秒,设为0表示关闭),slowlog-max-len 128(最多存128条,超过淘汰最早的)使用slowlog get 10 查看最近10条,输出 唯一id,执行时间戳,耗时,命令参数,客户端ip,
3.举例,线上qps下降,执行slowlog get 发现大量keys *(O(N)扫库操作)或者HGETALL巨大hash,定位后紧急改为scan或者hscan分配获取 - Redis的CPU亲和性绑定有什么作用?
1.将redsi主线程绑到指定cpu核心上执行,避免进程在多个cpu上漂移
二、数据结构与底层实现(14题)
- Redis支持哪些数据类型?分别用在什么场景?
回答:
一共 9种(核心5种 + 高级4种):
- String(字符串):最基础,场景:缓存对象(JSON序列化)、分布式锁、计数器(文章阅读量)、分布式ID生成(INCR)。
- Hash(哈希):场景:存储对象属性(如用户信息:hset user:1001 name jack age 20),适合频繁修改对象某个字段,省内存。
- List(列表):场景:最新消息列表(微博时间线)、轻量级阻塞队列(LPUSH + BRPOP)。
- Set(集合):场景:去重(抽奖活动唯一用户)、交集运算(共同好友)、随机抽取(SRANDMEMBER)。
- ZSet(有序集合):场景:排行榜(游戏积分)、延时队列(按时间戳排序)、带权重的任务调度。
- Bitmap(位图):场景:用户签到(365天只要365bit)、在线状态(1/0)。
- HyperLogLog:场景:UV统计(去重计数),误差率约0.81%,省内存极大(12KB可统计2^64)。
- Geo(地理位置):场景:附近的人、打车距离计算。
- Stream(流):场景:消息队列(替代Kafka做轻量级发布订阅,支持消费者组和ACK)。
- Redis的String底层是用什么实现的?SDS和C字符串有什么区别?
回答:
底层是 SDS(Simple Dynamic String,简单动态字符串)。
结构(以Redis 3.2+为例,分为sdshdr5/8/16/32/64):
c
struct sdshdr8 {
uint8_t len; // 已使用长度
uint8_t alloc; // 分配总长度(不含头部和空终止符)
unsigned char flags; // 类型标识
char buf[]; // 字节数组
};
与C字符串的3大区别:
- O(1)获取长度:C要遍历到\0,SDS直接读len。
- 二进制安全(Binary Safe):C以\0结尾无法存图片/视频,SDS以len判断结尾,可存任意二进制数据。
- 杜绝缓冲区溢出:C修改字符串可能溢出,SDS在修改前会检查alloc并自动扩容(空间预分配:小于1MB时翻倍,大于1MB时每次多分配1MB;惰性释放:缩短时不立即归还内存,而是修改len留待后用)。
- Redis的Hash底层在什么情况下用ziplist?什么情况用hashtable?
回答:
满足以下两个条件同时成立时用 ziplist(压缩列表),否则升级为 hashtable(字典):
· 键值对总数 < hash-max-ziplist-entries(默认 512)。
· 所有键和值的字符串长度 < hash-max-ziplist-value(默认 64字节)。
场景例:存储用户信息(字段固定且值短)用ziplist极省内存;若用户有上万标签(大哈希)或存了长文本,自动转hashtable以换取O(1)读写速度。
- Redis的List底层:quicklist是什么?为什么取代了ziplist+linkedlist?
回答:
quicklist(快速列表) 是 双向链表 + 每个节点内嵌ziplist 的混合体。
为什么取代:
· 旧版(❤️.2)用linkedlist(双向链表):每个元素要分配独立节点,内存碎片多,指针开销大(前后指针占16字节)。
· 旧版用ziplist(压缩列表):虽然内存紧凑,但增删元素会引起连锁更新(后续元素频繁移动),写性能差。
· quicklist权衡:将链表切成多段,每段用ziplist存储,既减少了内存碎片,又把连锁更新的影响范围控制在一个节点内。配置list-max-ziplist-size可控制每个ziplist的最大大小(默认-2,约8KB)。
- Redis的Set底层:intset和hashtable的转换条件是什么?
回答:
当Set存储的全是整数且元素数量 < set-max-intset-entries(默认 512)时,底层用 intset(整数集合),内存极紧凑(按值升序存储)。
一旦元素数量超过512,或者插入了非整数(字符串),立即转为 hashtable(字典),key存元素,value存NULL。
注意:转换是单向不可逆的,即使后来删除到512以下,也不会转回intset(防止频繁转换抖动)。
- Redis的ZSet底层:什么时候用ziplist?什么时候用skiplist+dict?
回答:
满足以下两个条件同时成立时用 ziplist:
· 元素数量 < zset-max-ziplist-entries(默认 128)。
· 成员(member)长度 < zset-max-ziplist-value(默认 64字节)。
此时ziplist内按分数(score)从小到大排序。
一旦超过阈值,转为 skiplist(跳表)+ dict(字典) 双结构:
· dict(哈希表):存储 member -> score,用于O(1)查分(ZSCORE)。
· skiplist(跳表):按score排序存储member,支持O(logN)范围查询(ZRANGE、ZRANK)。
- 跳表(skiplist)的数据结构是怎样的?为什么ZSet用跳表不用红黑树?
回答:
跳表结构:是一种多层有序链表。最底层(L0)包含所有元素,每向上一层,元素数量随机减半(概率1/4)。查找时从最高层开始,逐层跳跃式前进,类似"二分查找"。
为什么不用红黑树(4大理由):
- 实现极简:跳表增删改查代码量约为红黑树的1/3,不易出Bug。
- 范围查询更友好:红黑树找范围(ZRANGE)要中序遍历,跳表找到起点后,直接沿L0遍历即可,效率极高。
- 多维度查询:ZSet同时需要按member查score(交给dict)和按score查member(交给跳表),红黑树无法同时高效支撑这种多维需求。
- 并发友好(虽Redis单线程):跳表插入只影响局部节点,锁粒度更细(虽Redis不用锁)。
- Redis的跳表实现中,层数是如何决定的?最大层数是多少?
回答:
层数生成:采用幂律分布(Power-law),随机函数决定。
· 每次插入新节点时,随机生成一个层高值。
· 核心算法:初始层高为1,每循环一次有 25%(1/4) 的概率层数+1,直到达到上限或概率终止。
最大层数:Redis 5.0之前为 32,5.0之后(含)为 64(ZSKIPLIST_MAXLEVEL)。
为什么设上限:2^64 数据量已远超地球上的Key数量,绰绰有余,且限制层数能节省内存并控制跳跃步长。
- Redis的字典(dict)是如何实现的?渐进式rehash是什么?
回答:
实现:dictht(哈希表)+ dictEntry(链表数组),采用链地址法解决哈希冲突。使用MurmurHash2算法。
每个dict包含两个哈希表(ht0和ht1)。
渐进式rehash:当哈希表负载因子过高(used/buckets > 1且无法扩容,或 >5强制扩容)时,数据需要从ht0迁移到更大的ht1。为了不阻塞主线程,Redis采用"分而治之":
· 将迁移工作分摊到每次增删改查操作中,每次迁移 1个bucket(索引rehashidx指向的那个桶)里的所有节点。
· 同时,定时任务(serverCron)也会帮忙迁移 100个bucket,确保最终迁移完成。
- 渐进式rehash过程中,增删改查操作如何处理?
回答:
· 查(Get):先查 ht0,如果没找到,再查 ht1(因为部分数据已搬走)。
· 改(Update):先查 ht0,再查 ht1,找到后修改。
· 删(Delete):先查 ht0,再查 ht1,找到并删除。
· 增(Add):新键一律只写入 ht1,ht0只减不增,保证ht0的key最终会被搬空。
rehashidx 从0开始递增,当ht0所有bucket搬完,交换ht0和ht1,重置ht1并置rehashidx = -1。
- Redis的整数集合(intset)升级机制是什么?能降级吗?
回答:
升级机制:intset底层存储按整数类型(int16_t / int32_t / int64_t)中最小的来存。当插入一个超出当前类型范围的大整数时,intset会将底层数组全部升级(如从int16统一转为int32),并重新排列所有元素的位置。
不能降级:即使升级后删除了那个大整数,编码格式也不会回退。这是为了防止频繁抖动(若反复升降级,性能灾难)。
- Redis的压缩列表(ziplist)结构是怎样的?连锁更新问题是什么?
回答:
内存结构(连续内存块):
zlbytes(总长度) + zltail(尾偏移) + zllen(元素数) + 多个entry + zlend(结束符0xFF)。
每个entry内部编码包括:prevrawlen(前一个元素长度,1或5字节) + encoding + data。
连锁更新问题(Cascade Update):
当在中间插入一个大元素(>254字节)时,该元素的后一个元素的prevrawlen字段需要从1字节变为5字节存储长度,这导致该entry长度变长。如果变长导致该entry也超过254字节,又会引起下一个entry的prevrawlen扩张......引发连续连锁反应,最坏时间复杂度 O(N^2)。这就是Redis 7.0要用listpack替代它的根源。
- Redis 7.0用listpack替代ziplist,解决了什么问题?
回答:
核心解决:彻底根除连锁更新(Cascade Update)问题。
原理变化:listpack放弃了每个entry存储前一个元素的长度(prevlen)的依赖关系。每个entry只存储自身长度(编码在encoding字段中),不依赖前后节点。
这样插入或删除元素时,只影响当前entry的编码空间,绝不传染给后续元素,将最坏O(N^2)降为O(N),大大提升了Hash/List/ZSet在数据量少时的写入性能。
- Redis的Stream底层实现是什么?和List有什么区别?
回答:
底层实现:Radix Tree(基数树)+ listpack。
· 消息ID(时间戳+序号)作为key,指向一个listpack(存储该ID下的具体KV数据)。
· 同时支持消费者组(Consumer Group)、Pending队列(未ACK消息)和CLAIM(转移超时消息)。
与List(作为消息队列)的5大核心区别:
- 消费模式:List是pop即删除(单次消费),Stream支持多个消费者组,每条消息可被多个组重复消费(类似Kafka)。
- ACK机制:List无ACK,消费者挂了消息即丢失;Stream有XACK,保证消息被可靠处理。
- 消息持久化:Stream是Append-Only,消息持久保存在内存;Listpop后彻底消失。
- 回溯:List pop后无法回溯;Stream支持XRANGE按时间戳范围回溯任意历史消息。
- 阻塞读:都有阻塞读,但Stream支持XREADGROUP实现多消费者负载均衡。
三、持久化机制(12题)
- Redis的持久化方式有哪几种?各自的优缺点是什么?
- RDB的触发方式有哪些?SAVE和BGSAVE有什么区别?
- BGSAVE的fork子进程过程是怎样的?Copy-On-Write机制是什么?
- fork子进程时,如果父进程有大量写入,会发生什么?
- AOF的三种刷盘策略(appendfsync)是什么?怎么选?
- AOF重写(rewrite)的原理是什么?
- AOF重写期间,新的写入命令如何处理?
- RDB和AOF混合持久化(Redis 4.0+)是怎么玩的?
- 如果Redis宕机,混合持久化下会丢失多少数据?
- 如果RDB和AOF同时开启,Redis重启时优先加载哪个?
- 主从复制中,持久化对数据安全有什么影响?
- 大内存实例(如50GB)做BGSAVE会有什么问题?如何优化?
四、过期策略与内存淘汰(10题)
- Redis的过期键删除策略是什么?
- 惰性删除和定期删除分别怎么工作?为什么不用定时删除?
- 定期删除的默认执行频率是多少?怎么调整?
- Redis 4.0+提供了哪8种内存淘汰策略?
- LRU和LFU有什么区别?Redis的LRU是严格LRU吗?
- 生产环境怎么选择淘汰策略?
- Redis内存用完了会怎样?
- maxmemory设置多少合适?设置太小或太大会有什么问题?
- 如何监控Redis的内存使用情况?
- 32位Redis和64位Redis在内存占用上有什么区别?
五、主从复制与哨兵(10题)
- Redis主从复制的原理是什么?
- 全量同步和增量同步分别在什么情况下触发?
- 全量同步的完整流程是怎样的?RDB文件如何传输?
- 复制积压缓冲区(repl_backlog)的作用是什么?
- 主从复制断线后如何恢复?增量同步的条件是什么?
- 哨兵(Sentinel)模式的工作原理是什么?
- 哨兵集群为什么至少需要3个实例?
- 哨兵的故障转移流程是怎样的?主观下线和客观下线有什么区别?
- 哨兵模式下,主从切换期间服务是否可用?
- 什么是脑裂(split-brain)?哨兵模式下如何应对?
六、Redis Cluster集群(12题)
- Redis Cluster的数据分片原理是什么?
- 为什么Redis Cluster有16384个哈希槽而不是65536个?
- 哈希槽分配算法是怎样的?CRC16(key) % 16384
- 客户端如何知道key应该访问哪个节点?MOVED和ASK重定向的区别?
- Redis Cluster的节点间通信机制(Gossip协议)是怎样的?
- Cluster模式下,故障检测和故障转移是如何进行的?
- Cluster扩容时,槽和数据如何迁移?
- 扩容迁移期间,客户端请求如何处理?
- Redis Cluster最大支持多少个节点?为什么?
- 一致性哈希和哈希槽有什么区别?
- Redis Cluster支持跨槽的事务吗?为什么?
- Cluster模式下,批量操作(如mset/mget)有什么限制?
七、分布式锁(10题)
- Redis如何实现分布式锁?
- SETNX实现分布式锁有什么问题?
- 分布式锁的正确实现方式是什么?
- 为什么要用唯一标识(UUID)作为锁的值?
- 释放锁为什么必须用Lua脚本?
- 锁过期了但业务还没执行完怎么办?锁续期如何实现?
- Redisson的可重入锁是如何实现的?
- Redlock红锁算法是什么?解决了什么问题?
- Redlock有什么争议和缺陷?
- 分布式锁、数据库乐观锁、ZooKeeper锁怎么选型?
八、缓存三大问题(10题)
- 缓存穿透是什么?怎么解决?
- 布隆过滤器(Bloom Filter)的原理是什么?优缺点?
- 缓存击穿是什么?怎么解决?
- 缓存雪崩是什么?怎么解决?
- 热点Key怎么发现?怎么解决?
- 大Key(BigKey)怎么定义?有什么危害?
- 大Key如何检测?如何在线安全删除?
- 热Key重建缓存时,如何避免大量请求打到DB?
- 缓存预热怎么做?
- 如何保证本地缓存和Redis缓存的一致性?
九、缓存一致性(8题)
- 先更新数据库还是先删除缓存?为什么?
- 先删缓存后更新DB vs 先更新DB后删缓存,各有什么问题?
- 延时双删策略是什么?能保证强一致吗?
- 缓存与数据库的最终一致性如何保证?
- Canal等中间件如何实现缓存同步?
- 读写分离场景下,缓存一致性问题怎么处理?
- 强一致性和高可用在缓存场景如何权衡?
- 分布式事务(TCC/Saga)和缓存一致性如何结合?
十、性能优化与生产运维(12题)
- Redis常见的性能问题有哪些?
回答:
我将常见的性能问题归类为 6大类:
- 慢查询阻塞(CPU/命令):执行O(N)命令如 KEYS *、HGETALL 大Hash、ZRANGE(0,-1) 大ZSet,阻塞主线程。
- 网络带宽打满:频繁获取大Key(如10MB的JSON),或QPS过高(> 10万/秒),网卡成为瓶颈。
- 持久化引发的抖动(磁盘/内存):BGSAVE 或 AOF rewrite 触发的 fork 阻塞(COW内存翻倍),或磁盘IO能力不足导致AOF刷盘延迟(appendfsync everysec 卡顿)。
- 内存碎片与SWAP(操作系统):内存碎片率过高(mem_fragmentation_ratio > 1.5),或物理内存不足触发SWAP(将Redis内存页换出到磁盘),导致读写延迟从微秒级暴增到毫秒/秒级。
- 连接数耗尽:客户端连接池配置不当,或未正确释放连接,导致 maxclients 达到上限,新连接被拒绝。
- 过期淘汰策略抢占CPU:内存达到 maxmemory,频繁触发LRU/LFU淘汰算法,大量消耗CPU时间片。
- Redis突然变慢了,如何排查?
回答:
这是P6故障排查实操题,必须给出系统化的方法论。我按 "由内到外、由表及里" 的5步排查法:
第一步:看慢查询日志
shell
SLOWLOG GET 20
看是否有 KEYS *、HGETALL、SMEMBERS 等O(N)命令,或耗时超过10ms的异常命令。如果有,直接优化对应业务代码。
第二步:查看延迟诊断(Redis内置神器)
shell
LATENCY DOCTOR
LATENCY GRAPH command # 查看具体命令的延迟波动
Redis 4.0+ 自带延迟诊断器,会直接告诉你问题根源(如fork耗时、AOF刷盘延迟、网络抖动等)。
第三步:检查系统资源(操作系统层面)
· 内存SWAP:INFO memory 看 mem_fragmentation_ratio,如果 used_memory_rss < used_memory(比值<1),说明发生了SWAP,立即排查物理内存是否不足。
· CPU:top 看Redis进程CPU是否飙高(正常应<30%),vmstat 看上下文切换是否过高。
· 网络:iftop 或 sar -n DEV 看网卡流量是否打满。
· 磁盘IO:iostat -x 1 看磁盘 %util 是否接近100%(影响AOF刷盘)。
第四步:检查fork阻塞(持久化相关)
shell
INFO stats | grep fork
看 latest_fork_usec(最近一次fork耗时),如果超过1秒(1000000微秒),说明大实例fork阻塞了主线程,需要优化持久化策略(如只在从库执行BGSAVE)。
第五步:检查内存淘汰
INFO evicted_keys 如果持续快速增长,说明内存淘汰频繁,需评估是否扩容或调整 maxmemory-policy。
终极工具:使用 Redis Insight 或 Grafana + Prometheus 看综合大盘,定位时间轴上的抖动点。
- KEYS命令有什么问题?生产环境为什么不能用?
回答:
问题:KEYS pattern 命令会全量扫描整个数据库的Key空间,时间复杂度 O(N),N为数据库中的Key总数。
生产危害:
· 如果实例有1亿个Key,KEYS * 可能会执行 几秒甚至几十秒。
· Redis是单线程,执行期间完全阻塞所有其他请求,导致整个服务不可用(P99延迟飙升到秒级,引发上游超时雪崩)。
生产替代方案:
· SCAN 命令(游标迭代器):分批渐进式遍历,不阻塞主线程,每次返回少量Key。
· 如果确实需要全量导出:用 redis-cli --scan 命令(底层也是SCAN),或在从库上执行 KEYS(避免影响主库)。
记忆口诀:线上绝对禁止KEYS,见到一次扣一次绩效。
- SCAN命令如何使用?和KEYS有什么区别?
回答:
用法:
shell
SCAN cursor [MATCH pattern] [COUNT count]
· cursor:游标,从0开始,返回的游标为0时表示遍历结束。
· MATCH:模糊匹配模式(如 user:*)。
· COUNT:每次建议返回的数量(不是精确返回数,而是对底层bucket的扫描提示,默认10)。
与KEYS的3大核心区别:
- 阻塞性:KEYS 一次性全量阻塞;SCAN 每次只返回少量数据(默认约10~20条),不阻塞主线程。
- 数据一致性:KEYS 返回的是遍历瞬间的一致性快照;SCAN 在遍历过程中,若Key被修改(增删),可能返回重复Key,也可能遗漏Key(需要业务方接受或二次去重)。
- 返回结果:KEYS 直接返回Key列表;SCAN 返回 cursor, \[key1, key2...],需客户端迭代处理。
场景举例:需要找出所有 session:* 的Key并批量删除。用 SCAN 0 MATCH session:* COUNT 100 循环迭代,每次对返回的Key执行 UNLINK(异步删除),对线上零影响。
- Pipeline的作用是什么?和事务有什么区别?
回答:
Pipeline(管道)作用:将多条命令打包,一次性发送给Redis服务端,服务端按顺序执行后,一次性返回所有结果。
核心目的:减少网络往返时间(RTT,Round Trip Time)。例如执行100条命令,非Pipeline需要100次网络往返(100ms);Pipeline只需1次(1ms),极大提升吞吐量。
与事务(MULTI/EXEC)的区别(4点):
维度 Pipeline 事务(MULTI/EXEC)
原子性 不保证原子性,中间某条命令失败不影响其他命令执行 保证原子性(EXEC时批量执行,但Redis事务不支持回滚)
隔离性 命令之间可能插入其他客户端的命令 保证隔离性(EXEC期间独占主线程,不会被其他客户端打断)
目的 性能优化(减少网络IO) 原子性操作(保证多条命令连续执行)
依赖关系 命令间独立,不能依赖前一条命令的结果 命令间独立(WATCH例外,用于乐观锁)
场景选型:批量导入数据(追求极致性能)用 Pipeline;扣减库存需要原子性(检查库存>0再扣减)用 Lua脚本 或 事务+WATCH。
- Redis事务(MULTI/EXEC)支持ACID吗?
回答:
不支持完整的ACID,存在严重妥协(尤其是A原子性和D持久性)。
· A(原子性,Atomicity):部分支持,有重大缺陷。
· 若命令语法错误(如 SET key value 拼写错误),EXEC 会直接失败,所有命令都不执行,这符合原子性。
· 但若运行时错误(如对String执行 LPUSH),Redis会继续执行后续命令,且不回滚已执行的命令!这与传统关系型数据库的"全部成功或全部失败"的原子性定义相悖。
· C(一致性,Consistency):支持。单线程串行执行,不会破坏数据结构完整性。
· I(隔离性,Isolation):支持。Redis单线程,EXEC 执行期间不会被其他客户端命令打断,隔离性最高。
· D(持久性,Durability):仅当配置了 appendfsync always 时才支持。默认 everysec 下,宕机可能丢1秒数据,不满足D。
P6关键认知:正因事务的原子性缺陷(运行时错误不回滚),生产环境通常用Lua脚本替代事务,Lua脚本在遇到运行时错误时,Redis会放弃后续命令执行(但也不会回滚已执行的,不过在逻辑上更可控)。
- Lua脚本和事务有什么区别?Lua脚本能保证原子性吗?
回答:
区别(3个核心维度):
- 执行方式:Lua脚本整个作为一个命令发送,一次性执行;事务通过 MULTI 排队,EXEC 触发执行。
- 网络交互:Lua脚本1次RTT(发一次脚本内容);事务至少2次RTT(MULTI + 命令排队 + EXEC)。
- 逻辑复杂度:Lua支持条件判断、循环、变量,可实现复杂业务逻辑(如"先Get判断再Set");事务只是简单命令列表,无逻辑能力。
Lua脚本能保证原子性吗?------能(但要注意坑)。
· Redis执行Lua脚本时,整个脚本会被当成一个原子操作,脚本执行期间不会执行其他任何命令,保证了隔离性和部分原子性。
· 但是(重要):如果脚本中途执行到一半发生运行时错误(如对String执行 lpop),Redis会返回错误,但脚本中已执行成功的命令不会回滚!这与事务的运行时错误行为一致。
· 结论:Lua脚本只保证"执行期间不被打断",不保证"全成功或全失败"的回滚语义。要保证业务原子性,必须在脚本逻辑里做判断(如先检查类型,再操作)。
- 如何设计秒杀系统的库存扣减?
回答:
秒杀场景要求高并发下不超卖,且性能极致。我给出3层架构设计:
第一层:Lua脚本原子扣减(最核心)
在Redis中存储库存(如 stock:product_1001),使用Lua脚本一次性完成 "检查库存 > 0" + "扣减库存" 两个操作,保证原子性,避免并发超卖。
lua
-- Lua脚本
local stock = redis.call('get', KEYS[1])
if stock and tonumber(stock) > 0 then
redis.call('decr', KEYS[1])
return 1 -- 扣减成功
else
return 0 -- 库存不足
end
第二层:MQ异步落库(削峰填谷)
扣减Redis成功后,将"下单成功"的消息发送到 RocketMQ/RabbitMQ,消费者异步去扣减MySQL真实库存、生成订单。这样DB承受的流量从百万级削到可控的万级。
第三层:防重入与限流(上层防护)
· 限流:在API网关层使用令牌桶/漏桶限流(如只允许前1万人进入),拒绝超出的请求,保护后端。
· 防重入:用户成功扣减后,在Redis中记录 user:product:flag = 1,防止同一个用户重复秒杀。
热点Key优化(P6加分项):
· 如果秒杀商品是超火爆单品(如iPhone),单Key QPS可能突破10万,造成Redis单节点瓶颈。
· 解决方案:将库存分片存储,如 stock:product:1 ~ stock:product:10(10个分片)。用户请求随机路由到某个分片扣减,分散热点压力。但需注意:若某分片为0,需尝试其他分片(逻辑复杂),或使用本地预扣(应用层先扣本地内存,再异步同步到Redis)。
- 亿级数据的BigKey如何在线拆分优化?
回答:
场景:一个Hash大Key存储了1亿个用户标签(user_tags),内存占用10GB,导致 HGETALL 慢查询和网络带宽打满。
在线拆分方案(4步,零停机):
- 新Key设计(分桶):将大Hash拆分为 N个小Hash(如N=1000)。路由规则:new_key = "user_tags:" + (hash_tag(user_id) % 1000)。每个小Hash存储约10万用户。
- 双写过渡期:修改业务代码,同时写入旧Key和新Key(写两个地方),但读请求优先读新Key(若新Key无数据则降级读旧Key)。
- 后台数据迁移:编写脚本,使用 HSCAN 分批扫描旧Key,将数据按路由规则写入新Key,每次迁移1000条,使用 Pipeline 批量写入,控制速率防止拖垮Redis。
- 切流与清理:迁移完成后,业务代码完全切换到新Key,下线读旧Key逻辑。确认无误后,在低峰期对旧Key执行 UNLINK(异步删除,不阻塞主线程)。
注意事项:
· 分桶数(N)选择:预估单Key最大不超过1GB(建议500MB以内),N需结合未来增量规划。
· 若业务无法一次性改造路由逻辑,可使用 Proxy层(如Twemproxy)做映射,对业务透明。
- 1~2亿条数据需要缓存,如何设计存储方案?
回答:
这是一道容量规划 + 架构设计综合题。先做内存估算:
· 假设每条数据Key平均20字节、Value平均100字节,Redis内部对象开销约50字节/Key。
· 单条总开销 ≈ 20 + 100 + 50 = 170字节。
· 2亿条 × 170字节 ≈ 34GB 内存。
设计方案(按优先级推荐):
- 数据结构优化(首选):如果数据格式是 field-value 结构(如用户ID -> 用户信息),使用 Hash分桶(如 hash_tag = user_id % 10000),将2亿条打散到1万个Hash中。Hash在元素较少时使用ziplist编码,内存比普通String节省 50%以上。实测34GB可压缩到15GB左右。
- 实例分片(Cluster):若单实例内存超过物理机的60%(如64GB物理机只能给40GB),必须使用 Redis Cluster,将数据分片到多个节点(如6个节点,每节点约6GB)。
- 冷热分离:分析访问日志,识别出热数据(如最近7天活跃数据)和冷数据。热数据存Redis,冷数据存MySQL或对象存储(OSS),降低Redis内存成本。
- 压缩Value:如果Value是JSON,使用 MessagePack 或 Protobuf 序列化,比JSON节省30%~50%空间。
兜底方案:如果预算有限,开启Redis的 maxmemory + allkeys-lru 淘汰策略,让Redis自动淘汰冷数据,保证热数据常驻,命中率维持在95%以上。
- Redis的客户端连接池如何配置?连接池耗尽怎么办?
回答:
配置参数(以JedisPool为例):
· maxTotal(最大连接数):建议设置为 50~200(取决于业务并发量)。不是越大越好,过大会占用Redis服务端连接数(maxclients默认10000)。
· maxIdle(最大空闲连接):设为 maxTotal 的60%~80%,减少频繁创建销毁开销。
· minIdle(最小空闲连接):建议 5~10,保持预热。
· blockWhenExhausted(耗尽时是否阻塞):设为 true,配合 maxWaitMillis(建议 500ms ~ 1000ms)抛出异常,避免无限等待。
· testOnBorrow:不建议开启(每次借出都ping,增加开销),使用 testWhileIdle 配合空闲检测。
连接池耗尽怎么办(3步应急预案):
- 快速排查根因:通过 netstat -an | grep ESTABLISHED | wc -l 看当前连接数,若远超预期,检查业务代码是否有连接未归还(没执行 close())或异常中断导致连接泄漏。
- 紧急扩容:先临时调大 maxTotal(如从100调到500),并重启应用,保证服务恢复。
- 优化调用:如果并发确实很高(如QPS 5万),连接数不够用,改用 Pipeline 或 Lua脚本 批量操作,减少命令执行次数,从而降低对连接数的占用。
- 服务端调优:同时检查Redis服务端 maxclients 配置,如果客户端连接数逼近上限(INFO clients 看 connected_clients),需在Redis端调大 maxclients 或重启释放僵尸连接。
- 如何对Redis做监控告警?需要监控哪些指标?
回答:
监控方案:Prometheus + redis_exporter + Grafana(开源标准),或云服务商自带的监控(如阿里云Redis监控)。
核心监控指标(6大类,P6必背):
- 性能指标(延迟):监控 P99延迟(latency),超过5ms告警。关注 cmdstat_get、cmdstat