GQ Redis

Redis 主从复制的实现原理是什么?

主从复制就是,将一个主节点上的数据全部复制到其他的从节点上,以保持主从节点的数据一致。

为什么需要主从复制?

  • 支持负载均衡:为了减轻Redis的压力使用了读写分离的形式,也就是主节点写,从节点读,这样就必须要保证主从节点数据保持一致
  • 提供故障恢复:当主节点出现问题时,可以由从节点提供服务,实现快速的故障恢复;
  • 主从复制是 Redis 高可用的基础,也是哨兵和集群实施的基础。

两种同步复制方式

全量同步

全量同步是 Redis 主从同步中最关键的基础流程,尤其是在 第一次建立主从关系时。

这里有一个问题,master 如何得知 salve 是否是第一次来同步呢?

有几个概念,可以作为判断依据:

Replication Id:简称 replid,是数据集的标记,replid 一致则是同一数据集。每个 master 都有唯一的 replid,slave 则会继承 master 节点的 replid(根据 replid 是否一致来判断是不是第一次同步:如果是第一次同步,主从节点的 replid 不同,如果是断开重连,主从节点的 replid 相同

offset:偏移量,随着记录在 repl_baklog 中的数据增多而逐渐增大。slave 完成同步时也会记录当前同步的 offset。如果 slave 的 offset 小于 master 的 offset,说明 slave 数据落后于 master,需要更新。

img
主从建立连接后,从节点会向主节点发送同步请求,其中包括:

  • 自己当前的 replication id(replid)
  • 当前的 offset

主节点据此判断是否可以进行增量同步。如果以下任一情况满足,就会触发全量同步:

  • 主从 replid 不一致 → 意味着这是一个新的 slave,必须进行全量同步。
  • 从节点 offset 太旧,主节点 backlog 已无法满足增量需求 → 只能全量同步。
全量同步完整流程描述:

第一阶段建立连接,协商同步

  • slave 节点请求增量同步
  • master 节点判断 replid,发现不一致,拒绝增量同步,或者一致但是offset太旧无法增量同步。

第二阶段:主库同步数据到从库

  • master 将完整内存数据生成 RDB快照,发送 RDB 到 slave
  • slave 清空本地数据,加载 master 的 RDB

第三阶段:master发送新写指令到从库

  • master 将 RDB 期间的命令记录在 repl_baklog,并持续将 log 中的命令发送给 slave
  • slave 执行接收到的命令,保持与 master 之间的同步

增量同步

增量同步是只会把主从库网络断联期间主库收到的命令同步交给从库

为什么需要增量同步?
全量同步需要先做 RDB,然后将 RDB 文件通过网络传输给 slave,成本太高了。因此除了第一次做全量同步,其它大多数时候 slave 与 master 都是做增量同步。

master 怎么知道 slave 与自己的数据差异在哪里呢?

这就要说到全量同步时的 repl_baklog 文件了。这个文件是一个固定大小的数组,只不过数组是环形,也就是说角标到达数组末尾后,会再次从 0 开始读写,这样数组头部的数据就会被覆盖。
repl_baklog 中会记录 Redis 处理过的命令及 offset,包括 master 当前的 offset,和 slave 已经拷贝到的 offset,slave 与 master 的 offset 之间的差异,就是 salve 需要增量拷贝的数据了。

  • 需要注意的是:如果 slave 出现网络阻塞,导致 master 的 offset 远远超过了 slave 的 offset:
img
  • 如果 master 继续写入新数据,master 的 offset 就会覆盖 repl_baklog 中旧的数据,直到将 slave 现在的 offset 也覆盖:
img 棕色框中的红色部分,就是尚未同步,但是却已经被覆盖的数据。此时如果 slave 恢复,需要同步,却发现自己的 offset 都没有了,无法完成增量同步了。只能做全量同步。(这就是我们上面说的:从节点 offset 太旧,主节点 backlog 已无法满足增量需求,只能被迫做全量更新)

小结: 模拟增量同步的几个场景

场景 1:正常增量同步

  • slave 掉线一小会儿,master 继续接收写入,repl_backlog 记录新命令。
  • slave 恢复连接,发来自己的 offset。
  • master 在 repl_backlog 中找到这部分数据,推送给 slave。
  • 增量同步成功完成。

场景 2:repl_backlog 写满了

  • 随着主节点不停接收新写入,repl_backlog 也在持续写入。
  • 因为是固定大小的环形数组,旧数据会被新数据覆盖。
  • 如果覆盖的数据包含 slave 尚未同步的 offset,那么 slave 想要增量同步时找不到数据。
  • 增量同步失败,只能重新做 全量同步。

Redis 集群的实现原理是什么?

什么是Redis集群

Redis 集群是通过多个 Redis 实例组成的一个分布式集群,它可以通过分片(sharding)机制将数据分布到多个节点上,确保数据的高可用性和负载均衡。

工作原理:

  • 主从节点: Redis 集群由多个 master 节点和多个 slave 节点组成。每个 master 节点存储数据,而 slave 节点负责复制对应主节点的数据,提供数据冗余和故障恢复。
  • 数据路由: 客户端请求会通过哈希槽机制找到对应的主节点。每个 Redis 节点管理 16384 个哈希槽,每个键值(key)通过哈希计算后定位到某个哈希槽,进而找到数据存储的节点
  • 故障转移: 如果某个主节点不可用,Redis 会自动将对应的从节点提升为新的主节点,保证集群的高可用性

数据分片:

Redis 集群使用哈希槽(Hash Slot)将数据分配到多个节点。每个节点负责一部分哈希槽的管理,数据通过计算哈希值映射到这些槽中,从而实现数据的分片存储。

总结:

  • Redis 集群通过哈希槽机制实现了数据分片,确保数据的分布均衡。

  • 集群具有高可用性自动处理节点故障,确保服务不中断。

  • 客户端通过哈希槽查询数据,能够快速定位到存储节点

Redis 通常应用于哪些场景?

Redis 是一个高性能的内存数据库,除了做缓存,它在很多高并发业务中都能用上。常见的使用场景主要有这几类:

缓存(最经典、最常见)

Redis 最大的用途就是做缓存,用来加速数据读取、减轻数据库压力。比如用户信息、热点数据、页面渲染结果都可以先放到 Redis 里

用户登录后,把用户资料放进 Redis,后续请求直接查缓存,不用每次打数据库。

分布式系统组件

Redis 还能作为分布式架构的“中间件”来用,包括:

  • 分布式 Session:
    • 多台服务共享一个 Redis,所有用户登录状态都存在里面,解决 Session 不同步问题。
  • 分布式锁:
    • 通过 SETNX 或 RedLock 实现多实例间的互斥访问,比如防止重复下单。
  • 分布式限流:
    • 用计数器或 Lua 脚本来控制接口调用频率,实现限流保护。

简单说,Redis 不止能缓存数据,还能帮我们在分布式系统里做‘协调员’。

实时系统与排行榜

Redis 支持高并发的读写,非常适合实时数据场景,比如网站统计、在线用户数、排行榜等。
比如:排行榜常用 Redis 的 Sorted Set(有序集合)来实现:

  • ZADD 添加用户和分数;
  • ZREVRANGE 查询前 N 名;
  • ZINCRBY 更新分数。

想要实时榜单或统计接口量,用 Redis 分分钟搞定。

消息队列

Redis 自带的 List 或 Pub/Sub 功能可以做轻量级的消息队列,用来异步解耦任务。

用 LPUSH + BRPOP 实现简单任务队列,比如下单异步发消息、发送通知等。

计数器与统计

Redis 的自增操作原子性很强,非常适合做计数器。
比如:

  • 统计网站访问量;
  • 点赞、收藏、评论计数;
  • 接口限流统计。
1
INCR page:view:1001   # 访问量 +1

其他用处

除了上面这些,Redis 还经常用于:

  • Bitmap:实现签到、活跃天数统计;
  • Geo:计算用户地理位置、附近的人;
  • HyperLogLog:做 UV(去重计数);
  • 分布式 ID 生成器;
  • 时间轴、商品标签、用户关注关系等结构型数据。

小结

Redis 不仅是缓存工具,更是高性能的中间件组件。
它能做缓存、分布式锁、消息队列、排行榜、计数器、签到、地理位置计算等各种场景,几乎是互联网项目里必备的一环。

Redis 为什么这么快?

Redis 之所以快,主要有五个原因:

第一,基于内存存储
Redis 的数据都在内存里,内存访问比磁盘快几个数量级(RAM 延迟纳秒级,SSD 是微秒级),这决定了它的高读写速度。

第二,单线程 + IO 多路复用
Redis 用单线程执行命令,避免线程切换开销;同时用 I/O 多路复用(epoll)模型一个线程能同时处理上万连接,效率非常高

第三,高效的数据结构
内部结构比如哈希表、跳表、压缩列表(ziplist / listpack)等,都经过高度优化,大部分操作能在 O(1) 或 O(logN) 内完成。

第四,Redis 6.0 引入多线程 I/O
虽然核心命令仍是单线程执行,但网络读写、解包等可多线程并发处理,进一步提升吞吐。

总结一句话:Redis 快的本质,是“内存 + 单线程 + I/O多路复用 + 高效数据结构 + 多线程I/O” 共同作用的结果。

面试速答:

Redis 快主要因为:

  • 数据都在内存里,速度比磁盘快很多;
  • 单线程避免切换开销,用 I/O 多路复用提升并发;
  • 内部数据结构高度优化,操作复杂度低;
  • Redis 6 开始网络 I/O 多线程化,性能更高。

简单说就是:内存存储 + 高效结构 + I/O 多路复用 + 轻量线程模型。

面试官可能追问的延展

问:Redis 单线程为什么还能这么快?
答:因为主要耗时在 I/O,不在 CPU,单线程配合多路复用反而最优

问:Redis 什么时候用到多线程?
答:Redis 6.0 开始多线程处理网络读写(accept、read/write),核心命令执行仍是单线程。

问:和 Memcached 的性能差异?
答:Redis 除了内存快,还优化了数据结构(ZSet、Hash 等)和持久化机制(RDB、AOF)。

问:什么是I/O 多路复用?
答:I/O 多路复用就是:用一个线程同时监视很多连接的可读/可写事件(如 epoll/kqueue),哪个就绪就处理哪个。
它的好处是少线程、少上下文切换、能支撑成千上万连接,所以像 Redis 用“单线程 + 多路复用 + 事件循环”就能跑很快。

为什么 Redis 设计为单线程?6.0 版本为何引入多线程?

为什么 Redis 设计成单线程?

第一,Redis 是内存数据库,速度瓶颈不在 CPU
它主要做内存操作和网络通信,CPU 根本不是重点,用多线程反而会增加线程切换的开销

第二,单线程简单又稳定
一个线程顺序执行所有命令,不需要加锁、也不会死锁所有命令天然是原子操作,出问题也好排查。

第三,配合 I/O 多路复用就够快了
Redis 用 epoll 这类机制,一个线程就能同时处理成千上万个请求,所以根本不需要多线程

那 6.0 为何又引入多线程?

因为网络 I/O 开始成为瓶颈了,比如并发多、带宽高、传大数据的时候
所以 Redis 6.0 把网络读写这些活交给多个线程来做,提高了吞吐量。
但注意:真正执行命令的部分还是单线程,保证逻辑简单、结果可预测。

一句话总结:

Redis 单线程是为了简单高效,6.0 加多线程是为了让网络 I/O 更快,不会破坏原来的执行顺序。也就是说Redis 现在的做法是“多线程处理网络,单线程执行命令”,两头都兼顾。

面试速答:

Redis 单线程是因为它主要操作内存,不靠 CPU,单线程又简单又快,还避免加锁问题
到 6.0 后,为了加快网络读写,就把 I/O 改成多线程,但核心命令还是单线程执行

Redis 中常见的数据类型有哪些?

Redis 的核心数据结构,常用的有 五大类 + 四个扩展

五大类:

  • String:普通字符串,Redis 中最简单的数据类型
    • 使用场景: 计数器、分布式锁标记、缓存单值;支持整型自增、二进制安全。
  • Hash:键值对集合,内部使用哈希表实现
    • 使用场景:存对象字段(如用户信息),方便快速检索
  • List:Redis 中的 List 类型与 Java 中的 LinkedList 类似,有序集合,可以看做是一个双向链表结构。既可以支持正向检索和也可以支持反向检索。
    • 使用场景:消息队列/任务队列(LPUSH/RPOP、BLPOP阻塞取),储存用户操作的历史记录,便于快速访问。
  • Set:无序去重集合,内部使用哈希表实现。
    • 使用场景:兴趣标签、去重、集合运算(交并差)。
  • Sorted Set(ZSet):带 score 的有序集合,SortedSet 中的每一个元素都带有一个 score 属性,可以基于 score 属性对元素排序,底层的实现是一个跳表(SkipList)加 hash 表
    • 使用场景:排行榜、按权重/时间排序,支持区间查询。
img

扩展结构:

  • Bitmap:用 bit 存布尔状态,签到/活跃统计,超省内存。
  • HyperLogLog:基数统计(去重后的数量)近似值,内存固定很小。
  • GEO:存经纬度,附近的人/店,范围查询。
  • Stream:日志/消息流,消费组支持,做轻量消息队列。

选型思路:单值用 String;对象用 Hash;队列用 List/Stream;去重/集合运算用 Set;排序/排行榜用 ZSet;大规模去重计数用 HLL;签到这类布尔时间线用 Bitmap;地理位置就 GEO
生产上注意过期策略、内存上限、热键与大 Value 拆分,以及必要时持久化(RDB/AOF)。

补充:

  • ZSet 实现:跳表 + 字典;List 新版本用 quicklist;Hash 小对象是紧凑编码(listpack),大了转哈希表。
  • 怎么选 List 还是 Stream? 简单队列、吞吐不高用 List;要消费组/消息追溯就用 Stream。
  • 基数统计为什么用 HLL? 固定内存 12KB 左右,能做亿级近似去重计数,误差约 0.81%。

总结:

Redis 常见 5 类String、Hash、List、Set、Sorted Set
再加 4 个扩展:Bitmap、HyperLogLog、GEO、Stream。
对应场景:单值缓存/计数、对象字段、队列、去重集合、排行榜、布尔时间线、近似去重计数、地理围栏、消息流。

Redis 中跳表的实现原理是什么?

什么是跳表?
跳表主要是通过多层链表来实现,底层链表保存所有元素,而每一层链表都是下一层的子集。

  • 跳表 = 底层完整有序链表 + 若干层“稀疏索引链表”。上层是下层的子集,像高速路“快车道”,用来加速查找。

为什么用:

能实现有序集合的按 score 进行排序、范围查询、按名次(rank)定位等操作,复杂度接近平衡树但实现更简单。

查找(score / member):

img
简单来说就是——从上往下、从左往右找。先从最顶层开始,一路往右找;如果发现下一个节点比目标大,就往下一层走;一直重复“能右就右,不能右就下”,
最后到最底层,要么找到目标,要么确定没有。查找效率大概是 O(log n)。

插入

img

先按查找的方式找到插入的位置,再把元素插到最底层的链表里。然后 Redis 会用“抛硬币”的方式决定要不要把这个元素升到更高一层,比如有 50% 的概率升一层,再 50% 升到下一层……
直到“硬币没过”或者到了最高层。这样可以保证整体层级比较平衡。

删除

img

先按查找的方式找到目标元素在每一层的位置,把元素从最底层的链表里删掉,然后在各层都把它从链表里摘掉。
如果删完某一层没有节点了,这一层就自动消失。删除的复杂度也是 O(log n)。

总结:

跳表查找就是“能右就右,不能右就下”;插入像“抛硬币”决定高度;
删除就是“逐层摘链”;查、插、删平均都是 O(log n),实现简单又快,是 Redis 有序集合的关键结构。

Redis的跳表实现和普通跳表实现的区别点

  • 多了一个回退指针(backward,指向底层前驱),且score可以重复
  • 回退指针能让 Redis 跳表“会倒着走、能就地摘”,减少一次查找,反向遍历和删除/更新更快,非常符合 Redis ZSet 的使用场景。

Redis 的 hash 是什么?

什么是Redis的Hash?

Redis中的hash是一种键值对集合,可以将多个字段和值存储在一个键中,方便管理一些关联数据。

  • 可以把它理解成“存在 Redis 里的小型字典/对象”:一个 key 对应很多 field→value。
  • 适合把一个对象的多个属性放在一起存,比如:user:1001 => {name:Tom, age:18}。

底层实现(按大小自动切换)

  • 小对象:用压缩列表 紧凑存储,省内存;
  • 大对象:当hash类型元素个数小于512并且每个字段名和字段值的长度小于64个字节,切到 哈希表(dict),查改更快但占内存稍多。

什么时候用 Hash?

  • 需要把同一个对象的多字段放一起、且字段较多但单个字段不大
  • 想节省内存、又希望对字段做 O(1) 的 CRUD;
  • 做层级结构(比如用户、商品属性)比用很多独立 key 更好管理。

rehash(扩/缩容)怎么做?

  • 采用渐进式 rehash:不是一次性迁移、而是分批在后续读写操作里慢慢搬,避免卡顿;
  • 触发通常跟负载因子有关:
    • 过大(>≈1,或更激进时 >≈5)会扩容(一般按 2 倍);
    • 很小(<≈0.1)会缩容(节省内存);
  • rehash 期间会同时维护两个表,新数据逐步迁走,对外读写不受影响。

总结:

Redis Hash = 一个 key 里的“小字典”。小用 listpack 省内存,大用哈希表提性能;rehash 渐进式,扩缩容靠负载因子。最适合存对象型数据。

Redis 支持事务吗?如何实现?

  • Redis 是支持事务的,但和 MySQL 的事务不一样。
  • Redis 事务主要保证的是命令的原子性执行,而不支持回滚机制。

Redis 事务的实现方式

Redis 通过以下几个命令实现事务功能:

  • MULTI:标记事务的开始,之后输入的命令都会被暂存到队列中;
  • EXEC:执行事务队列中的所有命令;
  • DISCARD:放弃事务,清空命令队列;
  • WATCH:监控一个或多个 key,用于实现乐观锁机制。

Redis 事务其实就是一组命令的批量执行,所有命令会先入队,等到执行 EXEC 的时候,一次性顺序执行完,保证中间不会被打断。

Redis 事务的执行流程

  1. 开启事务:使用 MULTI 命令;
  2. 命令入队:事务中的每条命令先进入队列;
  3. 执行事务:调用 EXEC 批量执行命令;
  4. 放弃事务:如果中途要取消,可用 DISCARD;
  5. 并发控制:若用 WATCH 监控的 key 被其他客户端修改,EXEC 会直接取消执行,防止数据不一致。

Redis 事务与 MySQL 事务的区别

对比项 MySQL 事务 Redis 事务
一致性保证 支持完整的 ACID 特性 只保证命令的原子性执行
回滚机制 发生错误可回滚 不支持回滚
并发控制 支持锁机制 WATCH 实现乐观锁
隔离级别 可调节(如 Read Committed 等) 不支持多级隔离设置

“MySQL 的事务是严格的 ACID 模型,而 Redis 更像是一个命令队列批处理工具,执行时要么全成功,要么全失败,但不会回滚。”

为什么 Redis 不支持回滚
从 Redis 2.6.5 开始,Redis 能在命令入队阶段检测语法错误,但如果执行阶段某条命令出错,Redis 不会回滚已执行的命令,而是继续执行后续命令。

举个例子:
如果事务中一条命令失败,比如某条 INCR 操作在非整数上执行失败,Redis 不会撤销之前的命令。
官方说明中明确写到:
“Errors happening after EXEC are not handled in a special way; all the other commands will still be executed even if some command fails.”

小结

  • Redis 是支持事务的,但和 MySQL 不一样。Redis 的事务是通过 MULTI、EXEC、DISCARD、WATCH 这几个命令来实现的,本质上是命令的批量原子执行
  • 它能保证事务内的命令要么全部执行,要么全部丢弃,但执行过程中不会回滚,也没有多级隔离机制。
  • 所以我们可以理解 Redis 的事务其实更像一个命令队列的批处理,而不是数据库那种严格意义上的 ACID 事务。

Redis 数据过期后的删除策略是什么?

Redis 的过期 KEY 处理有两种策略,分别是惰性删除和周期删除

惰性删除是指在每次用户访问某个 KEY 时,才会判断 KEY 的过期时间:如果过期则删除;如果未过期则忽略。

周期删除有两种模式:

  • SLOW 模式:通过一个定时任务,定期的抽样部分带有 TTL 的 KEY,判断其是否过期。默认情况下定时任务的执行频率是每秒 10 次,但每次执行不能超过 25 毫秒。如果执行抽样后发现时间还有剩余,并且过期 KEY 的比例较高,则会多次抽样。

  • FAST 模式:在 Redis 每次处理 NIO 事件之前,都会抽样部分带有 TTL 的 KEY,判断是否过期,因此执行频率较高。但是每次执行时长不能超过 1ms,如果时间充足并且过期 KEY 比例过高,也会多次抽样

Redis 如何判断 KEY 是否过期呢?

答:Redis内部有两个哈希表,一个用来存放key和value,另一个专门记录key的过期时间。每次判断是否过期的时候,它就去那个记录过期时间的哈希表查一下这个key有没有过期

Redis 中有哪些内存淘汰策略?

Redis 支持 8 种不同的内存淘汰策略:

noeviction: 不淘汰任何 key,但是内存满时不允许写入新数据,默认就是这种策略。
volatile-ttl: 对设置了 TTL 的 key,比较 key 的剩余 TTL 值,TTL 越小越先被淘汰
allkeys-random:对全体 key ,随机进行淘汰。也就是直接从 db->dict 中随机挑选
volatile-random:对设置了 TTL 的 key,随机进行淘汰。也就是从 db->expires 中随机挑选。
allkeys-lru: 对全体 key,基于 LRU 算法进行淘汰
volatile-lru: 对设置了 TTL 的 key,基于 LRU 算法进行淘汰
allkeys-lfu: 对全体 key,基于 LFU 算法进行淘汰
volatile-lfu: 对设置了 TTL 的 key,基于 LFU 算法进行淘汰

策略名 描述
noeviction 不淘汰,内存满时直接报错,默认策略
volatile-ttl 仅淘汰设置了 TTL 的 key,优先淘汰 TTL 最小的
allkeys-random 所有 key 中随机淘汰
volatile-random TTL key 中随机淘汰
allkeys-lru 所有 key 中淘汰最久未使用的(近似 LRU)
volatile-lru TTL key 中淘汰最久未使用的
allkeys-lfu 所有 key 中淘汰访问频率最低的(近似 LFU)
volatile-lfu TTL key 中淘汰访问频率最低的

聊聊LRU和LFU:

LRU 是“最近最久未使用”,它记录每个 key 最近一次被访问的时间,然后从中挑最长时间没用的删掉。

但 Redis 的 LRU 是个近似算法,它会抽样一批 key,然后在里面选一个最久没用的来删。

LFU 是“最不常使用”,不是看最近一次用,而是看访问频率。它会记录每个 key 的访问次数,这样可以把访问频率低的删掉。不过因为内存有限,它其实记录的是一个“逻辑次数”,会做一些概率加减,最大值是 255,而且每分钟会自动衰减一次,这样就能反映出一个 key 的“热度”了。

小结

Redis 内存满了之后会通过内存淘汰机制来释放内存。它支持 8 种策略,比如 LRU、LFU、随机等,区别在于是对所有 key 还是仅限设置了 TTL 的 key。Redis 不是对所有 key 做全量排序,而是通过抽样 + 候选池的方式来快速选出要淘汰的 key,效率很高。

如果业务对内存敏感,推荐使用 allkeys-lfu,这能最大程度上保留热门数据,清理冷门数据,比较适合高频访问的场景。

img

Redis 的 Lua 脚本功能是什么?如何使用?

  • 功能:允许用户在redis服务端执行lua脚本,实现原子性操作和复杂的功能逻辑。
  • 怎么用:通过redis.call调用redis命令,也有if,else这种逻辑可以用
  • 原理:因为Redis会将Lua脚本封装成一个单独的事务,而这个单独的事务会在Redis客户端运行时,由Redis服务器自行处理并完成整个事务,如果在这个进程中有其他客户端请求的时候,Redis将会把它暂存起来,等到 Lua 脚本处理完毕后,才会再把被暂存的请求恢复。

Redis 的 Pipeline 功能是什么?

Redis 的 Pipeline(管道) 功能允许客户端在一次网络请求中批量发送多个命令
从而减少网络往返次数(RTT),大幅提升吞吐量

  • 简单说,Pipeline 就像寄快递时打包多个包裹一次寄出,节省了很多来回跑的时间。

Pipeline 与事务的区别

对比项 Pipeline Redis 事务(MULTI/EXEC)
是否保证原子性 ❌ 不保证 ✅ 保证
执行方式 批量发送命令,提高吞吐量 命令入队后一次性原子执行
失败处理 某条命令失败不影响其他命令 执行错误会影响整个事务结果
  • Pipeline 只是网络层面的优化,并不是事务。命令之间相互独立,即使一条失败,其他命令照样执行。

小结

Redis 的 Pipeline 是一个批量命令机制,让客户端一次性发多个命令给服务器,从而减少网络延迟,提高吞吐量。
它不保证原子性,也不是事务,只是性能优化
一般我们会控制每次打包的命令数,比如不超过 1 万条,太多可能导致内存压力。
如果需要保证命令的原子性,那就得用 Lua 脚本或者 Redis 事务

Redis 中的 Big Key 问题是什么?如何解决?

什么是big key

在 Redis 中,Big Key 是指占用大量内存的键Big Key 不仅仅是键的值很大,还包括对应的 value 占用大量内存的情况,常见的如 String、list、hash、set、zset 等数据类型的键。通常来说,这些数据类型包含的元素数量过多,或者字符串的值本身很大,都会被视为 Big Key

Big Key 问题的危害

  • 性能影响:Big Key 的值会占用大量内存,导致访问速度慢,从而影响 Redis 的整体性能。
  • 内存占用:大量 Big Key 会占满 Redis 的内存,导致无法存储其他数据,甚至可能造成 Redis 卡顿。
  • 内存分布不均:在 Redis 集群中,某些节点可能存储了 Big Key,这会导致多个节点之间的内存使用不均衡。
  • 备份和恢复困难:当通过 RDB 文件恢复数据时,Big Key 会导致恢复过程变得异常缓慢,甚至无法正常恢复。
  • 搜索困难:Big Key 的数据量大,查找时需要的时间长,且会影响查找效率。
  • 迁移困难:迁移 Big Key 也会消耗大量资源,可能会影响 Redis 数据一致性。
  • 过期执行耗时:当设置了过期时间的 Big Key 被删除时,删除过程会非常耗时。

如何解决 Big Key 问题

以下是几种解决 Big Key 问题的方法:

开发层面

  1. 数据压缩:针对存储数据的大小,可以进行压缩来减少内存占用。
  2. 拆分大 Key:将一个大 Key 拆分成多个小 Key,以减少单个 Key 的存储负担。
  3. 使用合适的数据结构:对于存储大量数据的场景,使用 Hash 类型、Set 类型等数据结构来优化存储方式。

业务层面

  1. 删除低频使用的 Big Key:可以根据访问频率,对一些不常用的 Big Key 进行删除。
  2. 合理设置缓存 TTL:通过设置合适的过期时间,避免过期的缓存不及时删除。
  3. 清理不必要的字段和信息:可以去掉一些不必要的数据或信息,减小内存占用。

数据分布层面

  1. 使用 Redis 集群:通过 Redis 集群对数据进行分片,将 Big Key 分布到不同的节点上,从而提高查询和写入的性能。

辅助工具–(识别big key)

  • redis-cli 工具:可以使用 redis-cli --bigkeys 来扫描 Redis 数据库,找出所有的大 Key。

如何解决 Redis 中的热点 key 问题?

什么是热key问题?

热 key 问题指的是在某个瞬间,大量请求集中访问 Redis 里的同一个固定 key,这会造成缓存击穿,使得请求都直接涌向数据库,最终拖垮缓存服务和数据库服务,进而影响应用服务的正常运行。

热点新闻、热点评论、明星直播这类读多写少的场景,很容易出现热点 key 问题。虽然 Redis 的查询性能比数据库高很多,但它也有性能上限,单节点查询性能一般在 2 万 QPS,所以对单个固定 key 的查询不能超过这个数值。

怎么解决热key问题?

对于热key的处理,主要在于事前预测和事中解决。

  • 事前预测:这通常是通过经验预测,即在促销活动或者已知的热点事件前,提前识别出可能成为热key的key,例如“双11秒杀商品”。
    • 然而这种方式并不完美,因为某些突发的热点事件(比如明星官宣)是无法提前预测的。
  • 事中解决:
  1. 多级缓存
  • 将数据缓存在多个层级的缓存中(例如,浏览器缓存、CDN缓存、Redis缓存等),减少直接访问后端数据库的次数,从而缓解系统压力。
  • 缓存策略包括使用CDN、客户端本地缓存等。
  1. 热key备份
  • 除了在单一的缓存服务器上进行缓存外,还可以将热点数据分发到多个缓存集群中。如果一个集群承载过重,其他集群可接管流量,保证系统的高可用性。
  1. 热key拆分
  • 一个热key拆分为多个子key,并且将这些子key分散到不同的缓存节点上。例如,将“热搜商品”拆分成多个子key存储到Redis集群的不同节点,这样多个请求就能分散到不同节点,从而避免了单点压力。

热key的识别方法

  • 经验预测:基于历史数据和业务经验预测可能成为热key的key。

  • 实时监控:通过实时监控并收集key的访问频率,使用Redis本身的热点key发现功能(如redis-cli中的–hotkeys选项)来动态识别。

多热算热,给个标准?

JD有一个框架叫做hotkey,他就是专门做热key检测的,他的热key定义是在单位时间内访问超过设定的阈值频次就是热key这个阈值需要业务自己设定,并不断的调整和优化。

热key的定义,通常以其接收到的Key被请求频率来判定,例如:

  • QPS集中在特定的Key:Redis实例的总QPS为10,000,而其中一个Key的每秒访问量达到了7,000。那么这个key就算热key了。
  • 带宽使用率集中在特定的Key:对一个拥有1000个成员且总大小为1 MB的HASH Key每秒发送大量的HGETALL操作请求。
  • CPU使用时间占比集中在特定的Key:对一个拥有10000个成员的Key(ZSET类型)每秒发送大量的ZRANGE操作请求。

Redis 的持久化机制有哪些?

为什么需要持久化

Redis 是一个基于内存的数据库,所有数据存储在内存中。如果 Redis 服务发生了宕机,内存中的数据会全部丢失。因此Redis提供了持久化机制,将数据保存在磁盘中,以便在服务重启的时候恢复数据。

目前Redis提供了三种持久化机制

RDB 持久化机制

RDB(Redis Database) 是通过生成内存快照,将数据保存为二进制文件(dump.rdb)的方式实现持久化。它记录了某一时刻的数据状态,可用于灾难恢复和快速重启。

RDB的优点是:快照文件小、恢复速度快,适合做备份和灾难恢复。

RDB的缺点是:定期更新可能会丢数据

AOF

AOF(Append Only File) 是通过将每个写操作追加到日志文件(appendonly.aof)的末尾实现持久化的方式。当Redis 重启时可以通过重放日志文件中的命令来恢复数据。

但是如果Redis刚刚执行完一个写命令,还没来得及写AOF文件就宕机了,那么这个命令和相应的数据就会丢失了。但是他也比RDB要更加靠谱一些。

AOF的优点是:可以实现更高的数据可靠性、支持更细粒度的数据恢复,适合做数据存档和数据备份。

AOF的缺点是:文件大占用空间更多,每次写操作都需要写磁盘导致负载较高

RDB与AOF比较

特性 RDB AOF
数据可靠性 可能会丢失最后一次快照之后的数据 保证最后一次写操作之前的数据不会丢失
性能 读写性能较高,适合做数据恢复 写性能较高,适合做数据存档
存储空间占用 快照文件较小,占用空间较少 AOF文件较大,占用空间较多
恢复时间 从快照文件中恢复数据较快 从AOF文件中恢复数据较慢

混合持久化

混合持久化 是 Redis 4.0 引入的一种机制,结合了 RDB 和 AOF 的优点:

  • RDB 用于快速保存大部分数据。
  • AOF 用于记录两次快照之间的增量数据。

通过 aof-user-rdb-preamble 配置开启混合持久化,在开启混合持久化的情况下,AOF 重写时会把 Redis 的持久化数据,以 RDB 的格式写入到 AOF 文件的开头,之后的数据再以 AOF 的格式化追加到文件的末尾。

这样的优势就是,混合持久化结合了 RDB 和 AOF 持久化的优点,开头为 RDB 的格式,使得 Redis 可以更快的启动,同时结合 AOF 的优点,有减低了大量数据丢失的风险

扩展知识

Redis能完全保证数据不丢失吗?

不能,因为Redis是基于内存存储的,当Redis进程异常退出或服务器断电等情况发生时,内存中的数据可能会丢失。

为了防止数据丢失,Redis提供了RDB和AOF的持久化机制,Redis可以将数据从内存保存到磁盘中,以便在Redis进程异常退出或服务器断电等情况下,通过从磁盘中加载数据来恢复数据。

但是,持久化机制也不是绝对可靠的,归根结底Redis还是个缓存,他并不是完全给你做持久化用的,所以还是要有自己的持久化方式,比如双写到数据库。

因此,为了最大程度地保障数据安全,建议采用多种手段来提高数据可靠性,如定期备份数据、使用主从复制机制、使用集群模式等。

AOF的三种写回策略

AOF有三种数据写回策略,分别是Always,Everysec和No。

  • Always,同步写回:每个写命令执行完,立马同步地将日志写回磁盘;
  • Everysec,每秒写回:每个写命令执行完,只是先把日志写到AOF文件的内存缓冲区,每隔一秒把缓冲区中的内容写入磁盘;
  • No,操作系统控制的写回:每个写命令执行完,只是先把日志写到AOF文件的内存缓冲区,由操作系统决定何时将缓冲区内容写回磁盘。

“同步写回”可靠性肯定是最高的,但是它在每一个写命令后都有一个落盘操作,而且还是同步的,这和直接写磁盘类型的数据库有啥区别?

"操作系统控制的写回"这种是最不靠谱的,谁知道操作系统啥时候帮你做持久化,万一没来及持久化就宕机了,不就gg了。

"每秒写回"是在二者之间折中了一下,异步的每秒把数据写会到磁盘上,最大程度的提升效率和降低风险。

Always也不能保证不丢

即使是在always策略下,也不能保证100%不丢失数据的,主要出于以下原因:

  1. 磁盘和系统故障:如果在写入操作和同步到磁盘之间发生硬件故障或系统崩溃,可能会丢失最近的写操作。

  2. 操作系统缓冲区:即使Redis请求立即将数据同步到磁盘,操作系统的I/O缓冲区可能会导致实际写入磁盘的操作延迟发生。如果在写入缓冲区之后,没写磁盘前,机器挂了,那么数据就丢了。

操作系统缓冲区,通常指的是操作系统用于管理数据输入输出(I/O)的一种内存区域。当程序进行文件写入操作时,数据通常首先被写入到这个缓冲区,而不是直接写入到硬盘。

  1. 磁盘写入延迟:磁盘的写入并非实时完成,特别是在涉及到机械硬盘时,写入延迟主要由磁盘旋转速度(RPM)和寻道时间决定。如果在这这个延迟过程中,机器挂了,那么数据也就丢了。

Redis 7.0 MP-AOF(Multi-Part Append Only File)

7.0 之前的 AOF 重写有三大问题:

  • 内存开销:aof_buf 和 aof_rewrite_buf 中大部分内容是重复的。
  • CPU 开销:主进程需要花费 CPU 时间往 aof_rewrite_buf 写入数据,并向子进程发送aof_rewrite_buf 中的数据。子进程需要消耗 CPU 时间将aof_rewrite_buf 写入新 AOF 文件。
  • 磁盘开销:aof_buf数据会写到当前的 AOF 文件,aof_rewrite_buf数据写到新的 AOF 文件,一份数据需要写两次磁盘。

针对以上问题 Redis 7.0 引入了 MP-AOF(Multi-Part Append Only File)机制。简单来说就是将一个 AOF 文件拆分成了多个文件:

  • 一个基础文件(base file),代表数据的初始快照
  • 增量文件(incremental files),记录自基础文件创建以来的所有写操作,可以有多个
  • 基础文件和增量文件都会存放在一个单独的目录中,并由一个清单文件(manifest file)进行统一跟踪和管理

Redis 在生成 RDB 文件时如何处理请求?

Redis 在生成 RDB 文件时,是通过 fork 子进程 的方式来做的。主进程继续处理客户端请求,而由子进程负责把数据写到 RDB 文件里,这样就不会影响线上正常读写。

原理说明

生成 RDB 的时候,Redis 主进程会执行 bgsave 命令,
它会 fork 出一个子进程来完成快照。

fork 的时候不会复制整块内存,而是主进程和子进程共享相同的内存页(共享物理页),
这就是写时复制(Copy On Write, COW) 技术。

那如果此时有写操作怎么办?

如果主进程在这时收到写命令,比如要修改某个 key 的数据,它就会把要修改的那一页内存复制一份,主进程在副本上修改数据,而子进程仍然指向旧的数据页。

这样一来:

  • 子进程看到的是老的数据(旧快照),可以安心生成 RDB;
  • 主进程修改的是新数据,不会互相影响。

这就保证了:RDB 快照是数据生成那一刻的状态,主进程还能照常提供服务

就像 Redis 在拍一张‘全量照片’,拍照的是子进程,而主进程还能继续‘工作’。
要是拍照过程中有人动了东西,主进程就复制一份新的给自己改,
照片里留下的还是当时的状态,不会乱。

小结

Redis 在生成 RDB 时通过 fork 子进程配合写时复制技术,让子进程负责快照、主进程继续处理请求,既保证数据一致性,又不影响正常服务。

Redis 的哨兵机制是什么?

Sentinel(哨兵) 的三个作用是什么?

哨兵机制主要有三个作用:

  • 第一个是监控 Redis 节点的运行状态,每秒发送 ping 判断节点是否正常;
  • 第二个是故障恢复,当主节点宕机时,它可以自动完成主从切换;
  • 第三个是通知机制当主从发生变化时会通过 API 通知其他服务,比如客户端或中间件,帮助它们更新连接信息。

Sentinel 如何判断一个 redis 实例是否健康?

  • 每隔 1 秒发送一次 ping 命令,如果超过一定时间没有相向则认为是主观下线(sdown)
  • 如果大多数 sentinel 都认为实例主观下线,则判定服务客观下线(odown)

故障转移步骤有哪些?

故障转移一共分 4 步:

  1. “首先,从所有哨兵中选出一个 leader只有 leader 有权限发起故障转移;”

  2. “然后 leader 会从所有从节点中选择一个作为新的主节点,执行 SLAVEOF NO ONE;”

  3. “再让其他的 slave 执行 SLAVEOF 新主节点,重新组成主从结构;”

  4. “最后,修改之前主节点的配置,让它重新变为新的主节点的从节点。”

sentinel 选举 leader 的依据是什么?

  • 票数超过 sentinel 节点数量 1 半
  • 票数超过 quorum (法定人数)数量,例如quorum 值为2代表着(三个哨兵,2 个认为主观下线就是客观下线)
  • 一般情况下最先发起 failover 的节点会当选

sentinel 从 slave 中选取 master 的依据是什么?

sentinel 会综合考虑多个因素来挑选新的主节点,优先级如下:

  1. “先看这个从节点与原主节点断开时间是否过长,太久的会被排除;”

  2. “再看 slave 节点的 slave-priority 值,数值小的优先,值为 0 则不参与选举;”

  3. “如果优先级一样,就比谁的 offset 更大,数据更全的优先;”

  4. “最后如果还一样,就比 run_id谁小谁胜出。”

Redis 集群会出现脑裂问题吗?

Redis 集群确实可能出现“脑裂(Split-Brain)”问题,
通常是由于 网络分区或主节点故障恢复不及时 导致系统中出现两个主节点同时对外提供服务,从而造成数据不一致或数据丢失

什么是脑裂

脑裂(Split-Brain)指的是在一个分布式系统中,同时出现多个主节点(Master),每个节点都以为自己是“主脑”,开始独立对外提供服务。

  • 脑裂就像一个团队同时有两个领导,都在下命令,最后每个人的任务都乱套了。
    在 Redis 中,这种情况常见于以下两种场景
  1. 网络分区
  • 比如主节点和从节点、哨兵之间的通信被隔断;
  • 哨兵无法联系上 Master,以为它挂了,就会重新选出新的 Master;
  • 此时原 Master 其实还在运行,两个 Master 同时对外服务。
  1. 主节点短暂故障后恢复
  • Master 因故障暂时下线,哨兵选出新的主节点;
  • 原来的 Master 恢复后没有及时变为从节点,而是继续接收写入请求。
    结果就是:两个主节点同时接受写操作

脑裂的危害

问题类型 描述
数据不一致 两个主节点分别写入不同的数据,导致集群状态不一致。
数据丢失 原主节点恢复后会被重新同步,新数据被清空。
重复写入 合并数据时可能造成相同命令重复执行。
  • 最严重的情况是数据丢失,因为新的主节点会让原主节点执行全量同步,而同步前会清空旧数据。

Redis 如何避免脑裂

Redis 提供了两个关键参数用于防止脑裂:

参数名 含义
min-slaves-to-write 主节点要求最少有多少从节点处于正常复制状态才能写入。
min-slaves-max-lag 主从之间允许的最大复制延迟时间(秒)。
  • 这两个条件必须同时满足,主节点才允许写入。
  • 如果从节点延迟过大或掉线,就会拒绝写入,从而防止旧 Master 继续接收请求。
  • 比如我设置 min-slaves-to-write=1,min-slaves-max-lag=10,如果 Master 宕机超过 10 秒,它和从节点的延迟太大,就会自动停止写入,避免旧主节点在网络恢复后继续接收写操作。

能彻底解决脑裂吗?

Redis 无法完全避免脑裂

因为主从切换、网络恢复、延迟检测等都是时间敏感操作,在一些极端场景(比如主节点刚恢复但新主还没完全接管),仍然可能出现短暂的双主情况

小结

  • Redis 集群是有可能出现脑裂问题的,比如网络分区或主节点恢复不及时时,可能会出现两个 Master 同时写数据。
  • 这样会导致数据不一致甚至丢失。
  • Redis 可以通过配置 min-slaves-to-write 和 min-slaves-max-lag 来降低风险,它们能让旧 Master 在检测到延迟或断连时拒绝写入,从而避免错误写操作。
  • 但因为分布式延迟是不可避免的,Redis 只能尽量规避,不能彻底解决脑裂。

Redis 中如何实现分布式锁?

背景:小徐是一个程序员,他开发了一个秒杀功能,但出现了超卖问题。

  • 超卖问题的出现:多线程并发环境下,如果同时对一个共享资源进行读写,数据会出现错乱的问题

这一次他加了一个同步锁 synchronized,这次终于不会超卖了,那我们都知道当多线程并发情况下我们加了同步锁,在同一时刻保证只有一个线程能拿到锁,其他进程进来会进行一个互斥,需要排队等待需要等持有锁线程处理完释放锁

但是随着用户量越来越多小徐发现服务器压力越来越大,性能达到了瓶颈,于是小徐通过nginx进行了负载均衡,他将服务器进行了水平扩展,通过nginx进行了分布式集群部署,但是测试时吞吐量确实上来了,但是秒杀功能又出现了超卖问题,经过发现原来是同步锁的问题,因为同步锁它是JVM级别的,它只能锁住单个进程,但是经过分布式部署之后呢,每台服务器在并发的情况下只能锁住一个线程

所以要解决这个问题我们就要用到分布式锁,分布式锁,顾名思义,分布式锁就是分布式场景下的锁,比如多台不同服务器上的进程,去竞争同一项资源,就是分布式锁。

主流的分布式锁的解决方案有Redis和zookeeper

这里我们主要讲解Redis的分布式锁

1.我们先实现一个最简单的分布式锁

直接用Redis的setnx命令,这个命令的语法是:setnx key value如果key不存在,则会将key设置为value,并返回1;如果key存在,不会有任务影响,返回0。

基于这个特性,我们就可以用setnx实现加锁的目的:通过setnx加锁,加锁之后其他服务无法加锁,用完之后,再通过delete解锁。

但是一定要加过期时间,因为如果用户在请求的过程中,服务器挂了,那么其他的服务器正常请求时,就会出现一个阻塞的情况,因为其他服务器的线程通过setnx进行上锁的时候,发现这个键里面一直有值,就会永远不会上锁成功,之前挂掉的服务器它一直持有锁从而造成了一个死锁的现象,所以此时我们要加上一个过期时间进行兜底,经过这个时间后锁就会自动释放,从而不影响其他服务器的正常请求

二个问题的解决(锁续期与锁误删)以及Lua脚本

虽然业务的扩展,我们又发现了问题,当业务(线程1)的处理时间超过了这把锁的过期时间时,此时业务还没有处理完,锁就释放掉了,其他的线程(线程2)就会趁虚而入,线程1处理完业务后,回来释放锁,此时释放的就是线程2的锁,而其他的线程此时又会趁虚而入,以此类推。总结下来就是有两个问题:

  • 锁过期时线程还在处理业务当中
  • 存在线程1释放掉线程2的锁,即锁误删现象分布式锁需要满足谁申请谁释放原则,不能释放别人的锁,也就是说,分布式锁,是要有归属的。)

我们先来解决第一个问题-锁过期时线程还在处理业务当中怎么办呢

  • 我们可以加长锁的一个过期时间,并且我们还需要考虑到如果我加长的这个时间还是不够怎么办呢,我们增加一个兜底的方案,在业务代码当中我们添加一个子线程,每10秒去确认主线程是不是在线,如果在线则将过期时间重置,也就是将锁续期

我们再来解决第二个问题-锁误删现象怎么解决

  • 就是我们给锁增加一个唯一ID(UUID),这样就能保证每一把锁的它的KEY是绑定的自己的那一个线程,从而业务执行完毕后会先检查锁是不是自己的,最后进行释放。就不会释放其他线程的锁

Lua脚本

也就是说我们完整的流程是竞争者获取锁执行任务,执行完毕后检查锁是不是自己的,最后进行释放。但是执行完毕后,检查锁,再释放,这些操作如何保证它是原子化的操作呢?Redis还有个特性,专门整合原子操作,就是Lua脚本。

Lua脚本可以保证原子性,因为Redis会将Lua脚本封装成一个单独的事务,而这个单独的事务会在Redis客户端运行时,由Redis服务器自行处理并完成整个事务,如果在这个进程中有其他客户端请求的时候,Redis将会把它暂存起来,等到 Lua 脚本处理完毕后,才会再把被暂存的请求恢复。

但是我们发现如果我们自己实现锁误删和锁续期这些代码非常的麻烦,还要保证它的一个健壮性,所以Redis有没有提供相关的组件来完成这些功能呢,有的兄弟有的,这就是Redisson

Redisson原理

Redis提供了一个Redisson完成我们刚刚说到的功能,实现起来也非常的简单,只要添加Redisson相关的一个依赖,把Redisson的客户端自动装配起来通过lock.lock()就可以实现Redisson的分布式锁

img

我们来说说Redisson的原理是什么,同样的多个线程请求同一资源,当然只有一个线程才能获取这把锁,比如线程1获取到了这把锁,它的key呢就如我们刚刚所说,它用的是UUID+线程ID合并起来保证我们的key和当前线程绑定在一起,这样就不会出现锁误删的问题,当线程1获取锁成功去处理业务的时候,它内部会有一个看门狗机制,它呢会每隔10秒看一下当前线程是否还持有锁,如果持有的话就延长生存时间,从而给这把锁续命。如果我们实现了Redis的集群呢,它就会选择Redis当中的某一个集群

那如果没有获取到锁的话,线程就会不停的自旋尝试获取锁只到超时为止

以上就是Redisson的实现原理

红锁的实现

在Redis中如果使用了主从集群的一个模式,因为Redis采用的是AP模式,也就是它只能保证这个高可用和高性能,但是不能保证高一致性,当我们设置一个锁的时候它其实只会往一个节点去设置一个锁,设置完了就会立马告诉你设置成功,然后内部进行主从同步,如果我们把这个key设置到了主节点,我们主从同步的时候正好主节点挂了,从节点并没有同步到这把锁导致新的主节点也没有同步过来锁信息,客户端可能重新获取新的主节点的锁,出现了多客户端同时持锁,导致数据不一致的问题。(主从异步复制+主从切换可能出现“旧主没释放锁,新主也没这把锁”的锁丢失/多客户端同时持锁问题。)这个时候应该怎么做呢

其实Redis也提供了相应的解决方法,那就是红锁RedLock,Redlock 是 Redis 官方提出的分布式锁算法,通过在N个(通常为5)相互独立的 master 上同时加锁,并且拿到多数派的锁(≥ N/2+1) 才算成功,来提升锁的安全性和可用性。如果没有RedLock就如我们刚刚所说,我们往一个主节点去设置一个锁,设置完了就会立马响应设置成功,而不去管从节点是否完成了同步,我们使用了RedLock,它要保证你提供的多数派节点(5个主节点,其中要5/2+1,即4个)都存储完毕了,它才会给你响应设置完成,来提升锁的安全性和可用性

分布式锁在未完成逻辑前过期怎么办?

它内部会有一个看门狗机制,它呢会每隔10秒看一下当前线程是否还持有锁,如果持有的话就延长生存时间30s,从而给这把锁续命

Redis 的 Red Lock 是什么?你了解吗?

red lock是一种分布式锁的实现方案。解决分布式锁中主从同步的问题。
在Redis中如果使用了主从集群的一个模式,因为Redis采用的是AP模式,也就是它只能保证这个高可用和高性能,但是不能保证高一致性,当我们设置一个锁的时候它其实只会往一个节点去设置一个锁,设置完了就会立马告诉你设置成功,然后内部进行主从同步,如果我们把这个key设置到了主节点,我们主从同步的时候正好主节点挂了,从节点并没有同步到这把锁导致新的主节点也没有同步过来锁信息,客户端可能重新获取新的主节点的锁,出现了多客户端同时持锁,导致数据不一致的问题。(主从异步复制+主从切换可能出现“旧主没释放锁,新主也没这把锁”的锁丢失/多客户端同时持锁问题。)这个时候应该怎么做呢

其实Redis也提供了相应的解决方法,那就是红锁RedLock,Redlock 是 Redis 官方提出的分布式锁算法,通过在N个(通常为5)相互独立的 master 上同时加锁,并且拿到多数派的锁(≥ N/2+1) 才算成功,以此来提升锁的安全性和可用性。如果没有RedLock就如我们刚刚所说,我们往一个主节点去设置一个锁,设置完了就会立马响应设置成功,而不去管从节点是否完成了同步,我们使用了RedLock,它要保证你提供的多数派节点(5个主节点,其中要5/2+1,即4个)都存储完毕了,它才会给你响应设置完成,来提升锁的安全性和可用性

Redis 实现分布式锁时可能遇到的问题有哪些?

问题一和问题二 锁过期问题和锁误删问题

当业务(线程1)的处理时间超过了这把锁的过期时间时,此时业务还没有处理完,锁就释放掉了,其他的线程(线程2)就会趁虚而入,线程1处理完业务后,回来释放锁,此时释放的就是线程2的锁,而其他的线程此时又会趁虚而入,以此类推。总结下来就是有两个问题:

  • 锁过期时线程还在处理业务当中
  • 存在线程1释放掉线程2的锁,即锁误删现象分布式锁需要满足谁申请谁释放原则,不能释放别人的锁,也就是说,分布式锁,是要有归属的。)

我们先来解决第一个问题-锁过期时线程还在处理业务当中怎么办呢

  • 我们可以加长锁的一个过期时间,并且我们还需要考虑到如果我加长的这个时间还是不够怎么办呢,我们增加一个兜底的方案,在业务代码当中我们添加一个子线程,每10秒去确认主线程是不是在线,如果在线则将过期时间重置,也就是将锁续期

我们再来解决第二个问题-锁误删现象怎么解决

  • 就是我们给锁增加一个唯一ID(UUID),这样就能保证每一把锁的它的KEY是绑定的自己的那一个线程,从而业务执行完毕后会先检查锁是不是自己的,最后进行释放。就不会释放其他线程的锁

问题三 主从问题不同步问题

在Redis中如果使用了主从集群的一个模式,因为Redis采用的是AP模式,也就是它只能保证这个高可用和高性能,但是不能保证高一致性,当我们设置一个锁的时候它其实只会往一个节点去设置一个锁,设置完了就会立马告诉你设置成功,然后内部进行主从同步,如果我们把这个key设置到了主节点,我们主从同步的时候正好主节点挂了,从节点并没有同步到这把锁,导致新的主节点也没有同步过来锁信息,客户端可能重新获取新的主节点的锁,出现了多客户端同时持锁,导致数据不一致的问题。(主从异步复制+主从切换可能出现“旧主没释放锁,新主也没这把锁”的锁丢失/多客户端同时持锁问题。)这个时候应该怎么做呢

其实Redis也提供了相应的解决方法,那就是红锁RedLock,Redlock 是 Redis 官方提出的分布式锁算法,通过在N个(通常为5)相互独立的 master 上同时加锁,并且拿到多数派的锁(≥ N/2+1) 才算成功,来提升锁的安全性和可用性。如果没有RedLock就如我们刚刚所说,我们往一个主节点去设置一个锁,设置完了就会立马响应设置成功,而不去管从节点是否完成了同步,我们使用了RedLock,它要保证你提供的多数派节点(5个主节点,其中要5/2+1,即4个)都存储完毕了,它才会给你响应设置完成,来提升锁的安全性和可用性

问题四 单点故障问题

在使用单节点Redis实现分布式锁时,如果这个Redis实例挂掉,那么所有使用这个实例的客户端都会出现无法获取锁的情况。

解决方案就是引入集群模式,通过哨兵检测redis实例挂掉的情况,提升整个集群的可用性。

Redis 中的缓存击穿、缓存穿透和缓存雪崩是什么?

缓存击穿(Hot Key Breakdown)

  • 现象:一个热点 key 正好过期(或被删)瞬间,大量并发同时打到数据库,数据库瞬时压力飙升,可能导致数据库崩溃。
  • 触发点:集中在单个热点 key 且恰好失效
  • 解决方案
    • 查询前先对 key 加分布式锁/互斥,确保同一时间只有一个请求可以去数据库查询并更新缓存
    • 热点 key 不过期
    • Redis 中存逻辑过期时间,过期后先返回旧值,后台异步刷新,避免并发“一起回源”。

缓存穿透(Cache Penetration)

  • 现象:请求一个不存在的数据(恶意 id、非法参数等),Redis 没有,数据库也没有,每次都直接把请求打到 数据库,造成数据库负担加重。
  • 触发点:大量不存在 key 的查询。
  • 解决方案
    • 缓存空值:DB 查不到也把“空结果”缓存一小会(短 TTL),避免反复打 DB。
    • 布隆过滤器:提前把可能存在的 key 放到 Bloom Filter,不在集合里的直接拦截。
    • 黑名单/防刷:拦截异常访问。
    • 参数校验/鉴权/限流:源头减少无效/恶意请求。

缓存雪崩(Cache Avalanche)

  • 现象:大量 key 在同一时刻集中失效(或缓存节点整体不可用),大批请求直击数据库,造成系统整体“雪崩”。
  • 触发点:批量同时过期、缓存宕机/集群故障。
  • 解决方案
    • 错峰过期(TTL 加随机值):避免同一时间点集中过期。
    • 预热与分批加载:大促/新版本上线前把核心数据预热进缓存;大批 key 分段加载。
    • 降级/限流/熔断:保护 DB 与核心链路,必要时返回兜底数据
    • 多级缓存 + 读写隔离:本地/边缘缓存兜底,减少直击 DB。

总结:

  • 缓存击穿:是指单个热点 key 在失效瞬间被大量请求“打穿”,直接冲到数据库。
    • 解决方案包括:互斥锁/分布式锁 确保只有一个线程重建缓存;逻辑过期+异步刷新 保证旧值可返回、后台慢更新;以及 热点 key 不过期或使用多级缓存 来分担压力。
  • 缓存穿透:是指请求访问不存在的数据,缓存和数据库都没有,导致每次都查数据库。
    • 解决方案包括:布隆过滤器 拦截无效 key;缓存空值 防止反复请求 DB;同时可配合 黑名单机制、参数校验或限流策略,从源头阻断恶意流量。
  • 缓存雪崩:是指大量 key 同时失效缓存节点宕机,引发海量请求瞬间打爆数据库。
    • 解决方案包括:错峰过期TTL 加随机值) 防止集中失效;预热与分批加载 提前写入热点数据;配合 降级/熔断/限流机制 保护数据库;并通过 多级缓存架构本地+Redis) 减少直击 DB 的压力。

Redis 中如何保证缓存与数据库的数据一致性?

缓存和数据库的同步可以通过以下几种方式:

1)先更新缓存,再更新数据库

2) 先更新数据库存,再更新缓存

3) 先删除缓存,再更新数据库,后续等查询把数据库的数据回种到缓存中

以上三种不推荐,以下是业内3种比较常见的具体方案:

1)先更新数据库,再删除缓存,后续等查询把数据库的数据回种到缓存中

2)缓存双删策略。更新数据库之前,删除一次缓存;更新完数据库后,再进行一次延迟删除

3)使用 Binlog 异步更新缓存,监听数据库的 Binlog 变化,通过异步方式更新 Redis 缓存

补充:

为什么优先选择删缓存而不是更新

删除缓存相对于更新缓存来说,操作更简单,出错几率更低,且能更好地保证数据一致性。尽管可能会出现缓存击穿,但可以通过加锁等措施解决。所以,建议优先选择删除缓存。

先写数据库还是先删缓存?

1. 先删缓存

  • 优点

    • 如果删除缓存成功,但数据库更新失败,这种情况是可以接受的。缓存被清空但不会出现脏数据,只需要重试更新数据库即可。
  • 缺点

    • 这种方式可能会放大"读写并发"导致的数据不一致问题。因为删除缓存后,读线程无法命中缓存,直接去查询数据库。如果在此过程中数据库被更新,但缓存未及时更新,就会导致缓存被错误的数据覆盖,从而产生数据不一致。
    • 虽然这种情况发生的概率比较低(因为数据库查询速度快),但如果恰好发生,就会导致缓存中的数据始终错误,影响后续的查询结果。

2. 先写数据库

  • 优点

    • 先写数据库可以确保数据的可靠性和一致性,特别是数据库作为持久层存储,先写数据库能保证数据在持久层中的更新,即使缓存删除失败,也不会丢失数据。
    • 删除缓存失败的概率较低,只有在网络问题或缓存服务器宕机时才会失败。
  • 缺点

    • 如果先更新数据库再删除缓存,可能会导致数据一致性问题:数据库已更新,但缓存未删除,导致缓存中的数据仍为旧值,造成缓存与数据库的不一致。

3. 解决方案:延迟双删

  • 为了避免旧数据回写缓存,可以先删除缓存,更新数据库后再延迟删除缓存。延迟删除可以通过消息队列、定时任务等方式实现。
  • 缺点:延迟时间不易确定,且在延迟过程中可能出现脏数据,无法保证强一致性。

总结:

  • 如果需要优先保证数据一致性,可以选择先更新数据库后删缓存
  • 如果考虑到并发问题缓存击穿,可以选择先删缓存后写数据库,但需要注意可能引发的数据不一致问题

场景:

小业务量、低并发:可以选择先更新数据库后删除缓存,因为这种方式简单易行,适用于较低并发的场景。

大业务量、高并发:建议选择先删除缓存,并引入延迟双删、分布式锁等机制,以降低并发问题。这种方式较为复杂,但能更好地保证数据一致性。

优化方案 Binlog 异步更新缓存:

异步删除缓存:使用数据库的binlog或基于异步消息的订阅机制来异步删除缓存。这样,先更新数据库后,发送异步消息,再由监听器删除缓存,保证数据库与缓存一致性。

无需延迟双删:因为通过binlog监听机制可以可靠地重试删除缓存操作,相比写代码删除缓存更稳定。

完美方案:

  1. 先删除缓存
  2. 更新数据库
  3. 监听binlog异步删除缓存

面试总结:

在Redis中保证缓存和数据库的数据一致性主要面临“缓存击穿”、“缓存穿透”和“缓存雪崩”等问题。为了避免这些问题,常见的做法是使用“删除缓存”而不是“更新缓存”,因为删除缓存能减少并发带来的数据不一致性问题,同时也避免了更新缓存时可能出现的复杂操作和错误。

在选择“先写数据库还是先删缓存”时,通常推荐先删除缓存。这是因为,如果先删除缓存再更新数据库,万一更新数据库失败,也不会有脏数据的问题,只需重试即可。不过,先删缓存会引发“读写并发”的问题,尤其是缓存删除后,可能会发生缓存中的数据被读线程以“旧数据”覆盖的情况,从而造成数据不一致。

为了更好的处理这个问题,可以使用“延迟双删”策略,或者引入“binlog异步更新缓存”的方案。通过监听数据库的binlog,确保在数据库变更后,缓存能异步更新或删除。

如果面对高并发的业务场景,建议使用先删除缓存再更新数据库的方式,并配合binlog的异步监听机制来确保缓存和数据库的最终一致性。这种方法虽然较复杂,但能有效保证数据一致性和系统的高可用性。

img

Redis String 类型的底层实现是什么?(SDS)

什么是SDS

Redis 的 String 类型底层主要是通过 SDS(Simple Dynamic String,简单动态字符串)结构实现的,它是 Redis 自己封装的一种字符串结构,用来代替 C 语言中的 char*。
为什么要用 SDS:

C 的字符串有两个问题:

  • 第一,取字符串长度要遍历 \0,效率低;
  • 第二,容易因为扩容或拼接导致缓冲区溢出。

SDS 通过在结构体中记录字符串的 长度(len) 和 分配空间(alloc),就解决了这些问题。

SDS 的结构设计亮点:

SDS 头部包含 len、alloc、flags 三个字段,buf 存真正的字符数据,并且末尾仍然保留 \0,保持对 C API 的兼容。
它有几个版本,比如 sdshdr5/8/16/32/64,根据字符串长度选择不同大小的头部,用来优化内存占用。

SDS 的核心优势:

  • O(1) 获取长度:直接读 len 字段;
  • 自动扩容 + 预分配机制,避免多次 realloc;
  • 惰性释放,减少内存碎片;
  • 二进制安全,任意字节都能存;
  • 兼容 C 字符串,保留 \0。

扩展:String 的三种编码方式:

Redis 的 String 类型底层不止 SDS,还会根据数据类型采用三种编码:

  • int:保存整数,最节省内存;
  • embstr:短字符串(一般 ≤44B),redisObject 和 SDS 一起分配;
  • raw:较长或可变字符串,单独分配 SDS。

编码会根据内容和操作自动切换,比如 embstr 修改后会变成 raw。

所以总结一下:
Redis 的 String 底层是 SDS 动态字符串结构,相比 C 字符串,它支持动态扩容、O(1) 获取长度、二进制安全,解决了内存管理和性能问题;
同时配合 int、embstr、raw 三种编码,在性能和内存之间做了最优平衡。

如何使用 Redis 快速实现排行榜?

实现排行榜,用 Redis 最顺手的就是 ZSet(有序集合),因为它天生支持“分数 + 排序”。

  • 使用ZADD命令实现用户和分数插入
  • 使用ZRANK获取某个用户的排名
  • 使用ZREVRANGE按分数从高到低返回指定区间内的成员列表
  • 使用ZINCRBY实现分数的修改

在 xx项目 里,我用 Redis 的有序集合(ZSet)做排行榜,思路很简单:我这边一条榜单就是一个 ZSet,

  • member 存视频 ID,
  • score 存播放量或热度值。

比如:

  • 日榜:rank:video:play:2025-11-09
  • 周榜:rank:video:week:2025-W45
  • 总榜:rank:video:all

我怎么写入(实时更新分数)

前端产生“播放、点赞、评论、收藏”事件后,
我先把这些事件写进 Redis 队列(或者 RocketMQ)做削峰
然后后台消费者去执行 ZINCRBY对应的加分操作。

1
2
ZINCRBY rank:video:play:2025-11-09 1 <videoId>
ZINCRBY rank:video:hot:2025-11-09 <加权分> <videoId>
  • 播放一次加 1;

  • 热度榜是个加权计算(播放=1、点赞=3、评论=2、收藏=4…)。

    • 前端产生“播放/点赞/投币/评论”这些事件后,我先进 Redis 队列(List),后台MQ消费(削峰),然后对对应榜单执行 ZINCRBY
      • 播放一次:ZINCRBY rank:video:play:2025-11-09 1 <videoId>
      • 热度分:ZINCRBY rank:video:hot:2025-11-09 <加权分> <videoId>
  • 为了减少 Redis 的网络往返,我用 pipeline 或 Lua 脚本 批量执行自增

  • 日榜会 设个 TTL(比如 1 天自动过期),周榜、总榜我用定时任务或 ZUNIONSTORE 聚合。

我怎么查(接口轻量)

查询很简单:

  • Top N 排行榜
    ZREVRANGE key 0 9 WITHSCORES
    拿到 videoId 后再批量查详情(封面、标题等)。

  • 某个视频的排名
    ZREVRANK key videoId,结果从 0 开始我会 +1。

切榜维度只要换 key,比如从日榜切到周榜,一次查询毫秒级就能出结果。

小结

我用 ZSet 做排行榜:写入用 ZINCRBY 实时加分,查询用 ZREVRANGE/ZREVRANK 秒级出榜;榜单按时间拆 key 管理(天/周/总榜),再配合队列削峰、定时回写持久化,
整体又快又稳,还能灵活扩展权重逻辑,支持各种‘热度榜’玩法。

如何使用 Redis 快速实现布隆过滤器?

布隆过滤器是一种用来快速判断某个数据是否存在的概率型结构,要用它来防止缓存穿透

原理

它的核心是一个超大的 bit 数组,初始全是 0。
每次我们往过滤器里加一个元素时,会通过 多个哈希函数 算出几个位置,把对应的 bit 置为 1。

之后如果要判断某个元素是否存在:

  • 只要有一个 bit 位是 0,那它肯定不存在;
  • 如果全是 1,那就“可能存在”。

(因为有哈希碰撞,所以布隆过滤器会有一定误判率,但可以控制到很低)

img

工作流程

img
1️ 客户端发请求,Redis 先查缓存:

  • 命中 → 直接返回;
  • 未命中 → 去看布隆过滤器。

2️ 如果布隆过滤器判断“不存在”,那就直接拒绝访问数据库(防止穿透)。

3️ 如果“可能存在”,才会去查数据库,然后把结果重新写回缓存。

Redis 实现布隆过滤器的两种方式

方式一:用 Bitmap 位图 自己实现

  • SETBIT / GETBIT 操作,自己维护 bit 数组;
  • 用多个 hash 函数计算对应位置;
  • 每次新增元素就把对应 bit 置 1;
  • 判断时检测那几个位置是不是都为 1。

适合想轻量实现、或者布隆过滤器逻辑特别简单的场景。

方式二:用 RedisBloom 模块
Redis 官方提供了一个插件模块 RedisBloom,线上项目一般直接用 RedisBloom,因为它比自己写 Bitmap 省很多维护成本。

小结

布隆过滤器通过哈希+位图判断元素‘可能存在’,结合 Redis 实现特别高效。
在项目里用它放在缓存层前面:请求先查 Redis,再看布隆过滤器。不存在就直接拒绝,存在才放行查库。
这样能防止恶意请求穿透数据库,也能显著减轻后端压力。

Redis 字符串类型的最大值大小是多少?

最大512MB,但是不建议存储大于2MB的在字符串

Redis 性能瓶颈时如何处理?

我们要先搞清楚瓶颈在哪,然后再扩容、分流、再设计

能加机器就扩容,撑不住就主从 / 集群横向扩,最后再加多级缓存和限流降级。

如何在 Redis 中实现队列和栈数据结构?

实现队列(FIFO,先进先出):

Redis 中,可以使用 LPUSH 命令向队列的左侧添加元素,使用 RPOP 命令从队列的右侧弹出元素。确保最先加入的元素最先被移除。

实现栈(LIFO,后进先出):

Redis 中,可以使用 LPUSH 命令向栈的左侧推入元素,使用 LPOP 命令从栈的左侧移除元素。确保最后加入的元素最先被移除。

  • 队列 = List 的「一头进、一头出」:LPUSH + RPOP(或反之),需要阻塞就用 BLPOP/BRPOP。
  • 栈 = List 的「一头进、一头出,但在同一头」:LPUSH + LPOP。
  • 要优先级,就用 ZSet 把“分数 = 优先级”,配合 ZADD + ZPOPMIN/ZREVRANGE。

Redis 中的 Ziplist 和 Quicklist 数据结构的特点是什么?

Ziplist(压缩列表)
就是一种“紧凑型、连续内存结构”,它把所有元素都顺序挨着放在一块连续内存中。

  • 优点是非常节省空间,适合小数据量、少修改的场景,比如小哈希、小列表。
  • 缺点每个元素前面都要记录上一个元素的大小,如果插入或删除一个元素,就会涉及到后续元素的连锁更新,成本高。所以 Ziplist 不适合频繁插入、删除的情况。

Ziplist 更轻量、更节省内存,但不灵活。

Quicklist(快速列表)

Redis 后面为了优化 List 的性能,引入了 Quicklist。它其实是一个“双向链表 + Ziplist的结合体”。

每个链表节点里,不再直接放一个元素,而是放一个小的 Ziplist。
这样既能保持链表的灵活性(插删快),又能利用 Ziplist 的紧凑存储节省内存。

Quicklist 从 Redis 3.2 开始就是 List 的默认底层实现。

小结

Ziplist 是“小而省”的代表,适合小量、少变的数据;
Quicklist 是“灵活高效”的代表,用链表包着多个 Ziplist,既能省内存,又能快速插删。

Redis 事务与关系型数据库事务的主要区别是什么?

  • Redis 支持事务,Redis中的事务主要保证的是多个命令执行的原子性,即所有的命令在一个原子操作中执行,不会被打断。而不是MYSQL中的ACID
  • Redis中的事务是不支持回滚的

Redis Cluster 模式与 Sentinel 模式的区别是什么?

cluster提供了数据分片的功能,适合大数据量场景
sentinel没有数据分片功能,适合主从高可用,读写分离场景

说说 Redisson 分布式锁的原理?

多个线程请求同一资源,只有一个线程才能获取这把锁

  • redission通过lua脚本封装多个redis的命令来实现加锁和获取锁的原子性操作。
  • redission用的是UUID+线程ID合并起来保证我们的key和当前线程绑定在一起,这样就不会出现X线程1释放掉了线程2的锁这样的“锁误删”问题
  • redission内部会有一个看门狗机制,它会每隔10秒看一下当前线程是否还持有锁,如果持有的话就延长生存时间,从而给这把锁续命。

img

Redis Zset 的实现原理是什么?

ZSet 是啥?

ZSet 是一个带分数的有序集合:每个成员一个 score,按分数排序,还要保证成员唯一。

底层怎么实现?

  • 跳表(skiplist):
    • 是一个多层索引的链表,主要是通过多层链表来实现,底层链表保存所有元素,而每一层链表都是下一层的子集。
    • 按 score 排好序,做范围/排名查询很快,插入/删除/查找都是 O(log N)。
  • 哈希表(dict):用于存储member -> score 的映射,提供快速查找O(1)

小数据优化:当有序集合的元素个数小于 128 ,并且每个元素的值小于 64 字节时,用压缩列表(ziplist)存,更省内存;超出阈值就用 跳表 + dict(字典)。

为什么要跳表 + 哈希表?

  • 跳表解决按分数进行有序的、范围的/排行的这类查询;
  • 哈希表解决按成员秒查/改分数
  • 两者结合,查某个成员 O(1),插入/删除/范围查 O(log N),同时还能分页/TopN。

典型场景:

排行榜、Feed 排序、延时队列、区间检索(如按分数查一段范围的人)。

总结:

ZSet = 跳表(排序与范围)+ 哈希表(按成员 O(1) 定位),小集合用压缩结构省内存;增删改 O(logN)、按成员查 O(1)、范围 O(logN+M),特别适合做排行榜/区间查询。

为什么 Redis Zset 用跳表实现而不是红黑树?B+树?

Redis 里 Zset(有序集合)用的是跳表 + 哈希表 组合实现的,而不是红黑树或者 B+ 树,主要是从实现复杂度、性能和使用场景几个方面考虑的。

为什么不用红黑树?

  1. 跳表实现更简单,维护成本低

红黑树虽然查找快,但每次插入、删除都要做复杂的平衡操作;
跳表只用随机化层级,维护成本小得多。

  1. 跳表更适合范围查询
    Zset 很多操作都是范围查询,跳表可以直接顺序往后扫,非常高效。
  2. 跳表结构更灵活,扩展性好
    小数据层少一点,大数据层多一点,不像红黑树那样固定死结构。

为什么不用 B+ 树?

B+ 树虽然在磁盘数据库(比如 MySQL)里很强,但 Redis 是纯内存型数据库

  1. B+ 树节点太大,指针太多
    B+ 树为磁盘优化设计,一个节点会存很多 key 和指针。
    而 Redis 跳表节点只存一个 key 和几个指针(前进、回退),
    在内存中结构更轻、更紧凑,缓存命中率高。

  2. B+ 树更适合磁盘存储,不适合内存操作

B+ 树是为减少磁盘 I/O 而设计的;
Redis 是内存操作,随机访问不再是瓶颈,所以没必要用 B+ 树这种“大块结构”。

跳表更契合 Redis 的实现逻辑

在 Redis 的 Zset 里:

  • Hash 用来根据成员快速查分数;
  • SkipList 用来按分数排序、做范围查询。

这俩结构组合起来,既能快速定位,又能高效排序。
跳表比平衡树更自然地满足这种“双需求”场景。

小结

Redis 之所以选跳表,不是因为红黑树或 B+ 树不行,而是因为跳表结构更简单、性能稳定、支持范围查询、内存利用率高,并且和哈希表组合(Hash + SkipList)能完美满足 Zset 的需求。

Redisson 看门狗(watch dog)机制了解吗?

  • Redisson中的看门狗机制(watchdog)主要用来避免业务逻辑还未执行完毕,Redis中的锁就因为过了超时时间而释放了
  • 它通过定期向Redis发送命令更新锁的过期时间来实现自动续期,默认每10s发送一次请求,每次续期30s。
  • 当客户端主动释放锁时,Redisson会取消看门狗刷新操作。如果客户端宕机了,定时任务自然也就无法执行了,此时等超时时间到了,锁也会自动释放。

你在项目中使用的 Redis 客户端是什么?

我这个项目用的是Lettuce

  • 用的是 Spring Data Redis 默认的 Lettuce(Boot 2.x 起默认)。
  • 为什么选它
    • 和 Spring 无缝集成,开箱即用,用 RedisTemplate 就能搞定缓存、验证码、token、队列、在线人数等场景。
    • 基于 Netty,线程安全、单连接多路复用,适合我们这种并发量不小的读写;相比老的 Jedis 不需要连接池,资源占用更稳。
    • 天然支持哨兵/集群、异步/Reactive,用起来省心,线上故障重连也更友好。
  • 没选 Jedis/Redisson 的原因
    • Jedis 更适合简单/单线程场景,我们需要高并发和异步能力。
    • Redisson偏“分布式对象/锁”套件,我们当前主要是KV/自增/过期事件这类操作,Lettuce + Spring Data Redis 已够用;若后续重度用分布式锁/延迟队列,再引 Redisson 不迟。

如果发现 Redis 内存溢出了?你会怎么做?请给出排查思路和解决方案

如果线上 Redis 内存溢出,我的第一反应是:先扩容,保证业务不受影响;然后再排查根因,做长期优化

一、第一步:止血(确保服务正常)

如果错误提示是 used memory > maxmemory,说明 Redis 达到最大内存限制,继续写入被拒绝。

所以第一时间我会:

  • 扩容 Redis 实例内存 或
  • 增加新的 Redis 节点分担压力
  • 先保证线上业务不挂,再排查根因。

二、第二步:排查 Redis 内存为什么涨满?

常见的几种情况

  1. 数据量太多
  • 存入 Redis 的 key/数据本身太大,超过内存容量。
  1. 大量 key 没有设置过期时间
  • 数据长期堆积不释放,导致内存持续增长。
  1. 大对象 / 大型数据结构滥用
  • 比如 hash、list、set 里塞了大量内容,占用极大空间。
  1. 持久化策略影响
  • RDB fork 时会额外占一份内存(写时复制机制),高峰容易炸。
  1. 业务代码误用 Redis 当数据库
  • 比如把日志、明细表全写进 Redis。

第三步:解决方案(短期 + 长期)

(1)短期优化

  • 启用合适的淘汰策略
    如 allkeys-lru、volatile-lru,让 Redis 自动删除不常用 key。

  • 为 key 设置合理的 TTL
    避免无过期数据永远占内存。

(2)长期优化

  • 优化数据结构设计

    • 大 hash 拆分成小 hash,减少单个结构的内存占用。
  • 水平扩展 Redis

    • 分库分槽,将数据打散到多个 Redis 节点降低单机压力。
  • 优化持久化策略

    • 减少 RDB 在高峰期触发并导致额外内存开销。

下面是 30 秒极速口语版,面试时直接说就能拿高分👇

小结

如果发现 Redis 内存溢出,我的第一反应是先扩容保证业务不挂,然后再排查原因。

常见根因主要有三类:

  1. 数据太多:写入的数据量超出 Redis 内存限制。
  2. 无过期时间:大量 key 没 TTL,越积越多。
  3. 大对象滥用:比如一个 hash、list、set 塞几十万条数据。

解决也很简单:

  • 开启合适的淘汰策略(allkeys-lru 等)
  • 给 key 设置合理 TTL
  • 拆分大对象,优化数据结构
  • 必要时做 Redis 水平扩容(分片/集群)
  • 优化 RDB/AOF 配置,避免高峰期 fork 占用额外内存

一句话总结:

先扩容止血,再找无 TTL、大对象、数据堆积等根因;最后通过淘汰策略、TTL、拆分数据结构和扩容彻底解决。