Redis一百道核心面试题

以下是100道对标阿里P6级别的Redis面试题,覆盖底层原理、数据结构、持久化、高可用、分布式、性能优化、生产运维七大维度。P6级别要求不仅能"用",更要"懂原理、能排查、会设计"。


一、底层原理与线程模型(12题)

  1. Redis为什么这么快?核心原因有哪些?
    1.纯内存操作,数据全在内存读写耗时少
    2.单线程+网络IO多路复用,避免多线程上下文切换,cpu缓存失效和锁竞争的开销,
    3.高效的数据结构,sds(简单动态字符串)预分配内存减少重分配,跳表(skiplist)实现快速范围查找,压缩列表(ziplist)节约内存,
  2. Redis为什么最初被设计成单线程的?
    1.redis核心瓶颈在于内存带宽和网络延迟,不在于核心数,单核处理内存速度远快于网络发包速度,
    2.redis内部数据结构(hash,跳表)极其复杂,引入多线程之后并发控制反而性能下降,代码维护难度高
    3.多线程切换时要保存上下文,寄存器,程序计数器,还会污染cpu缓存,代价大
  3. Redis 6.0为什么引入多线程?多线程只负责什么?
    1.负责网络层IO处理,随着带宽增大,网络层io处理开始占用cpu比重变大,需要增加对应能力
    2.网络层的io多路复用只负责Socket的读写(i/o)和协议解析
  4. Redis的单线程模型具体是怎样的?IO多路复用如何工作?
    概括为一个循环三个阶段,
    1.监听注册,主线程将多个客户端socket fd注册道epoll(Linux内核事件表)上
    2.等待事件,调用epoll_wait 进入阻塞/休眠状态,内核检查道某个fd可读写/可写时唤醒主线程,
    3.分发通知,主线程将就绪的事件放入队列,按顺序取出命令执行,执行完成后将响应数据写入输出缓冲区,再注册写事件,由内核将数据发回客户端
  5. Redis和Memcached有什么区别?
    1.redis数据结构丰富,它只支持k/v(string)结构
    2.redis支持rdb+aof,可持久化,它仅为内存操作
    3.redis支持集群,主从,哨兵,分片,它无原生集群,需要客户端一致性哈希实现
  6. 一条Redis命令从客户端到服务端执行的完整流程是怎样的?
    1.客户端发送,客户端将命令(如set key val)编码为resp协议数组,通过socket发送到网卡
    2.内核接收,数据进入linux讷河的socket接收缓冲区
    3.事件触发,epoll检测道该fd可读,触发epollin事件,唤醒redis主线程
    4.读取解析,主线程调用read()函数将数据从内核缓冲区读取到用户态内存。并根据resp协议解析出命令参数
    5.执行命令,主线程查找命令表,调用对应的处理函数执行命令,操作对应的数据结构
    6.返回响应,将执行结果编码为resp格式下入输出缓冲区,主线程注册epollout事件,内核异步将数据从内核缓冲区通过网卡发辉和客户端
  7. Redis的通信协议是什么?RESP协议的格式了解吗?
    1.resp(redis serialization protocol)协议,序列化协议
  8. Redis的IO线程模型在6.0前后有什么变化?
    1.在6.0之前主线程包揽全部活,6.0之后包含6.0将已建立连接的read任务分发给io线程池,并行处理read和resp协议解析,解析完之后,再放回队列交给主线程执行
  9. Redis为什么使用单核就能支撑高并发?
    1.redis的qps瓶颈在于网络收发包速度和内存访问延迟,cpu操作速度远高于这两部分
  10. Redis的epoll模型是如何实现的?
    1.主要依赖于内核事件表,将客户端socket fd和关注的事件epollin,epollout等事件注册入内核事件表,配合内核事件监听,将对应事件投入到对应的缓冲区,没有事件就休眠,只需关注对应事件触发就行了
  11. 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分配获取
  12. Redis的CPU亲和性绑定有什么作用?
    1.将redsi主线程绑到指定cpu核心上执行,避免进程在多个cpu上漂移

二、数据结构与底层实现(14题)

  1. Redis支持哪些数据类型?分别用在什么场景?

回答:

一共 9种(核心5种 + 高级4种):

  1. String(字符串):最基础,场景:缓存对象(JSON序列化)、分布式锁、计数器(文章阅读量)、分布式ID生成(INCR)。
  2. Hash(哈希):场景:存储对象属性(如用户信息:hset user:1001 name jack age 20),适合频繁修改对象某个字段,省内存。
  3. List(列表):场景:最新消息列表(微博时间线)、轻量级阻塞队列(LPUSH + BRPOP)。
  4. Set(集合):场景:去重(抽奖活动唯一用户)、交集运算(共同好友)、随机抽取(SRANDMEMBER)。
  5. ZSet(有序集合):场景:排行榜(游戏积分)、延时队列(按时间戳排序)、带权重的任务调度。
  6. Bitmap(位图):场景:用户签到(365天只要365bit)、在线状态(1/0)。
  7. HyperLogLog:场景:UV统计(去重计数),误差率约0.81%,省内存极大(12KB可统计2^64)。
  8. Geo(地理位置):场景:附近的人、打车距离计算。
  9. Stream(流):场景:消息队列(替代Kafka做轻量级发布订阅,支持消费者组和ACK)。

  1. 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大区别:

  1. O(1)获取长度:C要遍历到\0,SDS直接读len。
  2. 二进制安全(Binary Safe):C以\0结尾无法存图片/视频,SDS以len判断结尾,可存任意二进制数据。
  3. 杜绝缓冲区溢出:C修改字符串可能溢出,SDS在修改前会检查alloc并自动扩容(空间预分配:小于1MB时翻倍,大于1MB时每次多分配1MB;惰性释放:缩短时不立即归还内存,而是修改len留待后用)。

  1. Redis的Hash底层在什么情况下用ziplist?什么情况用hashtable?

回答:

满足以下两个条件同时成立时用 ziplist(压缩列表),否则升级为 hashtable(字典):

· 键值对总数 < hash-max-ziplist-entries(默认 512)。

· 所有键和值的字符串长度 < hash-max-ziplist-value(默认 64字节)。

场景例:存储用户信息(字段固定且值短)用ziplist极省内存;若用户有上万标签(大哈希)或存了长文本,自动转hashtable以换取O(1)读写速度。


  1. Redis的List底层:quicklist是什么?为什么取代了ziplist+linkedlist?

回答:

quicklist(快速列表) 是 双向链表 + 每个节点内嵌ziplist 的混合体。

为什么取代:

· 旧版(❤️.2)用linkedlist(双向链表):每个元素要分配独立节点,内存碎片多,指针开销大(前后指针占16字节)。

· 旧版用ziplist(压缩列表):虽然内存紧凑,但增删元素会引起连锁更新(后续元素频繁移动),写性能差。

· quicklist权衡:将链表切成多段,每段用ziplist存储,既减少了内存碎片,又把连锁更新的影响范围控制在一个节点内。配置list-max-ziplist-size可控制每个ziplist的最大大小(默认-2,约8KB)。


  1. Redis的Set底层:intset和hashtable的转换条件是什么?

回答:

当Set存储的全是整数且元素数量 < set-max-intset-entries(默认 512)时,底层用 intset(整数集合),内存极紧凑(按值升序存储)。

一旦元素数量超过512,或者插入了非整数(字符串),立即转为 hashtable(字典),key存元素,value存NULL。

注意:转换是单向不可逆的,即使后来删除到512以下,也不会转回intset(防止频繁转换抖动)。


  1. 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)。


  1. 跳表(skiplist)的数据结构是怎样的?为什么ZSet用跳表不用红黑树?

回答:

跳表结构:是一种多层有序链表。最底层(L0)包含所有元素,每向上一层,元素数量随机减半(概率1/4)。查找时从最高层开始,逐层跳跃式前进,类似"二分查找"。

为什么不用红黑树(4大理由):

  1. 实现极简:跳表增删改查代码量约为红黑树的1/3,不易出Bug。
  2. 范围查询更友好:红黑树找范围(ZRANGE)要中序遍历,跳表找到起点后,直接沿L0遍历即可,效率极高。
  3. 多维度查询:ZSet同时需要按member查score(交给dict)和按score查member(交给跳表),红黑树无法同时高效支撑这种多维需求。
  4. 并发友好(虽Redis单线程):跳表插入只影响局部节点,锁粒度更细(虽Redis不用锁)。

  1. Redis的跳表实现中,层数是如何决定的?最大层数是多少?

回答:

层数生成:采用幂律分布(Power-law),随机函数决定。

· 每次插入新节点时,随机生成一个层高值。

· 核心算法:初始层高为1,每循环一次有 25%(1/4) 的概率层数+1,直到达到上限或概率终止。

最大层数:Redis 5.0之前为 32,5.0之后(含)为 64(ZSKIPLIST_MAXLEVEL)。

为什么设上限:2^64 数据量已远超地球上的Key数量,绰绰有余,且限制层数能节省内存并控制跳跃步长。


  1. Redis的字典(dict)是如何实现的?渐进式rehash是什么?

回答:

实现:dictht(哈希表)+ dictEntry(链表数组),采用链地址法解决哈希冲突。使用MurmurHash2算法。

每个dict包含两个哈希表(ht0和ht1)。

渐进式rehash:当哈希表负载因子过高(used/buckets > 1且无法扩容,或 >5强制扩容)时,数据需要从ht0迁移到更大的ht1。为了不阻塞主线程,Redis采用"分而治之":

· 将迁移工作分摊到每次增删改查操作中,每次迁移 1个bucket(索引rehashidx指向的那个桶)里的所有节点。

· 同时,定时任务(serverCron)也会帮忙迁移 100个bucket,确保最终迁移完成。


  1. 渐进式rehash过程中,增删改查操作如何处理?

回答:

· 查(Get):先查 ht0,如果没找到,再查 ht1(因为部分数据已搬走)。

· 改(Update):先查 ht0,再查 ht1,找到后修改。

· 删(Delete):先查 ht0,再查 ht1,找到并删除。

· 增(Add):新键一律只写入 ht1,ht0只减不增,保证ht0的key最终会被搬空。

rehashidx 从0开始递增,当ht0所有bucket搬完,交换ht0和ht1,重置ht1并置rehashidx = -1。


  1. Redis的整数集合(intset)升级机制是什么?能降级吗?

回答:

升级机制:intset底层存储按整数类型(int16_t / int32_t / int64_t)中最小的来存。当插入一个超出当前类型范围的大整数时,intset会将底层数组全部升级(如从int16统一转为int32),并重新排列所有元素的位置。

不能降级:即使升级后删除了那个大整数,编码格式也不会回退。这是为了防止频繁抖动(若反复升降级,性能灾难)。


  1. 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替代它的根源。


  1. Redis 7.0用listpack替代ziplist,解决了什么问题?

回答:

核心解决:彻底根除连锁更新(Cascade Update)问题。

原理变化:listpack放弃了每个entry存储前一个元素的长度(prevlen)的依赖关系。每个entry只存储自身长度(编码在encoding字段中),不依赖前后节点。

这样插入或删除元素时,只影响当前entry的编码空间,绝不传染给后续元素,将最坏O(N^2)降为O(N),大大提升了Hash/List/ZSet在数据量少时的写入性能。


  1. Redis的Stream底层实现是什么?和List有什么区别?

回答:

底层实现:Radix Tree(基数树)+ listpack。

· 消息ID(时间戳+序号)作为key,指向一个listpack(存储该ID下的具体KV数据)。

· 同时支持消费者组(Consumer Group)、Pending队列(未ACK消息)和CLAIM(转移超时消息)。

与List(作为消息队列)的5大核心区别:

  1. 消费模式:List是pop即删除(单次消费),Stream支持多个消费者组,每条消息可被多个组重复消费(类似Kafka)。
  2. ACK机制:List无ACK,消费者挂了消息即丢失;Stream有XACK,保证消息被可靠处理。
  3. 消息持久化:Stream是Append-Only,消息持久保存在内存;Listpop后彻底消失。
  4. 回溯:List pop后无法回溯;Stream支持XRANGE按时间戳范围回溯任意历史消息。
  5. 阻塞读:都有阻塞读,但Stream支持XREADGROUP实现多消费者负载均衡。

三、持久化机制(12题)

  1. Redis的持久化方式有哪几种?各自的优缺点是什么?
  2. RDB的触发方式有哪些?SAVE和BGSAVE有什么区别?
  3. BGSAVE的fork子进程过程是怎样的?Copy-On-Write机制是什么?
  4. fork子进程时,如果父进程有大量写入,会发生什么?
  5. AOF的三种刷盘策略(appendfsync)是什么?怎么选?
  6. AOF重写(rewrite)的原理是什么?
  7. AOF重写期间,新的写入命令如何处理?
  8. RDB和AOF混合持久化(Redis 4.0+)是怎么玩的?
  9. 如果Redis宕机,混合持久化下会丢失多少数据?
  10. 如果RDB和AOF同时开启,Redis重启时优先加载哪个?
  11. 主从复制中,持久化对数据安全有什么影响?
  12. 大内存实例(如50GB)做BGSAVE会有什么问题?如何优化?

四、过期策略与内存淘汰(10题)

  1. Redis的过期键删除策略是什么?
  2. 惰性删除和定期删除分别怎么工作?为什么不用定时删除?
  3. 定期删除的默认执行频率是多少?怎么调整?
  4. Redis 4.0+提供了哪8种内存淘汰策略?
  5. LRU和LFU有什么区别?Redis的LRU是严格LRU吗?
  6. 生产环境怎么选择淘汰策略?
  7. Redis内存用完了会怎样?
  8. maxmemory设置多少合适?设置太小或太大会有什么问题?
  9. 如何监控Redis的内存使用情况?
  10. 32位Redis和64位Redis在内存占用上有什么区别?

五、主从复制与哨兵(10题)

  1. Redis主从复制的原理是什么?
  2. 全量同步和增量同步分别在什么情况下触发?
  3. 全量同步的完整流程是怎样的?RDB文件如何传输?
  4. 复制积压缓冲区(repl_backlog)的作用是什么?
  5. 主从复制断线后如何恢复?增量同步的条件是什么?
  6. 哨兵(Sentinel)模式的工作原理是什么?
  7. 哨兵集群为什么至少需要3个实例?
  8. 哨兵的故障转移流程是怎样的?主观下线和客观下线有什么区别?
  9. 哨兵模式下,主从切换期间服务是否可用?
  10. 什么是脑裂(split-brain)?哨兵模式下如何应对?

六、Redis Cluster集群(12题)

  1. Redis Cluster的数据分片原理是什么?
  2. 为什么Redis Cluster有16384个哈希槽而不是65536个?
  3. 哈希槽分配算法是怎样的?CRC16(key) % 16384
  4. 客户端如何知道key应该访问哪个节点?MOVED和ASK重定向的区别?
  5. Redis Cluster的节点间通信机制(Gossip协议)是怎样的?
  6. Cluster模式下,故障检测和故障转移是如何进行的?
  7. Cluster扩容时,槽和数据如何迁移?
  8. 扩容迁移期间,客户端请求如何处理?
  9. Redis Cluster最大支持多少个节点?为什么?
  10. 一致性哈希和哈希槽有什么区别?
  11. Redis Cluster支持跨槽的事务吗?为什么?
  12. Cluster模式下,批量操作(如mset/mget)有什么限制?

七、分布式锁(10题)

  1. Redis如何实现分布式锁?
  2. SETNX实现分布式锁有什么问题?
  3. 分布式锁的正确实现方式是什么?
  4. 为什么要用唯一标识(UUID)作为锁的值?
  5. 释放锁为什么必须用Lua脚本?
  6. 锁过期了但业务还没执行完怎么办?锁续期如何实现?
  7. Redisson的可重入锁是如何实现的?
  8. Redlock红锁算法是什么?解决了什么问题?
  9. Redlock有什么争议和缺陷?
  10. 分布式锁、数据库乐观锁、ZooKeeper锁怎么选型?

八、缓存三大问题(10题)

  1. 缓存穿透是什么?怎么解决?
  2. 布隆过滤器(Bloom Filter)的原理是什么?优缺点?
  3. 缓存击穿是什么?怎么解决?
  4. 缓存雪崩是什么?怎么解决?
  5. 热点Key怎么发现?怎么解决?
  6. 大Key(BigKey)怎么定义?有什么危害?
  7. 大Key如何检测?如何在线安全删除?
  8. 热Key重建缓存时,如何避免大量请求打到DB?
  9. 缓存预热怎么做?
  10. 如何保证本地缓存和Redis缓存的一致性?

九、缓存一致性(8题)

  1. 先更新数据库还是先删除缓存?为什么?
  2. 先删缓存后更新DB vs 先更新DB后删缓存,各有什么问题?
  3. 延时双删策略是什么?能保证强一致吗?
  4. 缓存与数据库的最终一致性如何保证?
  5. Canal等中间件如何实现缓存同步?
  6. 读写分离场景下,缓存一致性问题怎么处理?
  7. 强一致性和高可用在缓存场景如何权衡?
  8. 分布式事务(TCC/Saga)和缓存一致性如何结合?

十、性能优化与生产运维(12题)

  1. Redis常见的性能问题有哪些?

回答:

我将常见的性能问题归类为 6大类:

  1. 慢查询阻塞(CPU/命令):执行O(N)命令如 KEYS *、HGETALL 大Hash、ZRANGE(0,-1) 大ZSet,阻塞主线程。
  2. 网络带宽打满:频繁获取大Key(如10MB的JSON),或QPS过高(> 10万/秒),网卡成为瓶颈。
  3. 持久化引发的抖动(磁盘/内存):BGSAVE 或 AOF rewrite 触发的 fork 阻塞(COW内存翻倍),或磁盘IO能力不足导致AOF刷盘延迟(appendfsync everysec 卡顿)。
  4. 内存碎片与SWAP(操作系统):内存碎片率过高(mem_fragmentation_ratio > 1.5),或物理内存不足触发SWAP(将Redis内存页换出到磁盘),导致读写延迟从微秒级暴增到毫秒/秒级。
  5. 连接数耗尽:客户端连接池配置不当,或未正确释放连接,导致 maxclients 达到上限,新连接被拒绝。
  6. 过期淘汰策略抢占CPU:内存达到 maxmemory,频繁触发LRU/LFU淘汰算法,大量消耗CPU时间片。

  1. 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 看综合大盘,定位时间轴上的抖动点。


  1. KEYS命令有什么问题?生产环境为什么不能用?

回答:

问题:KEYS pattern 命令会全量扫描整个数据库的Key空间,时间复杂度 O(N),N为数据库中的Key总数。

生产危害:

· 如果实例有1亿个Key,KEYS * 可能会执行 几秒甚至几十秒。

· Redis是单线程,执行期间完全阻塞所有其他请求,导致整个服务不可用(P99延迟飙升到秒级,引发上游超时雪崩)。

生产替代方案:

· SCAN 命令(游标迭代器):分批渐进式遍历,不阻塞主线程,每次返回少量Key。

· 如果确实需要全量导出:用 redis-cli --scan 命令(底层也是SCAN),或在从库上执行 KEYS(避免影响主库)。

记忆口诀:线上绝对禁止KEYS,见到一次扣一次绩效。


  1. SCAN命令如何使用?和KEYS有什么区别?

回答:

用法:

shell 复制代码
SCAN cursor [MATCH pattern] [COUNT count]

· cursor:游标,从0开始,返回的游标为0时表示遍历结束。

· MATCH:模糊匹配模式(如 user:*)。

· COUNT:每次建议返回的数量(不是精确返回数,而是对底层bucket的扫描提示,默认10)。

与KEYS的3大核心区别:

  1. 阻塞性:KEYS 一次性全量阻塞;SCAN 每次只返回少量数据(默认约10~20条),不阻塞主线程。
  2. 数据一致性:KEYS 返回的是遍历瞬间的一致性快照;SCAN 在遍历过程中,若Key被修改(增删),可能返回重复Key,也可能遗漏Key(需要业务方接受或二次去重)。
  3. 返回结果:KEYS 直接返回Key列表;SCAN 返回 cursor, \[key1, key2...],需客户端迭代处理。

场景举例:需要找出所有 session:* 的Key并批量删除。用 SCAN 0 MATCH session:* COUNT 100 循环迭代,每次对返回的Key执行 UNLINK(异步删除),对线上零影响。


  1. 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。


  1. 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会放弃后续命令执行(但也不会回滚已执行的,不过在逻辑上更可控)。


  1. Lua脚本和事务有什么区别?Lua脚本能保证原子性吗?

回答:

区别(3个核心维度):

  1. 执行方式:Lua脚本整个作为一个命令发送,一次性执行;事务通过 MULTI 排队,EXEC 触发执行。
  2. 网络交互:Lua脚本1次RTT(发一次脚本内容);事务至少2次RTT(MULTI + 命令排队 + EXEC)。
  3. 逻辑复杂度:Lua支持条件判断、循环、变量,可实现复杂业务逻辑(如"先Get判断再Set");事务只是简单命令列表,无逻辑能力。

Lua脚本能保证原子性吗?------能(但要注意坑)。

· Redis执行Lua脚本时,整个脚本会被当成一个原子操作,脚本执行期间不会执行其他任何命令,保证了隔离性和部分原子性。

· 但是(重要):如果脚本中途执行到一半发生运行时错误(如对String执行 lpop),Redis会返回错误,但脚本中已执行成功的命令不会回滚!这与事务的运行时错误行为一致。

· 结论:Lua脚本只保证"执行期间不被打断",不保证"全成功或全失败"的回滚语义。要保证业务原子性,必须在脚本逻辑里做判断(如先检查类型,再操作)。


  1. 如何设计秒杀系统的库存扣减?

回答:

秒杀场景要求高并发下不超卖,且性能极致。我给出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)。


  1. 亿级数据的BigKey如何在线拆分优化?

回答:

场景:一个Hash大Key存储了1亿个用户标签(user_tags),内存占用10GB,导致 HGETALL 慢查询和网络带宽打满。

在线拆分方案(4步,零停机):

  1. 新Key设计(分桶):将大Hash拆分为 N个小Hash(如N=1000)。路由规则:new_key = "user_tags:" + (hash_tag(user_id) % 1000)。每个小Hash存储约10万用户。
  2. 双写过渡期:修改业务代码,同时写入旧Key和新Key(写两个地方),但读请求优先读新Key(若新Key无数据则降级读旧Key)。
  3. 后台数据迁移:编写脚本,使用 HSCAN 分批扫描旧Key,将数据按路由规则写入新Key,每次迁移1000条,使用 Pipeline 批量写入,控制速率防止拖垮Redis。
  4. 切流与清理:迁移完成后,业务代码完全切换到新Key,下线读旧Key逻辑。确认无误后,在低峰期对旧Key执行 UNLINK(异步删除,不阻塞主线程)。

注意事项:

· 分桶数(N)选择:预估单Key最大不超过1GB(建议500MB以内),N需结合未来增量规划。

· 若业务无法一次性改造路由逻辑,可使用 Proxy层(如Twemproxy)做映射,对业务透明。


  1. 1~2亿条数据需要缓存,如何设计存储方案?

回答:

这是一道容量规划 + 架构设计综合题。先做内存估算:

· 假设每条数据Key平均20字节、Value平均100字节,Redis内部对象开销约50字节/Key。

· 单条总开销 ≈ 20 + 100 + 50 = 170字节。

· 2亿条 × 170字节 ≈ 34GB 内存。

设计方案(按优先级推荐):

  1. 数据结构优化(首选):如果数据格式是 field-value 结构(如用户ID -> 用户信息),使用 Hash分桶(如 hash_tag = user_id % 10000),将2亿条打散到1万个Hash中。Hash在元素较少时使用ziplist编码,内存比普通String节省 50%以上。实测34GB可压缩到15GB左右。
  2. 实例分片(Cluster):若单实例内存超过物理机的60%(如64GB物理机只能给40GB),必须使用 Redis Cluster,将数据分片到多个节点(如6个节点,每节点约6GB)。
  3. 冷热分离:分析访问日志,识别出热数据(如最近7天活跃数据)和冷数据。热数据存Redis,冷数据存MySQL或对象存储(OSS),降低Redis内存成本。
  4. 压缩Value:如果Value是JSON,使用 MessagePack 或 Protobuf 序列化,比JSON节省30%~50%空间。

兜底方案:如果预算有限,开启Redis的 maxmemory + allkeys-lru 淘汰策略,让Redis自动淘汰冷数据,保证热数据常驻,命中率维持在95%以上。


  1. Redis的客户端连接池如何配置?连接池耗尽怎么办?

回答:

配置参数(以JedisPool为例):

· maxTotal(最大连接数):建议设置为 50~200(取决于业务并发量)。不是越大越好,过大会占用Redis服务端连接数(maxclients默认10000)。

· maxIdle(最大空闲连接):设为 maxTotal 的60%~80%,减少频繁创建销毁开销。

· minIdle(最小空闲连接):建议 5~10,保持预热。

· blockWhenExhausted(耗尽时是否阻塞):设为 true,配合 maxWaitMillis(建议 500ms ~ 1000ms)抛出异常,避免无限等待。

· testOnBorrow:不建议开启(每次借出都ping,增加开销),使用 testWhileIdle 配合空闲检测。

连接池耗尽怎么办(3步应急预案):

  1. 快速排查根因:通过 netstat -an | grep ESTABLISHED | wc -l 看当前连接数,若远超预期,检查业务代码是否有连接未归还(没执行 close())或异常中断导致连接泄漏。
  2. 紧急扩容:先临时调大 maxTotal(如从100调到500),并重启应用,保证服务恢复。
  3. 优化调用:如果并发确实很高(如QPS 5万),连接数不够用,改用 Pipeline 或 Lua脚本 批量操作,减少命令执行次数,从而降低对连接数的占用。
  4. 服务端调优:同时检查Redis服务端 maxclients 配置,如果客户端连接数逼近上限(INFO clients 看 connected_clients),需在Redis端调大 maxclients 或重启释放僵尸连接。

  1. 如何对Redis做监控告警?需要监控哪些指标?

回答:

监控方案:Prometheus + redis_exporter + Grafana(开源标准),或云服务商自带的监控(如阿里云Redis监控)。

核心监控指标(6大类,P6必背):

  1. 性能指标(延迟):监控 P99延迟(latency),超过5ms告警。关注 cmdstat_get、cmdstat
相关推荐
無a伟4 小时前
Redis持久化详解:RDB与AOF原理、配置、优缺点
数据库
muddjsv5 小时前
SQLite 外键进阶:CASCADE / SET NULL / 级联更新与完整性校验
数据库·sqlite
APItesterCris5 小时前
Open Claw 实战教程:5 分钟搭建京东商品自动化监控与数据分析系统
大数据·运维·数据库·数据仓库·自动化
Nturmoils5 小时前
查库存的 SQL 时灵时不灵,最后发现是 WHERE 里两个函数在打架
数据库
不想纳尼的青春6 小时前
where = 的作用?会影响性能吗?count(*) 和 count()哪个快?
数据库·oracle
倔强的石头_7 小时前
Spring Boot 接入金仓数据库:配置分层、启动自检与常见错误
数据库
黑桃小柒77 小时前
Django模型关系:从一对多到多对多全解析
数据库·django·sqlite
上海安当技术7 小时前
统一身份认证平台怎么落地?11 个异构业务系统接入 ASP 的完整实施路径
数据库·servlet·架构·kubernetes·jenkins
晚安code7 小时前
RocketMQ 顺序消费实战:批量接口加 Redis Pipeline同步数据
redis·rocketmq