Redis深入浅出

一、知识点总览

1.1 Redis 官方定义与核心定位

Redis(REmote DIctionary Server) 是一款开源、基于内存运行、支持持久化的高性能 Key-Value 型 NoSQL 数据库,使用 ANSI C 语言编写,遵循 BSD 协议。其官方定义为:

"Redis is an open source (BSD licensed), in-memory data structure store, used as a database, cache, message broker, and streaming engine."

即 Redis 是一个内存数据结构存储系统,可同时承担四类角色:

角色 说明 典型场景
数据库 支持持久化、事务、多数据结构 轻量级业务主数据存储、会话存储
缓存 亚毫秒级读写,降低后端数据库压力 热点数据缓存、接口结果缓存
消息中间件 Pub/Sub、Stream 数据结构 轻量消息通知、日志流处理
流处理引擎 Stream 支持消费者组与 ACK 机制 事件溯源、订单流水处理

核心特性:

  1. 极致性能:单机读写 QPS 可达 10 万+,读操作平均耗时微秒级;
  2. 丰富数据结构:String、Hash、List、Set、ZSet 五大基础类型,外加 Bitmap、HyperLogLog、GEO、Stream 等扩展类型;
  3. 持久化能力:RDB 快照 + AOF 命令日志,支持混合持久化;
  4. 高可用架构:主从复制、Sentinel 哨兵、Cluster 集群三级演进;
  5. 原子性保证:单线程命令处理模型 + Lua 脚本,天然规避并发竞争;
  6. 功能丰富:事务、发布订阅、分布式锁、限流、布隆过滤器(RedisBloom 模块)等。

1.2 Redis 为什么快(核心原因总览)

这是面试最高频的总览性问题,标准答案包含五个层面:

  1. 纯内存操作:数据存储在内存中,读写延迟纳秒级,无磁盘 I/O 瓶颈;
  2. 单线程命令处理模型(Redis 6.0 前):无锁、无上下文切换、无竞态条件,命令执行天然原子;
  3. I/O 多路复用:基于 epoll/kqueue 的事件驱动模型(ae 事件库),单线程高效处理海量连接;
  4. 高效底层数据结构:SDS、跳表、压缩列表、哈希表等针对性优化的结构;
  5. 6.0 引入多线程 I/O:网络读写与协议解析多线程化,命令执行仍单线程,进一步提升网络吞吐。

1.3 知识点可视化图例

(1)Redis 整体架构图

#mermaid-svg-wlb5KoxdpJLCU4Vk{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-wlb5KoxdpJLCU4Vk .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-wlb5KoxdpJLCU4Vk .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-wlb5KoxdpJLCU4Vk .error-icon{fill:#552222;}#mermaid-svg-wlb5KoxdpJLCU4Vk .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-wlb5KoxdpJLCU4Vk .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-wlb5KoxdpJLCU4Vk .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-wlb5KoxdpJLCU4Vk .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-wlb5KoxdpJLCU4Vk .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-wlb5KoxdpJLCU4Vk .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-wlb5KoxdpJLCU4Vk .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-wlb5KoxdpJLCU4Vk .marker{fill:#333333;stroke:#333333;}#mermaid-svg-wlb5KoxdpJLCU4Vk .marker.cross{stroke:#333333;}#mermaid-svg-wlb5KoxdpJLCU4Vk svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-wlb5KoxdpJLCU4Vk p{margin:0;}#mermaid-svg-wlb5KoxdpJLCU4Vk .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-wlb5KoxdpJLCU4Vk .cluster-label text{fill:#333;}#mermaid-svg-wlb5KoxdpJLCU4Vk .cluster-label span{color:#333;}#mermaid-svg-wlb5KoxdpJLCU4Vk .cluster-label span p{background-color:transparent;}#mermaid-svg-wlb5KoxdpJLCU4Vk .label text,#mermaid-svg-wlb5KoxdpJLCU4Vk span{fill:#333;color:#333;}#mermaid-svg-wlb5KoxdpJLCU4Vk .node rect,#mermaid-svg-wlb5KoxdpJLCU4Vk .node circle,#mermaid-svg-wlb5KoxdpJLCU4Vk .node ellipse,#mermaid-svg-wlb5KoxdpJLCU4Vk .node polygon,#mermaid-svg-wlb5KoxdpJLCU4Vk .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-wlb5KoxdpJLCU4Vk .rough-node .label text,#mermaid-svg-wlb5KoxdpJLCU4Vk .node .label text,#mermaid-svg-wlb5KoxdpJLCU4Vk .image-shape .label,#mermaid-svg-wlb5KoxdpJLCU4Vk .icon-shape .label{text-anchor:middle;}#mermaid-svg-wlb5KoxdpJLCU4Vk .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-wlb5KoxdpJLCU4Vk .rough-node .label,#mermaid-svg-wlb5KoxdpJLCU4Vk .node .label,#mermaid-svg-wlb5KoxdpJLCU4Vk .image-shape .label,#mermaid-svg-wlb5KoxdpJLCU4Vk .icon-shape .label{text-align:center;}#mermaid-svg-wlb5KoxdpJLCU4Vk .node.clickable{cursor:pointer;}#mermaid-svg-wlb5KoxdpJLCU4Vk .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-wlb5KoxdpJLCU4Vk .arrowheadPath{fill:#333333;}#mermaid-svg-wlb5KoxdpJLCU4Vk .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-wlb5KoxdpJLCU4Vk .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-wlb5KoxdpJLCU4Vk .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-wlb5KoxdpJLCU4Vk .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-wlb5KoxdpJLCU4Vk .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-wlb5KoxdpJLCU4Vk .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-wlb5KoxdpJLCU4Vk .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-wlb5KoxdpJLCU4Vk .cluster text{fill:#333;}#mermaid-svg-wlb5KoxdpJLCU4Vk .cluster span{color:#333;}#mermaid-svg-wlb5KoxdpJLCU4Vk div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-wlb5KoxdpJLCU4Vk .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-wlb5KoxdpJLCU4Vk rect.text{fill:none;stroke-width:0;}#mermaid-svg-wlb5KoxdpJLCU4Vk .icon-shape,#mermaid-svg-wlb5KoxdpJLCU4Vk .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-wlb5KoxdpJLCU4Vk .icon-shape p,#mermaid-svg-wlb5KoxdpJLCU4Vk .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-wlb5KoxdpJLCU4Vk .icon-shape .label rect,#mermaid-svg-wlb5KoxdpJLCU4Vk .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-wlb5KoxdpJLCU4Vk .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-wlb5KoxdpJLCU4Vk .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-wlb5KoxdpJLCU4Vk :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 高可用层
持久化层
Redis Server 服务端
客户端层
TCP + RESP 协议
fork 子进程
追加写入
Java / Go / Python 客户端

Jedis · Lettuce · go-redis
RESP 协议解析层
命令分发与执行层

单线程事件循环 aeEventLoop
数据结构实现层

SDS / dict / skiplist / listpack / intset
内存管理层

zmalloc + jemalloc
RDB 快照文件
AOF 命令日志
主从复制 Replication
哨兵 Sentinel
Cluster 集群

图注:客户端命令经 RESP 协议解析后进入单线程事件循环,命令在数据结构层落地执行;持久化与高可用能力以"旁路"形式挂载,不影响主链路低延迟。

(2)一条命令的完整执行流程图

#mermaid-svg-7KLUQ6cNuLFTuqCq{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-7KLUQ6cNuLFTuqCq .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-7KLUQ6cNuLFTuqCq .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-7KLUQ6cNuLFTuqCq .error-icon{fill:#552222;}#mermaid-svg-7KLUQ6cNuLFTuqCq .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-7KLUQ6cNuLFTuqCq .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-7KLUQ6cNuLFTuqCq .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-7KLUQ6cNuLFTuqCq .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-7KLUQ6cNuLFTuqCq .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-7KLUQ6cNuLFTuqCq .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-7KLUQ6cNuLFTuqCq .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-7KLUQ6cNuLFTuqCq .marker{fill:#333333;stroke:#333333;}#mermaid-svg-7KLUQ6cNuLFTuqCq .marker.cross{stroke:#333333;}#mermaid-svg-7KLUQ6cNuLFTuqCq svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-7KLUQ6cNuLFTuqCq p{margin:0;}#mermaid-svg-7KLUQ6cNuLFTuqCq .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-7KLUQ6cNuLFTuqCq .cluster-label text{fill:#333;}#mermaid-svg-7KLUQ6cNuLFTuqCq .cluster-label span{color:#333;}#mermaid-svg-7KLUQ6cNuLFTuqCq .cluster-label span p{background-color:transparent;}#mermaid-svg-7KLUQ6cNuLFTuqCq .label text,#mermaid-svg-7KLUQ6cNuLFTuqCq span{fill:#333;color:#333;}#mermaid-svg-7KLUQ6cNuLFTuqCq .node rect,#mermaid-svg-7KLUQ6cNuLFTuqCq .node circle,#mermaid-svg-7KLUQ6cNuLFTuqCq .node ellipse,#mermaid-svg-7KLUQ6cNuLFTuqCq .node polygon,#mermaid-svg-7KLUQ6cNuLFTuqCq .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-7KLUQ6cNuLFTuqCq .rough-node .label text,#mermaid-svg-7KLUQ6cNuLFTuqCq .node .label text,#mermaid-svg-7KLUQ6cNuLFTuqCq .image-shape .label,#mermaid-svg-7KLUQ6cNuLFTuqCq .icon-shape .label{text-anchor:middle;}#mermaid-svg-7KLUQ6cNuLFTuqCq .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-7KLUQ6cNuLFTuqCq .rough-node .label,#mermaid-svg-7KLUQ6cNuLFTuqCq .node .label,#mermaid-svg-7KLUQ6cNuLFTuqCq .image-shape .label,#mermaid-svg-7KLUQ6cNuLFTuqCq .icon-shape .label{text-align:center;}#mermaid-svg-7KLUQ6cNuLFTuqCq .node.clickable{cursor:pointer;}#mermaid-svg-7KLUQ6cNuLFTuqCq .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-7KLUQ6cNuLFTuqCq .arrowheadPath{fill:#333333;}#mermaid-svg-7KLUQ6cNuLFTuqCq .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-7KLUQ6cNuLFTuqCq .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-7KLUQ6cNuLFTuqCq .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-7KLUQ6cNuLFTuqCq .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-7KLUQ6cNuLFTuqCq .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-7KLUQ6cNuLFTuqCq .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-7KLUQ6cNuLFTuqCq .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-7KLUQ6cNuLFTuqCq .cluster text{fill:#333;}#mermaid-svg-7KLUQ6cNuLFTuqCq .cluster span{color:#333;}#mermaid-svg-7KLUQ6cNuLFTuqCq div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-7KLUQ6cNuLFTuqCq .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-7KLUQ6cNuLFTuqCq rect.text{fill:none;stroke-width:0;}#mermaid-svg-7KLUQ6cNuLFTuqCq .icon-shape,#mermaid-svg-7KLUQ6cNuLFTuqCq .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-7KLUQ6cNuLFTuqCq .icon-shape p,#mermaid-svg-7KLUQ6cNuLFTuqCq .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-7KLUQ6cNuLFTuqCq .icon-shape .label rect,#mermaid-svg-7KLUQ6cNuLFTuqCq .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-7KLUQ6cNuLFTuqCq .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-7KLUQ6cNuLFTuqCq .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-7KLUQ6cNuLFTuqCq :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 内存超限
校验通过
客户端发送命令
socket 可读事件触发
读取请求数据 readQueryFromClient
RESP 协议解析为 argv
命令表查找 processCommand
前置校验
执行内存淘汰策略
call 执行命令
更新内存数据结构
传播命令: AOF 缓冲 / 从库复制缓冲
写回响应 addReply

图注:理解"执行 → 传播 → 响应"三段式链路,是掌握持久化与主从复制原理的基础。

1.4 同类型技术横向对比

选取与 Redis 功能、用途、场景高度相似的 Memcached 与阿里 Tair 进行六维度对标:

对比维度 Redis Memcached Tair(阿里)
核心定位 内存数据结构服务器,兼顾持久化与高可用 纯内存 KV 缓存,极简设计 分布式 KV 存储,支持多引擎(MDB/LDB/RDB)
技术特性 五大+扩展数据类型、事务、Lua、Pub/Sub、Stream、RDB/AOF 持久化 仅 String 类型、无持久化、多线程模型 多引擎可选、自动数据迁移、双机房容灾
性能优劣 单线程模型,单核性能强;6.0 多线程 I/O 后网络吞吐提升;复杂数据结构操作耗时略高 多线程充分利用多核,简单 KV 场景吞吐略优 原生分布式,水平扩展能力强,运维成本高
适用场景 缓存、排行榜、分布式锁、会话、计数、消息流等复合场景 简单 KV 缓存、页面片段缓存 超大规模、需要自动分片与多副本的企业级场景
局限性 单实例内存受限,Cluster 对多键事务/Lua 有 slot 约束 无持久化、无数据类型、无原生高可用方案 社区生态弱,深度绑定阿里云体系
选型优先级 绝大多数互联网业务首选,功能全面、生态成熟 仅需简单缓存且追求多核利用率时备选 阿里云生态内超大规模场景优先

选型结论 :Redis 的差异化优势在于"数据结构丰富度 + 持久化 + 完整高可用方案"三位一体,是缓存与内存计算场景的事实标准;Memcached 仅在极简 KV 缓存场景有微弱优势;Tair 属于云厂商定制方案。


二、子知识点分类讲解

本章对 Redis 核心子知识点逐一深度拆解:数据结构与底层实现 → 持久化 → 过期与淘汰 → 缓存三大问题 → 分布式锁 → 主从复制 → 哨兵 → Cluster → 事务与 Lua → 发布订阅与 Stream。


2.1 五大基础数据类型与底层数据结构

2.1.1 子知识点概述

Redis 对外暴露 String、Hash、List、Set、ZSet 五大基础类型,但类型(type)与编码(encoding)是两个维度:同一类型会根据数据规模自动选择不同底层编码,以在"内存占用"与"操作性能"之间取得平衡。这是 Redis 内存效率的核心设计,也是面试深挖的第一重点。

类型与编码映射关系(Redis 7.x):

对外类型 可选编码 编码切换条件
String int / embstr / raw 整数值可存 long(64位有符号整数);≤44 字节 → embstr(SDS嵌入式);>44 字节 → raw(SDS)
Hash listpack / hashtable 元素数 >128 或值 >64 字节 → hashtable
List quicklist 统一使用 quicklist(双向链表串联 listpack)
Set intset / listpack / hashtable 纯整数且 ≤128 个 → intset;否则视规模升级
ZSet listpack / skiplist+dict 元素数 >128 或成员 >64 字节 → 跳表+字典

面试重点:单向升级,不会降级 :一旦从小结构(listpack/intset)转为 hashtable/skiplist/raw,数据减少后不会自动变回紧凑编码,只能删 key 重建才会恢复。embstr 只读:只要对 embstr 字符串做修改(append、setrange),redis 会先拷贝成 raw 再修改。

底层核心结构依赖关系:
#mermaid-svg-0006XgHXanQDt6hW{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-0006XgHXanQDt6hW .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-0006XgHXanQDt6hW .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-0006XgHXanQDt6hW .error-icon{fill:#552222;}#mermaid-svg-0006XgHXanQDt6hW .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-0006XgHXanQDt6hW .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-0006XgHXanQDt6hW .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-0006XgHXanQDt6hW .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-0006XgHXanQDt6hW .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-0006XgHXanQDt6hW .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-0006XgHXanQDt6hW .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-0006XgHXanQDt6hW .marker{fill:#333333;stroke:#333333;}#mermaid-svg-0006XgHXanQDt6hW .marker.cross{stroke:#333333;}#mermaid-svg-0006XgHXanQDt6hW svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-0006XgHXanQDt6hW p{margin:0;}#mermaid-svg-0006XgHXanQDt6hW .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-0006XgHXanQDt6hW .cluster-label text{fill:#333;}#mermaid-svg-0006XgHXanQDt6hW .cluster-label span{color:#333;}#mermaid-svg-0006XgHXanQDt6hW .cluster-label span p{background-color:transparent;}#mermaid-svg-0006XgHXanQDt6hW .label text,#mermaid-svg-0006XgHXanQDt6hW span{fill:#333;color:#333;}#mermaid-svg-0006XgHXanQDt6hW .node rect,#mermaid-svg-0006XgHXanQDt6hW .node circle,#mermaid-svg-0006XgHXanQDt6hW .node ellipse,#mermaid-svg-0006XgHXanQDt6hW .node polygon,#mermaid-svg-0006XgHXanQDt6hW .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-0006XgHXanQDt6hW .rough-node .label text,#mermaid-svg-0006XgHXanQDt6hW .node .label text,#mermaid-svg-0006XgHXanQDt6hW .image-shape .label,#mermaid-svg-0006XgHXanQDt6hW .icon-shape .label{text-anchor:middle;}#mermaid-svg-0006XgHXanQDt6hW .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-0006XgHXanQDt6hW .rough-node .label,#mermaid-svg-0006XgHXanQDt6hW .node .label,#mermaid-svg-0006XgHXanQDt6hW .image-shape .label,#mermaid-svg-0006XgHXanQDt6hW .icon-shape .label{text-align:center;}#mermaid-svg-0006XgHXanQDt6hW .node.clickable{cursor:pointer;}#mermaid-svg-0006XgHXanQDt6hW .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-0006XgHXanQDt6hW .arrowheadPath{fill:#333333;}#mermaid-svg-0006XgHXanQDt6hW .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-0006XgHXanQDt6hW .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-0006XgHXanQDt6hW .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-0006XgHXanQDt6hW .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-0006XgHXanQDt6hW .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-0006XgHXanQDt6hW .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-0006XgHXanQDt6hW .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-0006XgHXanQDt6hW .cluster text{fill:#333;}#mermaid-svg-0006XgHXanQDt6hW .cluster span{color:#333;}#mermaid-svg-0006XgHXanQDt6hW div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-0006XgHXanQDt6hW .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-0006XgHXanQDt6hW rect.text{fill:none;stroke-width:0;}#mermaid-svg-0006XgHXanQDt6hW .icon-shape,#mermaid-svg-0006XgHXanQDt6hW .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-0006XgHXanQDt6hW .icon-shape p,#mermaid-svg-0006XgHXanQDt6hW .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-0006XgHXanQDt6hW .icon-shape .label rect,#mermaid-svg-0006XgHXanQDt6hW .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-0006XgHXanQDt6hW .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-0006XgHXanQDt6hW .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-0006XgHXanQDt6hW :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} ptr 指向
redisObject
具体编码结构
dict 字典/哈希表(hashtable)
SDS 简单动态字符串(embstr / raw)
skiplist 跳表
listpack 压缩列表
quicklist 快速列表
intset 整数集合

图注 :所有键值对都封装为 redisObject,其中 type 字段标识逻辑类型、encoding 字段标识底层编码,ptr 指向实际数据结构。

2.1.2 完整运行原理

(1)redisObject 统一对象模型

每个键值对都由 redisObject 封装(源码 server.h):

c 复制代码
typedef struct redisObject {
    unsigned type:4;      // 逻辑类型: OBJ_STRING / OBJ_HASH / OBJ_LIST / OBJ_SET / OBJ_ZSET
    unsigned encoding:4;  // 底层编码: OBJ_ENCODING_RAW / EMBSTR / INT / HT / SKIPLIST...
    unsigned lru:24;      // LRU/LFU 访问时间戳, 供内存淘汰使用
    int refcount;         // 引用计数, 用于对象共享与释放
    void *ptr;            // 指向底层数据结构的指针
} robj;

(2)SDS------所有字符串的基石

Redis 没有直接使用 C 字符串,而是自研 SDS,解决 C 字符串三大缺陷:

c 复制代码
// sds.h (Redis 3.2+ 二进制安全头)
struct __attribute__ ((__packed__)) sdshdr8 {
    uint8_t len;        // 已使用长度 → O(1) 获取字符串长度
    uint8_t alloc;      // 总分配容量(不含头和结尾空字符) → 空间预分配依据
    unsigned char flags;// 类型标志位, 低3位区分 sdshdr5/8/16/32/64
    char buf[];         // 实际数据区, 二进制安全
};

SDS 核心设计考量:

  • O(1) 获取长度 :C 字符串需遍历到 \0(O(N)),SDS 直接读 len;
  • 杜绝缓冲区溢出 :拼接前检查 alloc,不足则自动扩容;
  • 空间预分配 + 惰性释放 :修改后若 len < 1MB 则多分配等量空间,减少连续修改的内存重分配次数;
  • 二进制安全 :以 len 判断结尾而非 \0,可存图片、序列化对象;
  • 五种头类型 :按字符串长度选择最小头部,短字符串节省大量内存(sdshdr5 甚至不存 alloc)。

(3)dict------渐进式 rehash 的哈希表

c 复制代码
typedef struct dict {
    dictType *type;       // 类型相关的操作函数表
    void *privdata;
    dictht ht[2];         // 两张哈希表: ht[0] 日常使用, ht[1] rehash 时使用
    long rehashidx;       // rehash 进度, -1 表示未进行
    ...
} dict;

typedef struct dictht {
    dictEntry **table;    // 桶数组, 大小恒为 2 的幂
    unsigned long size;
    unsigned long sizemask; // = size - 1, 用位运算取模加速定位
    unsigned long used;   // 已有节点数
} dictht;

typedef struct dictEntry {
    void *key;
    union { void *val; uint64_t u64; int64_t s64; double d; } v;
    struct dictEntry *next; // 链地址法解决哈希冲突
} dictEntry;

渐进式 rehash 执行链路:

  1. 触发扩容 :负载因子 used/size ≥ 1(无 BGSAVE 时)或 used/size > 5(BGSAVE 期间,避免 COW 内存翻倍),见 dict.c 中 dictResize 判断;
  2. 分配 ht[1](#分配 ht1 "citation") :大小为第一个 ≥ used*2 的 2^n;
  3. 分步迁移 :rehashidx 从 0 开始,每次对 dict 的增删改查操作顺带迁移一个桶 ,同时后台定时任务(databasesCron)每 100ms 迁移 1ms 的桶;
  4. 双表查询 :rehash 期间先查 ht[0] 再查 ht[1],新增一律写入 ht[1],保证 ht[0] 只减不增;
  5. 完成 :迁移完毕释放 ht[0],将 ht[1] 设为 ht[0],rehashidx 置 -1。

设计考量 :把 O(N) 的一次性 rehash 均摊到每次操作中,避免主线程长时间阻塞------这是"渐进式"思想的经典范例。

(4)skiplist------ZSet 的排序引擎

c 复制代码
typedef struct zskiplistNode {
    sds ele;              // 成员字符串
    double score;         // 分值, 排序依据
    struct zskiplistNode *backward; // 后退指针, 支持反向遍历
    struct zskiplistLevel {
        struct zskiplistNode *forward; // 各层前进指针
        unsigned long span;            // 跨度, 用于 O(logN) 计算排名
    } level[];            // 柔性数组, 每节点层数随机
} zskiplistNode;

typedef struct zskiplist {
    struct zskiplistNode *header, *tail;
    unsigned long length;
    int level;            // 当前最高层数
} zskiplist;

插入流程(zslInsert,t_zset.c):

  1. 从最高层头节点出发逐层查找插入位置,沿途记录每层的"前驱节点"到 update[] 数组与 rank[] 数组;
  2. 调用 zslRandomLevel() 随机生成层数:每层以 1/4 概率晋升,上限 32 层;
  3. 若新层数高于当前最高层,补齐头节点的新层指针;
  4. 在各层前驱节点后插入新节点,更新 span 跨度与 backward 指针。

为什么用跳表而不用红黑树?

  • 范围查询天然高效:ZRANGEBYSCORE 找到起点后沿最底层链表顺序遍历即可,红黑树需中序遍历且涉及回溯;
  • 实现简单:插入删除只需局部改指针,无旋转重平衡;
  • 并发友好、内存可控:通过调整晋升概率即可平衡时间与空间;
  • 作者 antirez 原话:跳表实现不比平衡树复杂,且调试更友好。

(5)listpack / ziplist------小数据量的极致压缩

ziplist 是一段连续内存:zlbytes | zltail | zllen | entry... | zlend。每个 entry 记录 prevlen(前一节点长度,1 或 5 字节)+ encoding + data。其致命缺陷是 连锁更新(cascade update):某节点长度跨越 254 字节边界时,可能引发后续所有节点 prevlen 字段扩容,最坏 O(N²)。

Redis 6.2 起以 listpack 替代 ziplist:每个 entry 改为记录自身长度而非前节点长度,彻底消除连锁更新,同时保留连续内存、缓存友好的优势。7.0 起 Hash/ZSet 的小编码全面切换为 listpack。

(6)quicklist------List 的统一实现

c 复制代码
typedef struct quicklist {
    quicklistNode *head;   // 双向链表头
    quicklistNode *tail;
    unsigned long count;   // 所有 listpack 中元素总数
    unsigned long len;     // quicklistNode 节点数
    int fill : 16;         // 每个 listpack 的大小限制 (listpack-max-ziplist-size)
    unsigned int compress : 16; // 两端不压缩的节点数 (listpack-max-ziplist-entries)
} quicklist;

即"双向链表 × listpack"的混合结构:既保留链表 O(1) 头尾插入,又通过 listpack 连续内存减少碎片、提升缓存命中率。

2.1.3 源码深度剖析(编码自动升级)

以 Hash 为例,t_hash.c 中判断是否从小编码升级:

c 复制代码
// 向 listpack 编码的 Hash 添加元素前的检查
if (listpackLength(o->ptr) > server.hash_max_listpack_entries ||  // 元素数超 128
    value_len > server.hash_max_listpack_value) {                 // 单值超 64 字节
    // 转换为 hashtable 编码: 逐条搬迁旧元素
    o->encoding = OBJ_ENCODING_HT;
    o->ptr = dictCreate(&hashDictType, NULL);
    ...
}

关键设计考量 :小数据量用紧凑结构省内存(一个 10 字段的 Hash 用 listpack 可比 hashtable 省 5~10 倍内存),数据量增大后果断切换为 O(1)/O(logN) 结构保性能------编码只升不降(升级后即使删除元素也不会退回,避免频繁抖动)。

2.1.4 关键细节与易错点
  1. embstr 与 raw 的分界线是 44 字节 :redisObject(16B) + sdshdr8(3B) + 1B 结尾空字符 = 20B,jemalloc 以 64B 为内存类对齐,64-20=44;
  2. 编码升级不可逆:Hash 一旦转为 hashtable,删除元素后不会退回 listpack;
  3. List 已无 ziplist:7.0 起全部是 quicklist,网上"ziplist 实现的 List"说法已过时;
  4. ZSet 同时维护 dict + skiplist:dict 提供 O(1) 按成员查分值(ZSCORE),跳表提供范围查询,两者共享成员字符串指针,不重复占内存;
  5. OBJECT ENCODING key 命令可在线查看键的当前编码,是排查内存问题的利器;
  6. 整数共享池 :0~9999 的整数对象被全局共享(shared.integers),引用计数复用。
2.1.5 优劣势与能力边界
  • 优势:编码自适应设计在内存与性能间取得极佳平衡;底层结构针对性优化(渐进式 rehash、跳表范围查询);
  • 固有缺陷:编码升级阈值(如 128/64)需结合业务调优;大 key 场景下任何紧凑编码都会退化为大对象;
  • 适用场景:中小规模数据享受紧凑编码红利;超大规模数据需提前评估 hashtable/skiplist 内存开销;
  • 不适用:单键存储超大集合(百万级成员的 ZSet)------应拆分或使用专业组件。

2.2 持久化机制:RDB 与 AOF

2.2.1 子知识点概述

Redis 内存数据必须通过持久化落盘,否则重启即数据全失。持久化是 Redis 区别于 Memcached 的核心能力,包含三种形态:

形态 机制 特点
RDB 某时刻全量内存快照(二进制) 恢复快、文件紧凑,但可能丢失最后一次快照后的数据
AOF 追加记录每条写命令 数据安全性高(最多丢 1 秒),文件大、恢复慢
混合持久化(4.0+) AOF 重写时先写 RDB 格式,再追加增量命令 兼顾恢复速度与数据安全,生产推荐
2.2.2 完整运行原理

(1)RDB 执行链路

触发方式:save(主线程阻塞执行,生产禁用)、bgsave(fork 子进程)、配置 save 900 1 等 savepoint 自动触发、主从全量复制时主节点自动执行。

bgsave 完整流程:
#mermaid-svg-jD3QUQwQUfN5WYSe{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-jD3QUQwQUfN5WYSe .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-jD3QUQwQUfN5WYSe .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-jD3QUQwQUfN5WYSe .error-icon{fill:#552222;}#mermaid-svg-jD3QUQwQUfN5WYSe .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-jD3QUQwQUfN5WYSe .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-jD3QUQwQUfN5WYSe .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-jD3QUQwQUfN5WYSe .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-jD3QUQwQUfN5WYSe .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-jD3QUQwQUfN5WYSe .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-jD3QUQwQUfN5WYSe .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-jD3QUQwQUfN5WYSe .marker{fill:#333333;stroke:#333333;}#mermaid-svg-jD3QUQwQUfN5WYSe .marker.cross{stroke:#333333;}#mermaid-svg-jD3QUQwQUfN5WYSe svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-jD3QUQwQUfN5WYSe p{margin:0;}#mermaid-svg-jD3QUQwQUfN5WYSe .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-jD3QUQwQUfN5WYSe .cluster-label text{fill:#333;}#mermaid-svg-jD3QUQwQUfN5WYSe .cluster-label span{color:#333;}#mermaid-svg-jD3QUQwQUfN5WYSe .cluster-label span p{background-color:transparent;}#mermaid-svg-jD3QUQwQUfN5WYSe .label text,#mermaid-svg-jD3QUQwQUfN5WYSe span{fill:#333;color:#333;}#mermaid-svg-jD3QUQwQUfN5WYSe .node rect,#mermaid-svg-jD3QUQwQUfN5WYSe .node circle,#mermaid-svg-jD3QUQwQUfN5WYSe .node ellipse,#mermaid-svg-jD3QUQwQUfN5WYSe .node polygon,#mermaid-svg-jD3QUQwQUfN5WYSe .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-jD3QUQwQUfN5WYSe .rough-node .label text,#mermaid-svg-jD3QUQwQUfN5WYSe .node .label text,#mermaid-svg-jD3QUQwQUfN5WYSe .image-shape .label,#mermaid-svg-jD3QUQwQUfN5WYSe .icon-shape .label{text-anchor:middle;}#mermaid-svg-jD3QUQwQUfN5WYSe .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-jD3QUQwQUfN5WYSe .rough-node .label,#mermaid-svg-jD3QUQwQUfN5WYSe .node .label,#mermaid-svg-jD3QUQwQUfN5WYSe .image-shape .label,#mermaid-svg-jD3QUQwQUfN5WYSe .icon-shape .label{text-align:center;}#mermaid-svg-jD3QUQwQUfN5WYSe .node.clickable{cursor:pointer;}#mermaid-svg-jD3QUQwQUfN5WYSe .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-jD3QUQwQUfN5WYSe .arrowheadPath{fill:#333333;}#mermaid-svg-jD3QUQwQUfN5WYSe .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-jD3QUQwQUfN5WYSe .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-jD3QUQwQUfN5WYSe .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-jD3QUQwQUfN5WYSe .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-jD3QUQwQUfN5WYSe .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-jD3QUQwQUfN5WYSe .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-jD3QUQwQUfN5WYSe .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-jD3QUQwQUfN5WYSe .cluster text{fill:#333;}#mermaid-svg-jD3QUQwQUfN5WYSe .cluster span{color:#333;}#mermaid-svg-jD3QUQwQUfN5WYSe div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-jD3QUQwQUfN5WYSe .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-jD3QUQwQUfN5WYSe rect.text{fill:none;stroke-width:0;}#mermaid-svg-jD3QUQwQUfN5WYSe .icon-shape,#mermaid-svg-jD3QUQwQUfN5WYSe .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-jD3QUQwQUfN5WYSe .icon-shape p,#mermaid-svg-jD3QUQwQUfN5WYSe .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-jD3QUQwQUfN5WYSe .icon-shape .label rect,#mermaid-svg-jD3QUQwQUfN5WYSe .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-jD3QUQwQUfN5WYSe .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-jD3QUQwQUfN5WYSe .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-jD3QUQwQUfN5WYSe :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 是
否
是
执行 BGSAVE 命令
是否已有子进程在运行?
拒绝执行
主进程 fork 子进程
子进程共享父进程内存页

写临时 RDB 文件
写完后 rename 替换旧 RDB
主进程继续服务客户端
写操作触发 COW?
内核复制被修改的内存页

子进程仍见旧数据

核心机制------写时复制(Copy-On-Write) :fork() 后父子进程共享物理内存页,仅当父进程修改某页时,内核才复制该页。因此 RDB 快照是 fork 瞬间的一致性视图,且额外内存开销与"快照期间被修改的页数量"成正比。

(2)AOF 执行链路

AOF 持久化分三步:命令追加 → 文件写入 → 文件同步(fsync)。

c 复制代码
// server.c: call() 执行命令后的传播逻辑
void call(client *c, int flags) {
    ...
    dirty += server.dirty - dirty_before; // dirty 记录自上次保存以来的写命令数
    ...
}
// 事件循环每轮末尾: beforeSleep() → flushAppendOnlyFile()
// 根据 appendfsync 策略决定是否调用 fsync 落盘

三种 fsync 策略(appendfsync):

策略 行为 数据安全 性能影响
always 每条命令立即 fsync 最多丢 1 条命令 每条命令一次磁盘写,最慢
everysec(推荐) 后台线程每秒 fsync 一次 最多丢 1 秒数据 折中方案
no 交给操作系统决定刷盘时机 可能丢数十秒数据 最快

AOF 重写(rewrite) :AOF 文件随命令累积不断膨胀,重写机制通过"读取当前内存状态、生成最精简命令集"压缩文件(如 100 次 INCR 合并为 1 条 SET)。关键点:

  1. bgrewriteaof fork 子进程,重写期间新的写命令同时写入 AOF 缓冲区和 AOF 重写缓冲区;
  2. 子进程完成重写后,将重写缓冲区的增量命令追加到新文件,原子替换旧文件;
  3. 重写期间为避免磁盘竞争,主进程对旧 AOF 的 fsync 降频为每 2 秒一次(aof_rewrite_scheduled)。

(3)Redis 7.0 Multi Part AOF

7.0 将单一 AOF 文件拆分为 base.aof(基线)+ incr.aof(增量)+ manifest(清单文件),重写期间不再需要重写缓冲区,消除了重写结束时的"追增量"阻塞窗口,且支持增量 AOF 的分段管理。

(4)混合持久化(4.0+,aof-use-rdb-preamble yes)

AOF 重写时,新 AOF 文件前半段为 RDB 格式的全量快照,后半段为 AOF 格式的增量命令。恢复时先按 RDB 快速加载,再重放增量命令------恢复速度与数据安全性兼得。

2.2.3 源码深度剖析

rdb.c 中 rdbSaveBackground 的 fork 逻辑:

c 复制代码
int rdbSaveBackground(char *filename, rdbSaveInfo *rsi) {
    pid_t childpid;
    if ((childpid = redisFork()) == 0) {
        // 子进程: 关闭监听 socket, 遍历所有 db 写 RDB
        redisSetProcTitle("redis-rdb-bgsave");
        retval = rdbSave(filename, rsi);   // 核心: 遍历 server.db 序列化
        exitFromChild((retval == C_OK) ? 0 : 1);
    } else {
        // 父进程: 记录子进程信息, 继续服务
        server.stat_fork_time = ...;
        server.rdb_child_pid = childpid;
        ...
    }
}

aof.c 中 everysec 策略的 fsync 由 bio 后台线程 执行(bio.c 定义了 BIO_CLOSE_FILE、BIO_AOF_FSYNC、BIO_LAZY_FREE 三类后台任务),避免主线程被磁盘 I/O 阻塞。

关键设计考量 :Redis 把所有耗时操作(RDB 保存、AOF fsync、大 key 异步删除)全部下沉到子进程或 bio 线程,主线程永远只做命令执行------这是 Redis 低延迟的根本保障。

2.2.4 关键细节与易错点
  1. fork 耗时与内存成正比 :大内存实例(>10GB)fork 可能耗时数百毫秒甚至秒级,引发抖动;可通过 latest_latency / INFO persistence 的 latest_fork_usec 监控;
  2. COW 内存放大:快照期间写流量越大,COW 复制的页越多,峰值内存可能接近 2 倍,需预留内存并避免在业务高峰 BGSAVE;
  3. AOF 文件损坏恢复 :redis-check-aof --fix 可截断损坏的尾部命令;
  4. AOF 的 fsync 卡死问题 :磁盘负载高时 fsync 超时,Redis 会记录日志并暂停写入(aof-last-write-status),需监控磁盘;
  5. RDB 不适合做实时备份:savepoint 是"条件触发",两次快照间宕机数据全丢;
  6. 主从复制与 RDB 耦合 :全量同步依赖 RDB,因此主节点上 disable_bgsave 需慎重。
2.2.5 优劣势与能力边界
  • RDB 优势 :恢复速度极快(直接加载二进制)、文件紧凑适合异地备份;缺陷:数据安全性差、fork 抖动;
  • AOF 优势 :最多丢 1 秒数据、文件可读可修复;缺陷:文件大、恢复慢、写放大;
  • 混合持久化:生产环境事实标准,但要求 Redis ≥ 4.0;
  • 边界:持久化不能替代备份策略------仍需定期把 RDB 归档到对象存储。

2.3 过期策略与内存淘汰机制

2.3.1 子知识点概述

Redis 是内存数据库,内存是稀缺资源。本子知识点解决两个问题:

  1. 过期键如何被删除------过期策略(针对设置了 TTL 的键);
  2. 内存写满时怎么办------内存淘汰策略(maxmemory 达到上限后的兜底)。

两者常被混淆:过期策略处理"到期键",淘汰策略处理"内存不够",淘汰策略的候选范围可以是全部键或仅过期键。

2.3.2 完整运行原理

(1)过期键的存储

所有过期时间集中存储在 db->expires 字典中(键为指针,值为毫秒级时间戳),与主字典 db->dict 分离,避免无过期键的额外开销。

(2)三种过期删除策略

策略 机制 优劣
定时删除 设置 TTL 时创建定时器,到期立即删 内存友好,但 CPU 开销大,Redis 未采用
惰性删除 访问键时才检查是否过期,过期则删 CPU 友好,但过期键可能长期占内存
定期删除 周期性随机抽样检查并删除过期键 折中方案

Redis 采用"惰性删除 + 定期删除"组合:

  • 惰性删除 :所有读写命令执行前经过 expireIfNeeded() 检查;
  • 定期删除 :由时间事件 databasesCron() → activeExpireCycle() 驱动,每 100ms 执行一次:
c 复制代码
// expire.c: activeExpireCycle 核心逻辑(简化)
// 每轮对每个 db 抽样检查
unsigned long expired;
do {
    // 1. 从 expires 字典随机抽 ACTIVE_EXPIRE_CYCLE_KEYS_PER_LOOP(默认20) 个键
    for (i = 0; i < ACTIVE_EXPIRE_CYCLE_KEYS_PER_LOOP; i++) {
        entry = dictGetRandomKey(db->expires);
        if (activeExpireCycleTryExpire(db, entry, now)) expired++; // 到期则删除
    }
    // 2. 若过期比例 > 25%, 说明过期键密集, 继续抽样
    // 3. 但总耗时不得超过时间上限(默认 25ms), 超时则下轮再战
} while (percentage > 25 && elapsed < timelimit);

关键分支逻辑:

  • 抽样过期率 >25% → 继续循环,保证过期键及时清理;
  • 执行超时 → 立即退出,绝不阻塞主线程;
  • 从库不主动删除过期键:等待主节点同步 DEL 命令,保证主从数据一致(这也是读写分离场景下从库可能读到过期数据的原因)。

(3)八种内存淘汰策略(maxmemory-policy)

策略 候选范围 算法 适用场景
noeviction --- 不淘汰,写操作直接报错 不允许丢数据的缓存
allkeys-lru 全部键 近似 LRU(最近最少使用) 通用缓存首选
volatile-lru 设了 TTL 的键 近似 LRU(最近最少使用) 部分键需常驻
allkeys-lfu 全部键 近似 LFU(4.0+)(最不经常使用) 热点数据缓存
volatile-lfu 设了 TTL 的键 近似 LFU (最不经常使用) 热点+常驻混合
allkeys-random 全部键 随机 访问分布均匀
volatile-random 设了 TTL 的键 随机 同上
volatile-ttl 设了 TTL 的键 TTL 最小者优先 明确希望快过期的先走

(4)近似 LRU 的实现原理

Redis 没有实现严格 LRU(需维护全局双向链表,内存开销大),而是采样近似:

c 复制代码
// evict.c: evictionPoolPopulate
// 随机采样 maxmemory-samples(默认5) 个键, 按空闲时间排序放入淘汰池
// 淘汰时选择池中"最久未访问"的键

redisObject.lru 字段(24 位)记录访问时间,采样 5 个后淘汰最旧者。Redis 3.0 起引入淘汰池(eviction pool):维护 16 个候选键,新采样键与池中键比较替换,使近似效果非常接近严格 LRU。

LFU(4.0+) :复用 lru 字段,拆分为 16 位"上次访问时间"+ 8 位"访问频率计数器"(对数计数,带时间衰减),解决 LRU 无法识别"低频但最近被访问"键的问题------缓存污染场景下 LFU 显著优于 LRU(如一次性全表扫描把热点挤出去)。

2.3.3 关键细节与易错点
  1. 过期时间精度 :存储为毫秒时间戳,但 EXPIRE 命令参数是秒;主从复制时过期时间以绝对时间戳传播,不受主从时钟偏差影响;
  2. 从库只读特性:从库不执行惰性/定期删除,读从库可能读到"逻辑已过期"的数据;
  3. 淘汰发生在写路径 :只有写命令触发 performEvictions(),纯读不会淘汰;
  4. LRU 采样数可调 :maxmemory-samples 10 可提升精度但增加 CPU 开销;
  5. noeviction 的报错是 OOM command not allowed------业务侧必须处理该异常;
  6. maxmemory 不包含副本缓冲:实际内存占用 = 数据 + 复制缓冲 + Lua 缓存等,需预留 20%~30%。
2.3.4 优劣势与能力边界
  • 优势:惰性+定期的组合在 CPU 与内存间取得工程最优;近似 LRU/LFU 以极小开销逼近理论效果;
  • 缺陷:定期删除是"抽样"而非全量,极端情况下过期键清理滞后;淘汰策略作用于单实例,集群场景需整体容量规划;
  • 适用 :所有缓存场景必配 maxmemory + 合理淘汰策略;
  • 不适用:把 Redis 当持久数据库且不允许任何数据丢失------应选 noeviction 并监控内存水位。

2.4 缓存穿透、缓存击穿、缓存雪崩

2.4.1 子知识点概述

这是缓存层三大经典故障模型,本质都是"缓存与数据库的一致性/可用性失衡导致流量打穿数据库"。三者的区别是面试必考的辨析点:

问题 定义 核心特征
缓存穿透 查询根本不存在的数据,缓存无、数据库也无 请求绕过缓存直达 DB,缓存形同虚设
缓存击穿 单个热点键过期瞬间,海量并发同时打到 DB 问题集中在一个键,瞬时洪峰
缓存雪崩 大量键同时过期或缓存服务宕机,请求集中打到 DB 大面积、持续性压力,可能引发级联故障
2.4.2 完整运行原理与解决方案

(1)缓存穿透
#mermaid-svg-BoK2NiiSo2t3iVy6{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-BoK2NiiSo2t3iVy6 .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-BoK2NiiSo2t3iVy6 .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-BoK2NiiSo2t3iVy6 .error-icon{fill:#552222;}#mermaid-svg-BoK2NiiSo2t3iVy6 .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-BoK2NiiSo2t3iVy6 .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-BoK2NiiSo2t3iVy6 .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-BoK2NiiSo2t3iVy6 .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-BoK2NiiSo2t3iVy6 .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-BoK2NiiSo2t3iVy6 .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-BoK2NiiSo2t3iVy6 .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-BoK2NiiSo2t3iVy6 .marker{fill:#333333;stroke:#333333;}#mermaid-svg-BoK2NiiSo2t3iVy6 .marker.cross{stroke:#333333;}#mermaid-svg-BoK2NiiSo2t3iVy6 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-BoK2NiiSo2t3iVy6 p{margin:0;}#mermaid-svg-BoK2NiiSo2t3iVy6 .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-BoK2NiiSo2t3iVy6 .cluster-label text{fill:#333;}#mermaid-svg-BoK2NiiSo2t3iVy6 .cluster-label span{color:#333;}#mermaid-svg-BoK2NiiSo2t3iVy6 .cluster-label span p{background-color:transparent;}#mermaid-svg-BoK2NiiSo2t3iVy6 .label text,#mermaid-svg-BoK2NiiSo2t3iVy6 span{fill:#333;color:#333;}#mermaid-svg-BoK2NiiSo2t3iVy6 .node rect,#mermaid-svg-BoK2NiiSo2t3iVy6 .node circle,#mermaid-svg-BoK2NiiSo2t3iVy6 .node ellipse,#mermaid-svg-BoK2NiiSo2t3iVy6 .node polygon,#mermaid-svg-BoK2NiiSo2t3iVy6 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-BoK2NiiSo2t3iVy6 .rough-node .label text,#mermaid-svg-BoK2NiiSo2t3iVy6 .node .label text,#mermaid-svg-BoK2NiiSo2t3iVy6 .image-shape .label,#mermaid-svg-BoK2NiiSo2t3iVy6 .icon-shape .label{text-anchor:middle;}#mermaid-svg-BoK2NiiSo2t3iVy6 .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-BoK2NiiSo2t3iVy6 .rough-node .label,#mermaid-svg-BoK2NiiSo2t3iVy6 .node .label,#mermaid-svg-BoK2NiiSo2t3iVy6 .image-shape .label,#mermaid-svg-BoK2NiiSo2t3iVy6 .icon-shape .label{text-align:center;}#mermaid-svg-BoK2NiiSo2t3iVy6 .node.clickable{cursor:pointer;}#mermaid-svg-BoK2NiiSo2t3iVy6 .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-BoK2NiiSo2t3iVy6 .arrowheadPath{fill:#333333;}#mermaid-svg-BoK2NiiSo2t3iVy6 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-BoK2NiiSo2t3iVy6 .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-BoK2NiiSo2t3iVy6 .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-BoK2NiiSo2t3iVy6 .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-BoK2NiiSo2t3iVy6 .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-BoK2NiiSo2t3iVy6 .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-BoK2NiiSo2t3iVy6 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-BoK2NiiSo2t3iVy6 .cluster text{fill:#333;}#mermaid-svg-BoK2NiiSo2t3iVy6 .cluster span{color:#333;}#mermaid-svg-BoK2NiiSo2t3iVy6 div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-BoK2NiiSo2t3iVy6 .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-BoK2NiiSo2t3iVy6 rect.text{fill:none;stroke-width:0;}#mermaid-svg-BoK2NiiSo2t3iVy6 .icon-shape,#mermaid-svg-BoK2NiiSo2t3iVy6 .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-BoK2NiiSo2t3iVy6 .icon-shape p,#mermaid-svg-BoK2NiiSo2t3iVy6 .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-BoK2NiiSo2t3iVy6 .icon-shape .label rect,#mermaid-svg-BoK2NiiSo2t3iVy6 .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-BoK2NiiSo2t3iVy6 .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-BoK2NiiSo2t3iVy6 .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-BoK2NiiSo2t3iVy6 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 未命中
不存在
每次请求都重复此路径
请求 id=-1 的商品
查缓存
查数据库
返回空
恶意攻击/脏数据

攻击者利用"不存在的数据"绕过缓存,因为缓存层对"不存在"没有记忆。解决方案:

  • 缓存空对象 :数据库查不到时,向 Redis 写入空值(如 SET product:-1 "" EX 60),短 TTL 防止长期占用;实现简单,但无法防御海量随机不存在的 key(内存被撑爆);
  • 布隆过滤器前置拦截:将所有合法 ID 预先载入布隆过滤器,请求先过 BF,判定"一定不存在"的直接拒绝。误判率可控(通常 <1%),内存开销极小(1 亿 key 约 100MB);
  • 参数合法性校验:id ≤ 0 等非法参数在网关层直接拦截,属于第一道防线。

(2)缓存击穿
#mermaid-svg-Z1oYVa62PwzROb5P{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-Z1oYVa62PwzROb5P .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-Z1oYVa62PwzROb5P .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-Z1oYVa62PwzROb5P .error-icon{fill:#552222;}#mermaid-svg-Z1oYVa62PwzROb5P .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-Z1oYVa62PwzROb5P .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-Z1oYVa62PwzROb5P .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-Z1oYVa62PwzROb5P .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-Z1oYVa62PwzROb5P .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-Z1oYVa62PwzROb5P .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-Z1oYVa62PwzROb5P .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-Z1oYVa62PwzROb5P .marker{fill:#333333;stroke:#333333;}#mermaid-svg-Z1oYVa62PwzROb5P .marker.cross{stroke:#333333;}#mermaid-svg-Z1oYVa62PwzROb5P svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-Z1oYVa62PwzROb5P p{margin:0;}#mermaid-svg-Z1oYVa62PwzROb5P .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-Z1oYVa62PwzROb5P .cluster-label text{fill:#333;}#mermaid-svg-Z1oYVa62PwzROb5P .cluster-label span{color:#333;}#mermaid-svg-Z1oYVa62PwzROb5P .cluster-label span p{background-color:transparent;}#mermaid-svg-Z1oYVa62PwzROb5P .label text,#mermaid-svg-Z1oYVa62PwzROb5P span{fill:#333;color:#333;}#mermaid-svg-Z1oYVa62PwzROb5P .node rect,#mermaid-svg-Z1oYVa62PwzROb5P .node circle,#mermaid-svg-Z1oYVa62PwzROb5P .node ellipse,#mermaid-svg-Z1oYVa62PwzROb5P .node polygon,#mermaid-svg-Z1oYVa62PwzROb5P .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-Z1oYVa62PwzROb5P .rough-node .label text,#mermaid-svg-Z1oYVa62PwzROb5P .node .label text,#mermaid-svg-Z1oYVa62PwzROb5P .image-shape .label,#mermaid-svg-Z1oYVa62PwzROb5P .icon-shape .label{text-anchor:middle;}#mermaid-svg-Z1oYVa62PwzROb5P .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-Z1oYVa62PwzROb5P .rough-node .label,#mermaid-svg-Z1oYVa62PwzROb5P .node .label,#mermaid-svg-Z1oYVa62PwzROb5P .image-shape .label,#mermaid-svg-Z1oYVa62PwzROb5P .icon-shape .label{text-align:center;}#mermaid-svg-Z1oYVa62PwzROb5P .node.clickable{cursor:pointer;}#mermaid-svg-Z1oYVa62PwzROb5P .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-Z1oYVa62PwzROb5P .arrowheadPath{fill:#333333;}#mermaid-svg-Z1oYVa62PwzROb5P .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-Z1oYVa62PwzROb5P .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-Z1oYVa62PwzROb5P .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-Z1oYVa62PwzROb5P .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-Z1oYVa62PwzROb5P .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-Z1oYVa62PwzROb5P .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-Z1oYVa62PwzROb5P .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-Z1oYVa62PwzROb5P .cluster text{fill:#333;}#mermaid-svg-Z1oYVa62PwzROb5P .cluster span{color:#333;}#mermaid-svg-Z1oYVa62PwzROb5P div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-Z1oYVa62PwzROb5P .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-Z1oYVa62PwzROb5P rect.text{fill:none;stroke-width:0;}#mermaid-svg-Z1oYVa62PwzROb5P .icon-shape,#mermaid-svg-Z1oYVa62PwzROb5P .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-Z1oYVa62PwzROb5P .icon-shape p,#mermaid-svg-Z1oYVa62PwzROb5P .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-Z1oYVa62PwzROb5P .icon-shape .label rect,#mermaid-svg-Z1oYVa62PwzROb5P .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-Z1oYVa62PwzROb5P .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-Z1oYVa62PwzROb5P .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-Z1oYVa62PwzROb5P :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 热点 key 过期
并发请求 1..N 同时到达
缓存未命中
请求 1 获得互斥锁
请求 2..N 等待/重试
查 DB 并重建缓存
释放锁
重新读缓存, 命中

解决方案:

  • 互斥锁重建(推荐) :缓存未命中时,用 SET lock_key unique_val NX EX 10 抢锁,只有拿到锁的线程去查 DB 重建缓存,其余线程短暂等待后重试。注意锁要设 TTL 防止死锁,释放锁时校验 unique_val 防误删;
  • 逻辑过期(不设 TTL) :热点键永不过期,值中内嵌逻辑过期时间;读到"逻辑过期"的旧值时,开异步线程重建缓存,当前请求先返回旧值------牺牲短暂一致性换取绝对可用性,适合对一致性要求不高的热点数据;
  • 热点预加载与永不过期:对可预知的热点(如秒杀商品),上线前预热并配合后台定时刷新。

(3)缓存雪崩

雪崩有两种成因,解法不同:

成因 解决方案
大量键同时过期 过期时间加随机抖动 :TTL = base + random(0, 300),打散过期时刻;热点数据永不过期 + 异步刷新
缓存服务宕机 高可用架构 :Sentinel/Cluster 保证缓存层不整体失效;多级缓存 :本地缓存(Caffeine)+ Redis 兜底;熔断限流降级:Sentinel/Hystrix 限制打到 DB 的流量阈值,超出直接返回降级数据

此外通用兜底手段:数据库层限流 (连接池/令牌桶)、缓存预热 (服务启动时批量加载)、构建缓存更新队列串行化重建,避免 DB 被瞬时打垮。

2.4.3 关键细节与易错点
  1. 布隆过滤器不支持删除(标准版),删除场景需用 Counting Bloom Filter 或定期重建;
  2. 互斥锁方案中,等待线程若无限重试会占用线程池,应设最大重试次数与退避;
  3. 逻辑过期方案的"异步重建"必须保证只有一个线程在重建(仍需分布式锁或 CAS 标记),否则重复重建浪费资源;
  4. 雪崩的"随机抖动"不是越大越好,需结合数据时效性要求权衡;
  5. 三大问题常并发出现:雪崩期间大量键失效 → 重建洪峰 → 演变为大面积击穿,因此架构上要组合设防。
2.4.4 优劣势与能力边界
  • 三大方案本质是"空间换时间、一致性换可用性、复杂度换稳定性"的权衡;
  • 布隆过滤器适合读多写少、可容忍极低误判的场景;
  • 互斥锁适合一致性要求高的场景,但会引入锁竞争与短暂请求等待;
  • 逻辑过期适合热点明确、可容忍秒级旧数据的场景;
  • 边界:所有方案都不能替代数据库自身的容量与限流设计,DB 永远是最后一道防线。

2.5 Redis 分布式锁

2.5.1 子知识点概述

分布式锁是 Redis 最高频的实战考点。其本质是:利用 Redis 单线程命令执行的原子性,用一个键的存在性/值来表达"锁的持有状态" 。核心要求是互斥、防死锁、可重入、高可用、可续期。

2.5.2 完整运行原理

(1)基础加锁原语

redis 复制代码
SET lock_key unique_request_id NX EX 30
  • NX:键不存在才设置成功 → 保证互斥;
  • EX 30:自动过期 → 防止持锁进程崩溃导致死锁;
  • unique_request_id:标识锁的持有者 → 解锁时校验,防止误删他人锁。

(2)解锁必须用 Lua 保证原子性

"判断值是否为自己 + 删除"两步操作若非原子,会出现误删:

lua 复制代码
-- unlock.lua
if redis.call("get", KEYS[1]) == ARGV[1] then
    return redis.call("del", KEYS[1])
else
    return 0
end

(3)锁续期(看门狗机制)

固定过期时间面临两难:设短了业务没执行完锁就过期(导致锁失效、他人进入临界区);设长了持锁者崩溃后锁迟迟不释放。**看门狗(Watchdog)**方案:初始过期时间设为 30 秒,后台线程每隔 10 秒(1/3 周期)检查持锁线程是否存活,存活则续期回 30 秒;线程结束或进程崩溃则停止续期,锁自然过期。Redisson 的 lock()(不指定 leaseTime)默认启用看门狗。

(4)可重入

Redisson 用 Hash 结构记录重入次数:

redis 复制代码
HSET lock_key <clientId:threadId> 1   -- 首次加锁
HINCRBY lock_key <clientId:threadId> 1 -- 重入时计数+1

解锁时计数减 1,归零才真正删除键。

(5)主从切换下的锁丢失与 Redlock

主节点加锁成功后、尚未同步到从节点时主节点宕机,从节点晋升为新主------锁丢失 ,另一客户端可在新主上再次加锁,互斥被破坏。针对此问题,antirez 提出 Redlock 算法:

  1. 向 N 个(建议 5 个)相互独立的 Redis 实例,用相同 key/value 依次尝试加锁;
  2. 只有在**大多数(N/2+1)**实例上加锁成功、且总耗时小于锁有效期,才算加锁成功;
  3. 锁的实际有效时间 = 设定有效期 − 加锁耗时;
  4. 失败则向所有实例发送解锁请求(即使某些实例没加上锁也要发,防止残留)。

Redlock 存在争议(Martin Kleppmann 指出其依赖时钟假设、GC 暂停等问题),工程上更常见的取舍是:对强一致性要求极高的场景用 ZooKeeper/etcd 的 CP 型锁;对性能敏感、可容忍极端情况下短暂双写的场景用单主/哨兵 Redis 锁 + 业务幂等兜底。

2.5.3 源码/实现剖析(Redisson 加锁核心逻辑伪代码)
lua 复制代码
-- Redisson tryLock 的 Lua 脚本(简化还原)
if (redis.call('exists', KEYS[1]) == 0) then
    redis.call('hincrby', KEYS[1], ARGV[2], 1);  -- 无锁: 记录持有者, 计数1
    redis.call('pexpire', KEYS[1], ARGV[1]);      -- 设置过期时间
    return nil;                                    -- nil 表示加锁成功
end;
if (redis.call('hexists', KEYS[1], ARGV[2]) == 1) then
    redis.call('hincrby', KEYS[1], ARGV[2], 1);  -- 已持有: 重入计数+1
    redis.call('pexpire', KEYS[1], ARGV[1]);
    return nil;
end;
return redis.call('pttl', KEYS[1]);               -- 锁被他人持有: 返回剩余TTL, 供订阅等待

设计考量:加锁失败时返回剩余 TTL,客户端据此订阅该键的过期事件(Pub/Sub),锁释放时被唤醒重试------避免盲目轮询带来的性能浪费。

2.5.4 关键细节与易错点
  1. 绝不用 SETNX + EXPIRE 两条命令 :非原子,中间崩溃即死锁,必须用 SET key val NX EX;
  2. 解锁必须校验持有者:否则慢请求超时后解锁会删掉新持有者的锁;
  3. 锁粒度要细 :lock:order:{orderId} 优于 lock:order,避免串行化整个业务;
  4. 看门狗只解决"续期",不解决"主从切换丢锁":两者是独立问题;
  5. 锁内操作要快:临界区耗时越长,锁竞争与超时风险越大;
  6. 集群模式下锁键与订阅:Redisson 依赖 Pub/Sub 通知,集群中订阅会自动路由到锁所在节点。
2.5.5 优劣势与能力边界
  • 优势:性能远高于数据库锁,实现轻量,Redisson 封装成熟(可重入、看门狗、公平锁、联锁);
  • 缺陷:AP 属性决定其在主从切换瞬间无法保证绝对互斥;Redlock 复杂且依赖时钟;
  • 适用:缓存重建互斥、定时任务防重、库存扣减串行化等绝大多数互联网场景;
  • 不适用:金融级强一致互斥------应选 CP 型协调服务或数据库唯一约束兜底。

2.6 主从复制

2.6.1 子知识点概述

主从复制是 Redis 高可用的基石:从节点复制主节点数据,实现读写分离、数据冗余、故障恢复的基础 。复制分为两个阶段:全量复制(首次同步/断线过久)与增量复制(断线重连后的差异补齐) ,核心机制是 PSYNC2 协议 + 复制积压缓冲区(repl_backlog)+ 复制 ID(replid)。

2.6.2 完整运行原理

(1)全量复制流程(首次同步)
主节点 从节点 主节点 从节点 #mermaid-svg-bjguGbxxPbKR2xOz{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-bjguGbxxPbKR2xOz .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-bjguGbxxPbKR2xOz .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-bjguGbxxPbKR2xOz .error-icon{fill:#552222;}#mermaid-svg-bjguGbxxPbKR2xOz .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-bjguGbxxPbKR2xOz .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-bjguGbxxPbKR2xOz .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-bjguGbxxPbKR2xOz .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-bjguGbxxPbKR2xOz .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-bjguGbxxPbKR2xOz .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-bjguGbxxPbKR2xOz .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-bjguGbxxPbKR2xOz .marker{fill:#333333;stroke:#333333;}#mermaid-svg-bjguGbxxPbKR2xOz .marker.cross{stroke:#333333;}#mermaid-svg-bjguGbxxPbKR2xOz svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-bjguGbxxPbKR2xOz p{margin:0;}#mermaid-svg-bjguGbxxPbKR2xOz .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-bjguGbxxPbKR2xOz text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-bjguGbxxPbKR2xOz .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-bjguGbxxPbKR2xOz .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-bjguGbxxPbKR2xOz .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-bjguGbxxPbKR2xOz .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-bjguGbxxPbKR2xOz #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-bjguGbxxPbKR2xOz .sequenceNumber{fill:white;}#mermaid-svg-bjguGbxxPbKR2xOz #sequencenumber{fill:#333;}#mermaid-svg-bjguGbxxPbKR2xOz #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-bjguGbxxPbKR2xOz .messageText{fill:#333;stroke:none;}#mermaid-svg-bjguGbxxPbKR2xOz .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-bjguGbxxPbKR2xOz .labelText,#mermaid-svg-bjguGbxxPbKR2xOz .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-bjguGbxxPbKR2xOz .loopText,#mermaid-svg-bjguGbxxPbKR2xOz .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-bjguGbxxPbKR2xOz .loopLine{stroke-width:2px;stroke-dasharray:2,2;stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-bjguGbxxPbKR2xOz .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-bjguGbxxPbKR2xOz .noteText,#mermaid-svg-bjguGbxxPbKR2xOz .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-bjguGbxxPbKR2xOz .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-bjguGbxxPbKR2xOz .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-bjguGbxxPbKR2xOz .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-bjguGbxxPbKR2xOz .actorPopupMenu{position:absolute;}#mermaid-svg-bjguGbxxPbKR2xOz .actorPopupMenuPanel{position:absolute;fill:#ECECFF;box-shadow:0px 8px 16px 0px rgba(0,0,0,0.2);filter:drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4));}#mermaid-svg-bjguGbxxPbKR2xOz .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-bjguGbxxPbKR2xOz .actor-man circle,#mermaid-svg-bjguGbxxPbKR2xOz line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-bjguGbxxPbKR2xOz :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} loop 命令传播(持续) PSYNC ? -1 (首次, 无 replid) +FULLRESYNC replid offset BGSAVE 生成 RDB (fork) 期间写命令存入"复制缓冲区" 发送 RDB 文件 清空旧数据, 加载 RDB 发送复制缓冲区中的增量命令 执行增量命令, 追平主节点 异步发送写命令

(2)增量复制(断线重连)

从节点重连后发送 PSYNC <replid> <offset>:

  • 若主节点发现该 offset 仍在复制积压缓冲区 (环形缓冲,默认 1MB)内 → 部分重同步(+CONTINUE),只补发缺失的命令,代价极小;
  • 若 offset 已滑出缓冲区,或 replid 不匹配 → 退化为全量复制。

关键细节 :主节点的 replid 在每次从节点晋升为主时会变化;Redis 4.0 引入 replid2 记录"上一任主节点的 replid",使得故障切换后新主仍能与旧从节点部分重同步,避免切换引发的全量风暴。

(3)命令传播与心跳

  • 主节点每次执行写命令后,向所有从节点传播该命令(异步);
  • 主从互发心跳:从节点默认每秒发 REPLCONF ACK <offset>,主节点据此判断从节点状态与延迟(master_link_status、slave_repl_offset);
  • REPLCONF ACK 还承担检测复制延迟 与保证 AOF 持久化级别 (min-replicas-to-write)的职责。

(4)无盘复制

repl-diskless-sync yes:主节点不落地 RDB 文件,直接通过 socket 把 RDB 流式发给从节点,适合磁盘慢但网络快的场景。

2.6.3 源码要点

replication.c 中 masterTryPartialResynchronization 判断部分重同步:

c 复制代码
// 校验 replid 是否匹配(当前或前任), offset 是否在 backlog 范围内
if (replid 匹配 && offset >= master_repl_offset - backlog_size) {
    // 返回 +CONTINUE, 从 backlog 中补发 [offset, master_repl_offset] 区间命令
} else {
    // 走全量: startBgsaveForReplication
}
2.6.4 关键细节与易错点
  1. 复制是异步的:主从存在毫秒级延迟,读写分离时从库可能读到旧数据,强一致读需读主;
  2. 全量复制代价巨大 :fork + RDB 传输 + 从库清空重载,大实例全量可能持续分钟级,repl_backlog 要足够大(建议按"断线时长 × 写入速率"估算)以避免频繁全量;
  3. 从节点默认只读 :replica-read-only yes,且从节点的写不会同步给主;
  4. 复制缓冲区的内存 :每个从节点有独立的客户端输出缓冲,从节点过多或网络慢会导致主节点内存暴涨(client-output-buffer-limit replica 需合理配置);
  5. 主从复制不保证事务命令的顺序完全一致(Lua/MULTI 作为整体传播,但多从之间到达顺序可能不同------单从内是一致的)。
2.6.5 优劣势与能力边界
  • 优势:架构简单、读写分离扩展读能力、为哨兵/集群提供基础;
  • 缺陷:异步复制有数据丢失窗口;全量复制开销大;写能力无法扩展(单主);
  • 适用:读多写少业务的读扩展、数据冗余备份;
  • 不适用:需要强一致读、或写压力需要水平扩展的场景(应上 Cluster)。

2.7 Sentinel 哨兵

2.7.1 子知识点概述

主从架构的痛点是主节点故障后需要人工切换 。Sentinel 是一个独立运行的进程集群,负责:监控 主从健康、自动故障转移 、配置中心 (向客户端通告当前主节点地址)、通知。Sentinel 本身以集群方式部署(至少 3 个,奇数),避免单点误判。

2.7.2 完整运行原理

(1)监控机制

  • 每个 Sentinel 每 1 秒向主/从节点及其他 Sentinel 发送 PING;
  • 若某实例在 down-after-milliseconds 内未有效回复 → 该 Sentinel 判定其主观下线(SDOWN);
  • 对主节点的 SDOWN,Sentinel 会询问其他 Sentinel;当超过 quorum 个 Sentinel 都认为主节点下线 → 客观下线(ODOWN)。从节点和哨兵自身只会有主观下线,不会触发故障转移。

(2)故障转移流程
#mermaid-svg-wlHuGmDMP8GeI8SO{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-wlHuGmDMP8GeI8SO .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-wlHuGmDMP8GeI8SO .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-wlHuGmDMP8GeI8SO .error-icon{fill:#552222;}#mermaid-svg-wlHuGmDMP8GeI8SO .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-wlHuGmDMP8GeI8SO .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-wlHuGmDMP8GeI8SO .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-wlHuGmDMP8GeI8SO .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-wlHuGmDMP8GeI8SO .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-wlHuGmDMP8GeI8SO .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-wlHuGmDMP8GeI8SO .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-wlHuGmDMP8GeI8SO .marker{fill:#333333;stroke:#333333;}#mermaid-svg-wlHuGmDMP8GeI8SO .marker.cross{stroke:#333333;}#mermaid-svg-wlHuGmDMP8GeI8SO svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-wlHuGmDMP8GeI8SO p{margin:0;}#mermaid-svg-wlHuGmDMP8GeI8SO .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-wlHuGmDMP8GeI8SO .cluster-label text{fill:#333;}#mermaid-svg-wlHuGmDMP8GeI8SO .cluster-label span{color:#333;}#mermaid-svg-wlHuGmDMP8GeI8SO .cluster-label span p{background-color:transparent;}#mermaid-svg-wlHuGmDMP8GeI8SO .label text,#mermaid-svg-wlHuGmDMP8GeI8SO span{fill:#333;color:#333;}#mermaid-svg-wlHuGmDMP8GeI8SO .node rect,#mermaid-svg-wlHuGmDMP8GeI8SO .node circle,#mermaid-svg-wlHuGmDMP8GeI8SO .node ellipse,#mermaid-svg-wlHuGmDMP8GeI8SO .node polygon,#mermaid-svg-wlHuGmDMP8GeI8SO .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-wlHuGmDMP8GeI8SO .rough-node .label text,#mermaid-svg-wlHuGmDMP8GeI8SO .node .label text,#mermaid-svg-wlHuGmDMP8GeI8SO .image-shape .label,#mermaid-svg-wlHuGmDMP8GeI8SO .icon-shape .label{text-anchor:middle;}#mermaid-svg-wlHuGmDMP8GeI8SO .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-wlHuGmDMP8GeI8SO .rough-node .label,#mermaid-svg-wlHuGmDMP8GeI8SO .node .label,#mermaid-svg-wlHuGmDMP8GeI8SO .image-shape .label,#mermaid-svg-wlHuGmDMP8GeI8SO .icon-shape .label{text-align:center;}#mermaid-svg-wlHuGmDMP8GeI8SO .node.clickable{cursor:pointer;}#mermaid-svg-wlHuGmDMP8GeI8SO .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-wlHuGmDMP8GeI8SO .arrowheadPath{fill:#333333;}#mermaid-svg-wlHuGmDMP8GeI8SO .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-wlHuGmDMP8GeI8SO .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-wlHuGmDMP8GeI8SO .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-wlHuGmDMP8GeI8SO .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-wlHuGmDMP8GeI8SO .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-wlHuGmDMP8GeI8SO .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-wlHuGmDMP8GeI8SO .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-wlHuGmDMP8GeI8SO .cluster text{fill:#333;}#mermaid-svg-wlHuGmDMP8GeI8SO .cluster span{color:#333;}#mermaid-svg-wlHuGmDMP8GeI8SO div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-wlHuGmDMP8GeI8SO .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-wlHuGmDMP8GeI8SO rect.text{fill:none;stroke-width:0;}#mermaid-svg-wlHuGmDMP8GeI8SO .icon-shape,#mermaid-svg-wlHuGmDMP8GeI8SO .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-wlHuGmDMP8GeI8SO .icon-shape p,#mermaid-svg-wlHuGmDMP8GeI8SO .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-wlHuGmDMP8GeI8SO .icon-shape .label rect,#mermaid-svg-wlHuGmDMP8GeI8SO .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-wlHuGmDMP8GeI8SO .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-wlHuGmDMP8GeI8SO .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-wlHuGmDMP8GeI8SO :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 主节点客观下线 ODOWN
Sentinel 间选举 Leader
Leader 执行故障转移
从从节点中选出新主
向新主发送 SLAVEOF NO ONE
让其他从节点复制新主
更新配置, 通知客户端

Leader 选举 :通过 Raft 算法,SDOWN 发生后各 Sentinel 发起投票,获得半数以上且 ≥ quorum 票者成为 Leader 执行转移。

新主选择标准(按优先级依次判断):

  1. 排除不健康的从节点(下线、断连过久);
  2. slave-priority(replica-priority)数值最小者优先(0 表示永不参选);
  3. 复制偏移量最大者(数据最新)优先;
  4. runid 最小者兜底。

(3)客户端交互

客户端连接 Sentinel 获取主节点地址,订阅 +switch-master 频道;主从切换后 Sentinel 推送新地址,客户端自动重连------客户端必须实现订阅与重连逻辑(Jedis/Lettuce 的 Sentinel 模式已内置)。

2.7.3 关键细节与易错点
  1. quorum 与多数派:quorum 只决定 ODOWN,实际故障转移还需 Sentinel 总数过半,因此 3 节点集群容忍 1 个哨兵故障,2 个则无法转移;
  2. 脑裂风险 :网络分区时旧主可能与哨兵失联而被降级,分区恢复后出现"双主",旧主以从节点身份重新加入会清空数据------可配 min-replicas-to-write 让旧主在无从节点时拒绝写入,减小脑裂丢数据窗口;
  3. Sentinel 自身无持久化需求 :其状态通过 INFO 与 gossip 实时获取,配置文件仅存初始拓扑;
  4. 故障转移期间写入会丢失:异步复制 + 切换窗口,业务需容忍秒级数据丢失或做幂等。
2.7.4 优劣势与能力边界
  • 优势:自动故障转移,运维成本低,客户端接入成熟;
  • 缺陷 :写能力与存储容量仍受限于单主,无法水平扩展;切换期间有短暂不可用与数据丢失窗口;
  • 适用:数据量单实例可容纳、需要高可用的中小规模场景;
  • 不适用:数据量超过单机内存、或写 QPS 超过单机能力的场景------应选 Cluster。

2.8 Redis Cluster 集群

2.8.1 子知识点概述

Redis Cluster 是 Redis 官方的分布式解决方案 ,解决哨兵无法突破的两大瓶颈:数据分片(突破单机内存)与写能力扩展 。核心机制:16384 个哈希槽(hash slot)+ Gossip 协议 + 客户端重定向(MOVED/ASK)+ 集群内自动故障转移。

2.8.2 完整运行原理

(1)数据分片:哈希槽

集群将所有数据划分为 16384 个槽 ,每个键通过 CRC16(key) % 16384 映射到槽,槽再分配给各主节点。

c 复制代码
// cluster.c: keyHashSlot
// 支持哈希标签: 若 key 含 {...}, 只对大括号内内容计算哈希
// 例如 {user1000}.following 与 {user1000}.followers 必落同一槽 → 支持多键操作
unsigned int keyHashSlot(char *key, int keylen) {
    // 找到第一个 { 与之后的第一个 }, 对该子串做 CRC16
}

为什么是 16384 个槽?(高频追问)

  • 节点间心跳包(PING/PONG)要携带自身的槽位图(bitmap),16384 位 = 2KB,可接受;若是 65536 位则心跳包过大;
  • 官方建议集群规模不超过 1000 节点,16384 个槽足够分配(平均每节点至少 16 个槽);
  • 槽数量压缩后心跳包更小,Gossip 通信开销可控。

(2)Gossip 协议与集群状态

节点间通过 MEET(握手)、PING/PONG(每秒随机选节点互发,携带自身状态与部分已知节点信息)、FAIL(广播某节点下线)等消息交换集群拓扑。每个节点维护 clusterState,所有槽都有主节点负责时为 ok。

(3)客户端路由:MOVED 与 ASK

  • 客户端可向任意节点发请求;若键不在该节点,返回 MOVED <slot> <ip:port> → 客户端更新本地槽映射缓存并重定向到正确节点;
  • 槽迁移过程中,源节点对迁移中的键返回 ASK 重定向 → 客户端先发 ASKING 再发命令到目标节点,不更新本地缓存(因为迁移未完成,键可能还在源端)。

(4)故障检测与转移

  • 集群内节点互相 PING,超时未回复则标记 PFAIL(疑似下线);
  • 当超过半数主节点 都将某主节点标记 PFAIL → 该节点被标记 FAIL(客观下线)并广播;
  • 故障转移与哨兵类似:下线主节点的从节点发起选举(需获得半数以上主节点投票),晋升为新主并接管原槽位,广播 PONG 通知全集群。

(5)槽迁移(resharding)

redis-cli --cluster reshard 在线迁移槽:逐个把槽内的键从源节点 MIGRATE 到目标节点,迁移期间通过 ASK 重定向保证可用性。

2.8.3 关键细节与易错点
  1. 多键操作限制 :MGET、事务、Lua 涉及多个键时,所有键必须在同一槽,否则报错------用哈希标签强制同槽,但要防止数据倾斜;
  2. 客户端必须实现重定向:JedisCluster/Lettuce 已内置槽缓存与重定向,裸用 Jedis 连集群会失败;
  3. 集群不支持跨槽的 SELECT 多数据库:只有 db0;
  4. 发布订阅在集群中是全集群广播:消息会发到所有节点,注意流量放大;
  5. 扩容加节点后需手动/自动 rebalance:新节点默认无槽,不 rebalance 则不承载数据;
  6. 集群模式下哨兵不再需要:Cluster 自带故障转移,二者不叠加使用。
2.8.4 优劣势与能力边界
  • 优势:水平扩展内存与写能力、自动故障转移、官方原生支持;
  • 缺陷:多键/事务受槽约束、客户端复杂度高、运维(扩缩容、迁移)成本高于哨兵、异步复制仍有丢数据窗口;
  • 适用:数据量大(>单机内存)或写 QPS 高的中大型业务;
  • 不适用:小数据量场景(哨兵更简单);需要强一致的场景(考虑带 Raft 的方案或业务层幂等)。

2.9 事务与 Lua 脚本

2.9.1 子知识点概述

Redis 事务(MULTI/EXEC)与 Lua 脚本都用于"批量命令的原子执行 ",但能力差异显著:事务不支持回滚、命令错误分两类处理 ;Lua 脚本以单命令的原子性执行任意逻辑,是事实上的首选。

2.9.2 完整运行原理

(1)事务的三个阶段

redis 复制代码
MULTI          -- 开启事务, 后续命令入队
SET k1 v1
SET k2 v2
EXEC           -- 原子执行队列中所有命令
  • 入队阶段:命令不执行,仅放入客户端事务队列;
  • 执行阶段 :EXEC 后按顺序执行,期间不会插入其他客户端命令(单线程保证隔离性);
  • 错误处理两类 :
    • 语法/编译错误 (如命令拼错):EXEC 前即报错,整个事务被丢弃;
    • 运行时错误 (如对 String 执行 LPUSH):仅该命令失败,其余命令照常执行,不回滚。

(2)WATCH 乐观锁

redis 复制代码
WATCH balance          -- 监视键
val = GET balance
MULTI
SET balance (val - 100)
EXEC                   -- 若 balance 在 WATCH 后被修改, EXEC 返回 nil(事务失败)

WATCH 实现 CAS:被监视键在 EXEC 前被任何客户端修改,事务自动失败,客户端可重试------这是 Redis 实现乐观锁的唯一途径。

(3)Lua 脚本

redis 复制代码
EVAL "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end" 1 lock_key unique_id
  • Redis 将 EVAL 的脚本当作单个命令执行,期间独占主线程,天然原子;
  • 脚本通过 SCRIPT LOAD + EVALSHA 复用,减少网络传输;
  • 脚本要短平快 :长脚本会阻塞所有请求,可用 SCRIPT KILL 终止(仅限未产生写副作用时);
  • 集群限制:Lua 中操作的键必须在同一槽(哈希标签解决)。

(4)Redis 7.0 Function

Function 是 Lua 脚本的工程化升级:随库持久化、可复制、支持声明只读(#!lua flags=no-writes),解决 EVALSHA 脚本丢失问题,是 7.0+ 的推荐方案。

2.9.3 关键细节与易错点
  1. Redis 事务不支持回滚:官方认为命令失败多是编程错误,回滚不解决根本问题,且保持简单高性能;
  2. DISCARD 取消事务 、UNWATCH 清除监视;
  3. 事务中不能使用查询命令的结果决定后续命令:入队时命令已固定,需要条件逻辑请用 Lua;
  4. Lua 脚本的随机性命令限制 :脚本中调用 TIME、RANDOMKEY 等非确定性命令后再写,复制行为受限(4.0+ 的 effects replication 已放宽);
  5. EVAL 的 numkeys 参数:必须正确声明 KEYS 数量,否则参数错位。
2.9.4 优劣势与能力边界
  • 事务:适合"固定命令序列的原子提交",不适合有条件分支、需要回滚的复杂业务;
  • Lua:适合原子化的复合逻辑(解锁、限流、库存扣减),但脚本复杂度上升后可读性/可维护性下降;
  • 边界:涉及多服务、多数据源的分布式事务,Redis 事务/Lua 无能为力,需 Saga/TCC 等方案。

2.10 发布订阅与 Stream

2.10.1 子知识点概述

Redis 提供两种消息能力:Pub/Sub (广播式、无持久化、即发即弃)与 Stream(5.0+,支持持久化、消费者组、ACK、消息回溯),后者使 Redis 具备轻量级消息队列能力。

2.10.2 完整运行原理

(1)Pub/Sub

redis 复制代码
SUBSCRIBE channel:news      -- 订阅
PUBLISH channel:news "hello" -- 发布, 返回收到消息的订阅者数
  • 消息不存储:订阅者离线期间的消息永久丢失;
  • 支持模式订阅 PSUBSCRIBE channel:*;
  • 集群模式下消息全节点广播;
  • 适用:配置变更通知、轻量事件广播;不适合可靠消息。

(2)Stream

redis 复制代码
XADD mystream * field1 value1          -- 追加消息, * 表示自动生成 ID(毫秒-序号)
XREAD COUNT 10 STREAMS mystream 0      -- 从头读取
XGROUP CREATE mystream mygroup $ MKSTREAM -- 创建消费者组, $ 表示从最新消息开始
XREADGROUP GROUP mygroup consumer1 COUNT 10 STREAMS mystream >  -- 组内消费, > 表示新消息
XACK mystream mygroup <message-id>     -- 确认消息, 从 PEL 移除
XPENDING / XCLAIM                       -- 查询/转移超时未确认消息

核心机制:

  • 消费者组 :组内消息竞争消费(一条消息只被组内一个消费者处理),不同组各自独立消费全量消息;
  • PEL(Pending Entries List) :记录已投递未 ACK 的消息,消费者崩溃后可 XCLAIM 转移给其他消费者------实现可靠投递;
  • 消息持久化 :Stream 存于内存并随 RDB/AOF 持久化,支持 MAXLEN 裁剪长度。
2.10.3 关键细节与易错点
  1. Pub/Sub 的客户端输出缓冲 :订阅者消费慢会导致缓冲区膨胀,可能触发 client-output-buffer-limit pubsub 强制断开;
  2. Stream 的消息 ID 单调递增 ,* 自动生成依赖服务器时间,时钟回拨会报错(用 * 时 Redis 保证不回退);
  3. XREADGROUP 阻塞读 :BLOCK 0 可长轮询,但要注意客户端连接管理;
  4. Stream 不能替代专业 MQ:无分区水平扩展(单键单实例)、吞吐低于 Kafka,适合中小规模、不想引入额外中间件的场景。
2.10.4 优劣势与能力边界
  • Pub/Sub:极简、低延迟,但不可靠;
  • Stream:轻量可靠、零额外组件,适合订单通知、日志收集等中小流量场景;
  • 边界:高吞吐、多租户、复杂路由的消息场景仍应选 Kafka/RocketMQ。

三、实战最佳实践

3.1 业务使用案例集

案例一:接口热点数据缓存 + 防穿透/击穿(电商商品详情)
  • 业务背景:商品详情页 QPS 峰值 5 万,直接查 MySQL 导致数据库 CPU 打满;同时存在爬虫查询不存在商品 ID 的穿透风险。

  • 实现目标:热点商品走 Redis,命中率 >95%;不存在的 ID 被拦截;热点键过期不击穿。

  • 前置准备 :Spring Boot + spring-boot-starter-data-redis(Lettuce)、布隆过滤器(Redisson RBloomFilter 或 RedisBloom 模块)、Jackson。

  • 实现步骤:

    1. 服务启动时把全量商品 ID 载入布隆过滤器;
    2. 请求先过 BF,再查缓存;
    3. 缓存未命中用互斥锁重建,空结果缓存 60 秒;
    4. 缓存 TTL 加随机抖动防雪崩。
  • 核心代码实现:

java 复制代码
public ProductVO getProduct(Long id) {
    // 1. 参数校验 + 布隆过滤器拦截穿透
    if (id == null || id <= 0) return null;
    if (!productBloomFilter.contains(id)) {   // BF 判定不存在, 直接返回
        return null;
    }
    String cacheKey = "product:" + id;
    String json = redisTemplate.opsForValue().get(cacheKey);
    if (json != null) {
        return "".equals(json) ? null : JSON.parseObject(json, ProductVO.class); // 空对象标记
    }
    // 2. 缓存未命中: 互斥锁防击穿
    String lockKey = "lock:product:" + id;
    String uuid = UUID.randomUUID().toString();
    try {
        if (tryLock(lockKey, uuid, 10)) {     // SET NX EX 10
            json = redisTemplate.opsForValue().get(cacheKey); // double check
            if (json != null) return JSON.parseObject(json, ProductVO.class);
            ProductVO vo = productMapper.selectById(id);      // 查 DB
            int ttl = 1800 + ThreadLocalRandom.current().nextInt(300); // 随机抖动防雪崩
            if (vo == null) {
                redisTemplate.opsForValue().set(cacheKey, "", 60, TimeUnit.SECONDS); // 缓存空值
            } else {
                redisTemplate.opsForValue().set(cacheKey, JSON.toJSONString(vo), ttl, TimeUnit.SECONDS);
            }
            return vo;
        } else {
            Thread.sleep(50);                 // 未获锁: 短暂等待后重试
            return getProduct(id);
        }
    } finally {
        unlock(lockKey, uuid);                // Lua 原子解锁
    }
}
  • 效果验证 :压测对比开启前后 DB QPS(应从 5 万降至 <2000);redis-cli INFO stats 观察 keyspace_hits/(hits+misses) 命中率;故意查询不存在 ID 验证被 BF 拦截。
  • 重点注意事项:BF 需随商品上新增量更新;互斥锁的 TTL 要大于 DB 查询最坏耗时;空值缓存 TTL 要短,防止新商品上架后被长时间误判为不存在。
案例二:秒杀库存扣减(Lua 原子操作)
  • 业务背景:秒杀场景下库存扣减必须保证不超卖,数据库行锁在高并发下成为瓶颈。

  • 实现目标:库存扣减原子化、不超卖、高性能(单实例 10 万+ QPS)。

  • 前置准备 :预热阶段把库存写入 stock:{skuId};Lua 脚本预加载。

  • 实现步骤:

    1. 活动开始前 SET stock:1001 500;
    2. 请求执行 Lua 脚本:校验库存充足则扣减并返回 1,否则返回 0;
    3. 扣减成功的请求才进入后续下单流程(发 MQ 异步创建订单);
    4. 活动结束后库存回写数据库。
  • 核心代码实现:

lua 复制代码
-- decr_stock.lua: 原子扣减库存
local stock = tonumber(redis.call('get', KEYS[1]))  -- 读取当前库存
if stock == nil then
    return -1                                       -- 库存键不存在(未预热)
end
if stock <= 0 then
    return 0                                        -- 已售罄, 快速失败
end
redis.call('decr', KEYS[1])                         -- 扣减 1
return 1                                            -- 扣减成功
java 复制代码
public boolean tryDecrStock(Long skuId) {
    DefaultRedisScript<Long> script = new DefaultRedisScript<>(LUA_SCRIPT, Long.class);
    Long ret = redisTemplate.execute(script,
            Collections.singletonList("stock:" + skuId)); // KEYS[1]
    return ret != null && ret == 1L;
}
  • 效果验证 :JMeter 并发 1 万请求压测,验证 GET stock:1001 最终值 = 初始库存 − 成功请求数,绝不出现负数;对比数据库行锁方案,QPS 提升 10 倍以上。
  • 重点注意事项 :Lua 脚本保持短小,禁止在脚本中执行 O(N) 命令;集群模式下库存键与用户限购键需用哈希标签归入同一槽;扣减成功但下单失败时要有库存回补机制(事务消息或本地消息表保证最终一致)。
案例三:订单超时自动关闭(ZSet 延迟队列)
  • 业务背景:用户下单后 30 分钟未支付需自动关闭订单并释放库存。定时扫表方案延迟高且扫全表压力大。

  • 实现目标:秒级精度的延迟触发、不重复消费、可水平扩展。

  • 前置准备:下单时向 ZSet 写入"到期时间戳 + 订单号";多个消费实例用分布式锁或分片竞争消费。

  • 实现步骤:

    1. 下单成功:ZADD delay:order <now+1800s 的毫秒时间戳> <orderId>;
    2. 支付成功:ZREM delay:order <orderId> 移除;
    3. 消费线程轮询取出到期元素,原子摘除后执行关单逻辑。
  • 核心代码实现:

lua 复制代码
-- pop_expired.lua: 原子取出并删除到期任务, 防止多消费者重复消费
local items = redis.call('zrangebyscore', KEYS[1], '0', ARGV[1], 'LIMIT', 0, ARGV[2])
if #items > 0 then
    redis.call('zrem', KEYS[1], unpack(items))  -- 取到即删, 独占消费
end
return items
java 复制代码
@Scheduled(fixedDelay = 500)   // 每 500ms 轮询一次
public void pollExpiredOrders() {
    long now = System.currentTimeMillis();
    List<String> orderIds = redisTemplate.execute(POP_SCRIPT,
            List.of("delay:order"), String.valueOf(now), "100");
    for (String orderId : orderIds) {
        orderService.closeIfUnpaid(orderId);   // 关单前二次校验支付状态(幂等)
    }
}
  • 效果验证:构造测试订单不支付,观察 30 分钟 ±1 秒内订单状态流转为"已关闭"且库存回补;多实例部署下通过日志确认同一订单只被一个实例处理。
  • 重点注意事项 :关单逻辑必须幂等(支付回调与关单任务可能竞争);轮询间隔决定精度,业务要求高可缩短间隔或分片多键;超大规模场景(亿级延迟任务)应评估 RocketMQ 延迟消息或时间轮方案,Redis ZSet 单键容量与轮询开销有上限。

3.2 行业通用规范

(1)键设计规范

  • 命名格式统一为 业务名:对象名:ID,如 mall:product:1001,禁止含空格与特殊字符;
  • 键长度控制在 128 字节内,值控制在 10KB 内(超过即视为大 key 预警);
  • 所有键必须设置 TTL(除明确需要常驻的数据),且 TTL 加随机抖动;
  • 禁止使用 keys 前缀保留字,避免与系统键冲突。

(2)命令使用规范

  • 生产禁用 KEYS *、FLUSHALL、FLUSHDB,用 SCAN 渐进遍历,rename-command 屏蔽高危命令;
  • 避免 O(N) 命令作用于大集合:HGETALL/LRANGE 0 -1/SMEMBERS 改用 HSCAN/分页 LRANGE/SSCAN;
  • 删除大 key 用 UNLINK(4.0+ 异步删除),禁用同步 DEL;
  • 批量操作用 MGET/MSET、Pipeline 减少 RTT,单次 Pipeline 命令数控制在千级以内。

(3)内存与配置规范

  • 必须配置 maxmemory(建议物理内存的 60%~70%,为 COW 与复制缓冲留余量)+ 明确的淘汰策略;
  • 开启混合持久化 aof-use-rdb-preamble yes,appendfsync everysec;
  • 单实例内存建议不超过 10~16GB,更大容量走 Cluster 分片。

(4)客户端与连接规范

  • 必须使用连接池(Lettuce/JedisPool),设置合理的 maxTotal/maxIdle 与超时;
  • 命令必须设置读超时,避免网络异常时线程永久阻塞;
  • 客户端禁止在 Redis 中存储未序列化的对象,统一 JSON/Protobuf。

(5)监控规范

  • 必监控指标:内存水位(used_memory)、命中率(keyspace_hits)、阻塞客户端数(blocked_clients)、慢查询数(slowlog)、主从延迟(slave_repl_offset 差值)、fork 耗时(latest_fork_usec);
  • 定期执行 redis-cli --bigkeys / MEMORY USAGE 巡检大 key。

3.3 高频踩坑案例

踩坑一:大 key 引发集群抖动

  • 错误现象:某分片周期性 CPU 100%、其他命令超时,监控显示单键 2GB(一个存了千万级成员的 Hash)。
  • 核心原因 :大 key 的任何读写都是 O(N),一次 HGETALL 阻塞主线程数秒;删除时同步 DEL 触发长时间阻塞;主从复制与 Cluster 迁移大 key 时流量放大。
  • 修复方案 :拆分为多个小 key(user:1000:friends:{0..N} 按 ID 取模);删除改用 UNLINK 或 HSCAN 分批 HDEL。
  • 规避技巧 :上线前评审数据结构,值超 10KB、集合超 5000 元素即告警;定期 --bigkeys 扫描。

踩坑二:KEYS * 打挂生产

  • 错误现象 :运维执行 KEYS * 统计数量,服务瞬间全部超时数十秒。
  • 核心原因 :KEYS 是 O(N) 阻塞命令,千万级 key 遍历期间主线程无法处理任何请求。
  • 修复方案 :立即停止;改用 SCAN 0 COUNT 100 游标迭代,或离线分析 RDB 文件(rdb-tools)。
  • 规避技巧 :rename-command KEYS "" 直接禁用;统计需求用 INFO keyspace 或 DBSIZE。

踩坑三:缓存与数据库双写不一致

  • 错误现象:修改商品价格后,页面长时间展示旧价格。
  • 核心原因:采用"更新 DB + 更新缓存"策略,并发下两个请求交叉执行导致旧值覆盖新值;或缓存更新失败无补偿。
  • 修复方案 :统一采用 Cache Aside 模式------读:先缓存后 DB 回填;写:先更新 DB,再删除缓存;对一致性要求高的加"延迟双删"(删缓存 → 更新 DB → 延迟 500ms 再删一次);终极方案是订阅 binlog(Canal)异步删缓存,保证最终一致。
  • 规避技巧:缓存层永远定位为"可丢失的加速副本",以数据库为准;为所有缓存设置兜底 TTL。

踩坑四:maxmemory 配置缺失导致 OOM 被杀

  • 错误现象:Redis 进程被操作系统 OOM Killer 杀掉,服务雪崩。
  • 核心原因 :未设 maxmemory,数据无限增长撑爆物理内存;或设了但忽略了复制缓冲区、COW 的额外开销。
  • 修复方案 :配置 maxmemory + allkeys-lru;监控 used_memory_rss(含碎片与 COW)而非仅 used_memory;开启 activedefrag 治理碎片。
  • 规避技巧:容量规划预留 30% 余量;内存水位 80% 即告警扩容。

踩坑五:连接池耗尽引发应用侧雪崩

  • 错误现象 :应用日志大量 Could not get a resource from the pool,接口全部超时,但 Redis 本身正常。
  • 核心原因:慢命令/网络抖动导致连接归还变慢,连接池过小(默认 8)被瞬间耗尽,线程全部阻塞等待。
  • 修复方案 :调大 maxTotal(按峰值并发估算);设置 maxWaitMillis 快速失败而非无限等待;排查并消除慢命令(slowlog get)。
  • 规避技巧:连接池大小压测定标;接入熔断器,Redis 异常时快速降级走数据库限流。

四、分层面试题库

4.1 基础入门题

问:什么是 Redis?它有哪些核心特点?

标准答案:Redis 是一款开源的、基于内存运行并支持持久化的高性能 Key-Value 型 NoSQL 数据库,用 ANSI C 编写。核心特点有五点:一是纯内存操作,读写延迟微秒级,单机 QPS 可达 10 万+;二是支持 String、Hash、List、Set、ZSet 五大基础数据类型及 Bitmap、HyperLogLog、GEO、Stream 等扩展类型;三是提供 RDB 与 AOF 两种持久化方式,重启数据不丢失;四是支持主从复制、哨兵、Cluster 集群等高可用架构;五是单线程命令模型保证操作原子性,还支持事务、Lua 脚本、发布订阅等丰富特性。它常被用作缓存、分布式锁、排行榜、计数器和轻量消息队列。

问:Redis 支持哪些数据类型?分别有什么应用场景?

标准答案:五大基础类型:①String------最简单,用于缓存对象 JSON、计数器(INCR)、分布式锁、Session 共享;②Hash------字段级读写的字典,适合存储用户、商品等对象,比存 JSON 更省且可局部更新;③List------双向链表(底层 quicklist),支持两端推拉,用于消息队列、最新列表(如朋友圈时间线);④Set------无序去重集合,支持交并差运算,用于抽奖(SRANDMEMBER/SPOP)、共同好友、标签去重;⑤ZSet------带分值的有序集合,用于排行榜、延迟队列、滑动窗口限流。扩展类型:Bitmap 做签到/在线状态统计,HyperLogLog 做亿级 UV 基数统计(误差 0.81%),GEO 做附近的人,Stream 做可靠消息队列。

问:Redis 为什么这么快?

标准答案:五个原因:第一,完全基于内存操作,避免磁盘 I/O;第二,核心命令处理是单线程模型,没有锁竞争和上下文切换,命令执行天然原子;第三,使用 I/O 多路复用(epoll)的事件驱动架构,单线程即可处理海量并发连接;第四,底层精心设计的数据结构------SDS、跳表、压缩列表、渐进式 rehash 的哈希表等,针对场景做了极致优化;第五,Redis 6.0 引入多线程 I/O,把网络读写和协议解析交给多线程,命令执行仍单线程,进一步释放网络吞吐。综合起来就是"内存 + 单线程无锁 + 多路复用 + 高效数据结构"。

问:Redis 和 Memcached 有什么区别?

标准答案:六个维度:①数据类型:Memcached 只支持简单 String,Redis 支持五大类型加扩展类型;②持久化:Memcached 无持久化,重启数据全丢,Redis 有 RDB/AOF;③线程模型:Memcached 是多线程,Redis 命令执行单线程(6.0 起 I/O 多线程);④内存管理:Redis 用 jemalloc 并有对象共享、紧凑编码等优化,内存利用率更高;⑤高可用:Redis 原生支持主从、哨兵、集群,Memcached 无原生方案;⑥特性:Redis 支持事务、Lua、发布订阅。结论是绝大多数场景选 Redis,只有纯简单 KV 缓存且要榨取多核性能时才考虑 Memcached。

问:Redis 的持久化方式有哪些?RDB 和 AOF 怎么选?

标准答案:两种基础方式加一种组合。RDB 是某一时刻的全量内存快照,通过 BGSAVE fork 子进程生成二进制文件,优点是文件紧凑、恢复极快,适合备份与灾备,缺点是两次快照之间宕机会丢数据,fork 大实例时有抖动。AOF 以追加方式记录每条写命令,支持 always/everysec/no 三种刷盘策略,everysec 最多丢 1 秒数据,安全性高,缺点是文件大、恢复慢,可通过 AOF 重写压缩。生产环境推荐 4.0+ 的混合持久化:AOF 重写时前半段存 RDB 格式、后半段追加增量命令,兼顾恢复速度和数据安全。

问:Redis 的过期键是怎么删除的?

标准答案:Redis 采用"惰性删除 + 定期删除"组合策略。惰性删除是指任何命令访问一个键时,先检查它是否过期,过期就删除再返回,这样不浪费 CPU,但过期键可能长期占内存。定期删除是弥补:Redis 的时间事件每隔 100 毫秒执行一次 activeExpireCycle,从每个库的过期字典里随机抽取 20 个键检查,删除其中已过期的;如果过期比例超过 25% 就继续抽,但单次执行不超过时间上限(默认 25 毫秒),超时则下轮继续,保证不阻塞主线程。另外从库不会主动删除过期键,它等待主库同步 DEL 命令,以保证主从数据一致。

问:Redis 的内存淘汰策略有哪些?

标准答案:当内存达到 maxmemory 上限且写命令到来时触发淘汰,共八种:按候选范围分两类,allkeys 系列作用于所有键,volatile 系列只作用于设置了 TTL 的键。具体有:allkeys-lru(近似最近最少使用,通用缓存首选)、volatile-lru、allkeys-lfu(4.0+,按访问频率淘汰,适合热点数据)、volatile-lfu、allkeys-random、volatile-random、volatile-ttl(优先淘汰剩余存活时间最短的)、以及 noeviction(不淘汰,写操作直接报 OOM 错误)。实现上 Redis 用采样近似 LRU:随机取 5 个键比较空闲时间,配合淘汰池,效果接近严格 LRU 但开销极小。

问:什么是缓存穿透、击穿、雪崩?怎么解决?

标准答案:三者都是缓存失效导致流量打到数据库。穿透是查询根本不存在的数据,缓存和数据库都没有,请求每次都打到 DB,解法是缓存空对象(短 TTL)或前置布隆过滤器拦截非法 ID。击穿是单个热点键过期瞬间,海量并发同时查 DB,解法是互斥锁(只让一个线程重建缓存)或逻辑过期(不设 TTL,异步刷新)。雪崩是大面积键同时过期或缓存服务宕机,解法是过期时间加随机抖动、热点永不过期、多级缓存(本地缓存 + Redis)、高可用架构(哨兵/集群)加熔断限流降级。一句话区分:穿透查"没有的",击穿是"一个热点挂了",雪崩是"一大片都挂了"。

问:Redis 分布式锁怎么实现?要注意什么?

标准答案 :加锁用一条原子命令 SET lock_key unique_id NX EX 30:NX 保证互斥,EX 设置过期时间防止持锁者崩溃后死锁,unique_id 标识持有者防止误删别人的锁。解锁必须用 Lua 脚本原子地"先判断值是不是自己的再删除"。三个进阶问题:一是锁续期,业务执行时间不确定时用看门狗机制(Redisson 默认 30 秒锁、每 10 秒续期一次);二是可重入,Redisson 用 Hash 结构记录重入计数;三是主从切换可能丢锁,极端场景可评估 Redlock(向多数独立实例加锁),但更务实的做法是接受 AP 特性并用业务幂等兜底。

问:Redis 的高可用方案有哪些?

标准答案:三级演进。第一级主从复制:从节点复制主节点数据,实现读写分离和数据冗余,但主节点故障需人工切换。第二级哨兵 Sentinel:独立的哨兵进程集群监控主从健康,主节点客观下线后自动选举新主并通知客户端,解决自动故障转移,但写能力和容量仍受限于单机。第三级 Redis Cluster:数据分成 16384 个哈希槽分布到多个主节点,每个主节点带从节点,兼具数据分片、写能力扩展和自动故障转移,是大数据量场景的官方方案。选型原则:数据量单机放得下用哨兵,放不下或写压力高用 Cluster。

4.2 原理深挖题

问:Redis 单线程模型具体是怎么工作的?6.0 为什么引入多线程?

标准答案:Redis 基于 ae 事件库实现 Reactor 模型:主线程运行事件循环,通过 epoll 监听大量 socket 连接。可读事件触发时读取请求、解析 RESP 协议,命令进入执行器按顺序执行(单线程保证无并发竞争),执行结果写回客户端;同时穿插时间事件处理过期键、持久化等后台任务调度。单线程的瓶颈不在 CPU 而在网络 I/O------大并发下协议解析、读写 socket 消耗大量 CPU。所以 6.0 把网络 I/O 环节多线程化:主线程把已建立连接的读写、协议解析分发给 I/O 线程并行处理,但命令执行仍然回到主线程串行,既提升吞吐又保留原子性。注意多线程只用于网络,不用于命令执行,"Redis 是单线程"的说法针对的始终是命令执行模型。

问:详细讲讲渐进式 rehash?

标准答案 :Redis 的字典内部维护两张哈希表 ht0 和 ht1。当负载因子达到阈值(正常情况 used/size ≥ 1,BGSAVE 期间放宽到 5 以避免 COW 内存翻倍)触发扩容:分配大小为第一个大于 used×2 的 2 的幂的 ht1,然后不是一次性迁移,而是把 rehashidx 置 0,分两步渐进迁移:一是对字典的每次增删改查操作顺带迁移一个桶;二是后台定时任务每 100ms 花 1ms 迁移。迁移期间查询先查 ht0 再查 ht1,新键一律写入 ht1,保证 ht0 只减不增,最终迁移完成后释放 ht0、ht1 转正。这样把 O(N) 的一次性开销均摊到每次操作,避免主线程长时间阻塞。缩容同理。

问:ZSet 为什么用跳表而不用红黑树?

标准答案:跳表是多层链表结构:插入时从最高层头节点逐层查找落点,新节点层数以 1/4 概率逐层晋升(上限 32 层),查找、插入、删除平均 O(logN)。选它而非红黑树有四个原因:第一,范围查询是 ZSet 的核心场景(ZRANGEBYSCORE/ZRANGE),跳表定位到起点后沿底层链表顺序遍历即可,红黑树需要中序遍历且涉及父指针回溯,效率低;第二,实现简单,插入删除只需局部改指针,没有旋转重平衡的复杂逻辑,antirez 本人也说过跳表实现不比平衡树复杂且更易调试;第三,跳表的 span 跨度字段可以 O(logN) 计算元素排名(ZRANK),红黑树要额外维护子树大小;第四,通过调整晋升概率可以灵活权衡时间与空间。另外 ZSet 底层是"跳表 + 字典"双结构:字典提供 O(1) 的按成员查分值,跳表负责排序和范围操作,两者共享成员字符串指针。

问:RDB 的 fork + COW 原理?子进程为什么能看到一致的数据?

标准答案:BGSAVE 时主进程调用 fork 创建子进程。fork 之后父子进程共享同一份物理内存页,内核把页表项标记为只读。子进程遍历内存生成 RDB 文件,它看到的是 fork 瞬间的一致性快照。此期间主进程继续服务客户端,当某个内存页被写操作修改时,触发缺页中断,内核才把该页复制一份------这就是写时复制(Copy-On-Write):父进程用新页,子进程仍持有旧页。所以 RDB 的额外内存开销不是翻倍,而是与"快照期间被修改的页数量"成正比。这也解释了三个工程问题:为什么快照期间写流量越大内存峰值越高(COW 复制的页越多);为什么 fork 本身耗时与实例内存成正比(要复制页表);为什么建议在业务低峰做 BGSAVE。

问:AOF 重写是怎么工作的?重写期间的新写入怎么处理?

标准答案:AOF 文件随命令累积会膨胀(比如 1000 次 INCR 记了 1000 条),重写机制通过 fork 子进程、读取当前内存状态直接生成最精简的命令集来压缩文件------注意重写不读旧 AOF 文件。重写期间客户端的新写命令会同时进入两个缓冲:AOF 缓冲区(照常追加到旧 AOF 文件,保证重写失败不丢数据)和 AOF 重写缓冲区。子进程完成新文件写入后,主进程把重写缓冲区中积累的增量命令追加到新 AOF 文件,然后原子 rename 替换。为避免重写期间磁盘 I/O 竞争,主进程对旧 AOF 的 fsync 会降频为每 2 秒一次。Redis 7.0 改为 Multi Part AOF(base + incr + manifest),重写期间不再需要重写缓冲区,消除了收尾时的阻塞窗口。

问:Redis 的近似 LRU 是怎么实现的?和 LFU 有什么区别?

标准答案:严格 LRU 需要维护全局双向链表,内存开销大,Redis 用采样近似:每个键的 redisObject 里有 24 位 lru 字段记录访问时间。淘汰时随机采样 maxmemory-samples 个(默认 5)键,比较空闲时间,淘汰最旧的;3.0 起引入 16 个位置的淘汰池,新采样键与池中键比较替换,使近似效果非常接近严格 LRU。LFU 是 4.0 引入的,把 24 位拆成 16 位上次访问时间 + 8 位对数频率计数器:访问频率越高计数越难增长,且计数随时间衰减。两者区别在于:LRU 看"最近一次访问时间",无法区分"一直热的数据"和"刚被偶然访问一次的冷数据"------全表扫描这类一次性访问会污染 LRU,把真正的热点挤出去;LFU 看"访问频率",能识别真正的热点。所以有缓存污染风险的场景选 allkeys-lfu。

问:详细讲讲主从复制的 PSYNC2 流程?

标准答案 :分全量和增量两条路径。首次同步时从节点发送 PSYNC ? -1,主节点回复 +FULLRESYNC replid offset,然后 BGSAVE 生成 RDB,期间新的写命令存入复制缓冲区;RDB 发给从节点,从节点清空旧数据加载,主节点再把复制缓冲区的增量命令发过去,之后进入持续的命令传播阶段。断线重连时从节点发送 PSYNC <replid> <offset>:如果 replid 匹配且 offset 还在复制积压缓冲区(环形缓冲,默认 1MB)内,主节点返回 +CONTINUE 只补发缺失命令,代价极小;否则退化为全量。4.0 引入的 replid2 解决故障切换问题:从节点晋升为主时保留前任的 replid 作为 replid2,这样其他从节点重连时仍能部分重同步,避免切换引发全量风暴。工程上要把 repl_backlog 调大(按断线时长 × 写入速率估算),避免网络抖动就触发全量。

问:Sentinel 故障转移的完整过程?

标准答案:分监控、判定、选举、转移四步。监控:每个哨兵每秒向主从和其他哨兵发 PING。判定:某实例在 down-after-milliseconds 内没有效回复,该哨兵标记其主观下线 SDOWN;如果下线的是主节点,哨兵会询问其他哨兵,达到 quorum 个哨兵都认为下线则升级为客观下线 ODOWN------从节点和哨兵自己只会有主观下线。选举:哨兵之间用 Raft 算法选 Leader,获得半数以上且不少于 quorum 票者胜出,由它执行故障转移。转移:Leader 从从节点中选新主,标准依次是排除不健康的、slave-priority 数值小的优先、复制偏移量大的(数据最新)优先、runid 小的兜底;然后向新主发 SLAVEOF NO ONE,让其他从节点复制新主,最后通过发布订阅通知客户端新地址。还要注意脑裂风险:网络分区时旧主可能被降级后仍以主身份写,可配 min-replicas-to-write 缓解。

问:Cluster 的哈希槽是怎么工作的?为什么是 16384 个?MOVED 和 ASK 有什么区别?

标准答案 :集群把数据分成 16384 个槽,每个键用 CRC16(key) % 16384 映射到槽,槽分配给各主节点;如果键包含 {...} 哈希标签,只对大括号内的内容计算哈希,可以让相关键强制落同一槽以支持多键操作。选 16384 的原因:节点间心跳包要携带自己的槽位图 bitmap,16384 位是 2KB 可接受,65536 位就是 8KB 心跳包太大;官方建议集群不超过 1000 个节点,16384 个槽足够分(平均每节点至少 16 个)。路由方面:客户端可以向任意节点发请求,键不归该节点管时返回 MOVED 并带目标地址,客户端更新本地槽缓存后重定向------这是永久性的;槽迁移过程中返回 ASK 重定向,客户端先发 ASKING 再发命令到目标节点,但不更新本地缓存,因为迁移未完成、键的归属未定。

问:Redis 事务为什么不支持回滚?WATCH 的原理?Lua 为什么是原子的?

标准答案:三个问题分开答。事务不回滚:Redis 认为命令运行时失败(如对 String 执行 LPUSH)通常是编程错误,是客户端该发现的问题,回滚掩盖不了错误;且不做回滚让事务实现保持简单、执行保持高效,所以事务中某条命令失败,其余命令照常执行。但注意编译期错误(命令拼错)会导致整个事务被丢弃。WATCH 原理:WATCH 把键加入客户端的监视列表,EXEC 时服务器检查被监视键自 WATCH 后是否被修改过,修改过则 EXEC 返回 nil 事务失败,客户端可重试------这是 CAS 乐观锁实现。Lua 原子性:Redis 把 EVAL 的整个脚本当作一条命令,在主线程的事件循环中执行,执行期间不会穿插任何其他客户端的命令,所以脚本内的多条命令对外表现为原子操作;代价是长脚本会阻塞所有请求,脚本必须短平快。

4.3 场景题

问:线上 Redis 突然变慢,所有命令延迟飙升,如何排查?

解题思路与方案 :按"先看监控、再定位命令、后查资源"三步走。第一步看监控面板:CPU 是否打满、内存水位、是否有主从全量同步在进行、fork 耗时(latest_fork_usec)是否异常------BGSAVE 的 fork 和 COW 是常见抖动源。第二步用 slowlog get 20 查慢查询,定位是否有 O(N) 命令(KEYS、HGETALL 大 Hash、SORT 大集合);用 redis-cli --bigkeys 或离线分析 RDB 找大 key,大 key 的任何操作都会阻塞主线程。第三步查 INFO clients 看是否有输出缓冲区堆积、INFO persistence 看 AOF fsync 是否卡磁盘(aof_delayed_fsync)、INFO replication 看是否在做大流量同步。对应处理:慢命令改写或拆分、大 key 拆分并 UNLINK 删除、把 BGSAVE 挪到低峰、磁盘换 SSD。最后用 redis-cli --latency 持续采样验证恢复。

问:如何保证缓存与数据库的双写一致性?

解题思路与方案:先明确缓存的定位是"可丢失的加速副本",追求的是最终一致而非强一致。标准方案是 Cache Aside 模式:读时先读缓存,未命中读 DB 回填;写时先更新数据库,再删除缓存(而不是更新缓存)。为什么删而不是更新:并发写时更新缓存可能出现旧值覆盖新值,删除则让下次读自然重建。为什么先更 DB 再删缓存:先删缓存再更 DB 的窗口期内,读请求会把旧值重新回填。对一致性要求更高的场景加两层保险:一是延迟双删------删缓存、更新 DB、延迟几百毫秒再删一次缓存,覆盖窗口期回填的旧值;二是订阅 binlog(Canal)异步删除缓存,即使应用侧删除失败也有兜底。所有缓存必须设置兜底 TTL,保证最坏情况下最终一致。金融级强一致场景不要依赖缓存,直接读库加锁。

问:设计一个日活 10 亿级的用户签到统计系统?

解题思路与方案 :核心是选对数据结构。方案一 Bitmap:每个用户每天一个位,SETBIT sign:{userId}:{yyyyMM} (day-1) 1,单用户单月仅 4 字节,查当月签到天数用 BITCOUNT,连续签到用位运算(把多天的 bitmap AND 后数 1)。10 亿用户按月分键,单键很小,无大 key 问题。方案二如果只关心"总签到人数"这类基数统计,用 HyperLogLog,12KB 存亿级基数,误差 0.81%。容量与扩展:按用户 ID 哈希分片到 Cluster 多个槽;月初自动滚动新键、旧键设 TTL 归档。性能优化:签到是写多读少,可先在应用层本地聚合再批量 SETBIT;统计报表类查询走离线(RDB 导出到数仓),不打在线库。这个方案比用 MySQL 存签到记录省两个数量级的存储。

问:Redis 集群需要在线扩容,如何平滑迁移?

解题思路与方案 :以 3 主 3 从扩到 4 主 4 从为例。第一步:新节点以空节点身份加入集群(redis-cli --cluster add-node),与现有节点握手同步拓扑。第二步:给新主节点挂一个从节点保证高可用。第三步:执行 reshard(redis-cli --cluster reshard),把部分槽从旧主迁移到新主:迁移以槽为单位,槽内键逐个 MIGRATE,迁移中通过 ASK 重定向保证读写不中断;建议低峰执行并限速(pipeline 参数),每迁完一个槽用 CLUSTER SETSLOT 确认归属。第四步:观察 CLUSTER NODES 确认槽分布均衡、INFO memory 各节点水位接近,最后验证业务读写正常。回滚预案:迁移中途出问题可以把槽迁回原主。关键注意点:迁移期间避免大 key(MIGRATE 是同步阻塞的)、客户端必须支持重定向、扩容后记得给新主补从节点。

问:设计一个秒杀系统,Redis 在其中承担什么角色?

解题思路与方案 :秒杀的流量模型是瞬时洪峰、绝大多数请求注定失败,设计原则是"把请求尽量挡在上游"。分层拦截:前端按钮置灰防重复提交 → 网关按用户限流(Redis + Lua 的令牌桶)→ 应用层本地缓存标记"已售罄"直接拒绝后续请求。库存核心:活动前预热 SET stock:{sku} N,请求用 Lua 脚本原子判断并扣减(库存 ≤0 快速返回),扣减成功才发 MQ 异步创建订单------Redis 挡住 99% 的无效流量,数据库只处理真实成交。防超卖靠 Lua 原子性,防重复下单靠 SETNX user:{uid}:{sku} 1。兜底:库存扣减成功但下单失败要回补(事务消息);数据库层用乐观锁(UPDATE ... WHERE stock > 0)做最终防线。容量上热点库存键是典型热 key,可拆成多个分片库存(stock:sku:0~7)分散到不同分片。

问:Redis 发生 OOM,如何排查和恢复?

解题思路与方案 :先看日志确认 OOM 类型------是 Redis 报 OOM command not allowed(达到 maxmemory 且 noeviction)还是进程被系统 OOM Killer 杀掉(物理内存耗尽)。前者查 INFO memory:used_memory 是否贴近 maxmemory,keyspace 键数量是否异常增长------定位是数据自然增长还是键没设 TTL 导致堆积,用 --bigkeys 找大 key,处理是清理无主键、设置合理淘汰策略、扩容。后者更隐蔽:used_memory 可能不高,但 used_memory_rss 很高,说明是内存碎片(开 activedefrag yes)或 fork 的 COW 放大(BGSAVE 期间写流量大)或复制缓冲区堆积(client-output-buffer-limit replica 配置过大、从节点过多),处理是错峰持久化、收紧缓冲区配置、减少从节点数量。恢复流程:若进程已被杀,优先从 RDB/AOF 恢复数据重启实例;若开启了混合持久化,恢复速度最快。恢复后立即检查 maxmemory 是否配置、淘汰策略是否合理,补上监控告警(内存水位 80% 告警),防止再次发生。根本治理:容量规划时预留 30% 余量给 COW 和碎片,maxmemory 设为物理内存的 60%~70%。

问:如何用 Redis 实现一个可靠的延迟队列?和 MQ 延迟消息怎么选型?

解题思路与方案 :Redis 方案用 ZSet:下单时 ZADD delay:order <到期时间戳> <orderId>,消费端轮询 ZRANGEBYSCORE delay:order 0 <now> LIMIT 0 N 取到期任务,用 Lua 脚本原子"取出并删除"防止多消费者重复消费,然后执行关单逻辑。关键设计点:①消费必须幂等------支付回调和关单任务可能竞争,关单前二次校验订单状态;②轮询间隔决定精度,500ms~1s 是常见选择;③多实例部署时用分布式锁或按 orderId 哈希分片到多个 ZSet 键,避免所有实例抢同一个键;④ZSet 单键容量有上限,亿级任务要分片。与 MQ 延迟消息(RocketMQ)的选型:任务量在百万级以内、精度要求秒级、团队已有 Redis 基建,用 ZSet 方案简单可控;任务量亿级、需要精确的延迟等级、需要消息回溯和死信处理,用 RocketMQ 延迟消息更合适。Redis 方案的优势是轻量、延迟低、不引入新组件;劣势是没有原生的重试、死信、消息回溯能力,需要自行实现。

问:Redis 热点 key 导致单分片过载,怎么解决?

解题思路与方案 :热点 key 的本质是所有请求都打到同一个槽、同一个节点,Cluster 的分片机制无法分散。解决分三层:第一层,应用侧本地缓存------用 Caffeine/Guava Cache 在 JVM 内缓存热点数据,TTL 设 1~5 秒,绝大部分请求不出进程,Redis 压力降一到两个数量级;这是最优先、成本最低的方案。第二层,读写分离------热点 key 的读请求分散到多个从节点,主节点只承担写;适合读多写少的场景。第三层,key 拆分------把一个热点 key 拆成 N 个副本(如 hot:product:1001:{0..7}),读请求随机路由到某个副本,写请求同时更新所有副本或用 Lua 保证一致;这样流量被分散到多个槽、多个节点。拆分副本数根据 QPS 和单节点承载能力估算。辅助手段:用 redis-cli --hotkeys(需 LFU 策略)或 MONITOR 采样提前发现热点;接入层做请求合并(相同 key 的并发请求只发一次 Redis)。

问:设计一个基于 Redis 的分布式限流方案,支持滑动窗口和令牌桶两种模式。

解题思路与方案 :两种模式分别实现。滑动窗口:用 ZSet,member 是请求唯一 ID(如 UUID),score 是请求时间戳。每次请求:①ZREMRANGEBYSCORE key 0 <now-window> 清除窗口外的旧记录;②ZCARD key 统计窗口内请求数;③未超限则 ZADD key <now> <uuid> 并 EXPIRE key <window+1>;超限则拒绝。整个逻辑封装在一个 Lua 脚本里保证原子性。令牌桶:用 Hash 存两个字段------tokens(当前令牌数)和 last_refill(上次补充时间)。每次请求:①计算距上次补充经过的时间,按速率补充令牌(不超过桶容量);②令牌 ≥1 则扣减并放行,否则拒绝;同样用 Lua 脚本原子执行。集群模式下,限流键用哈希标签 {user:10001}:ratelimit 保证落同一槽。精度与性能权衡:滑动窗口精度取决于窗口大小和 ZSet 内存,高 QPS 场景可改用固定窗口计数器(INCR + EXPIRE)牺牲精度换性能;令牌桶适合允许突发流量的场景。兜底:Redis 不可用时降级为本地限流(Guava RateLimiter),保证服务不完全裸奔。

问:Redis 主从切换后,客户端如何感知新主?如何避免切换期间的数据丢失?

解题思路与方案 :客户端感知分两种架构。哨兵模式:客户端连接哨兵集群,订阅 +switch-master 频道;主节点故障转移完成后,哨兵通过该频道广播新主地址,客户端收到后重建连接池。Lettuce 和 Jedis 都有原生 Sentinel 客户端支持,自动完成这一过程。Cluster 模式:客户端维护本地槽缓存表,收到 MOVED 重定向后自动更新槽映射,无需额外感知机制。切换期间数据丢失的根源:主节点故障时,复制缓冲区中尚未同步到从节点的写命令会丢失(异步复制的固有缺陷)。缓解措施:①配置 min-replicas-to-write 1 + min-replicas-max-lag 10,主节点在至少一个从节点 10 秒内有响应时才接受写入,否则拒绝写------牺牲可用性换数据安全性;②repl-backlog-size 调大,减少断线重连后全量同步的概率;③业务层对关键写操作做幂等设计,切换后重试不产生副作用;④对数据一致性要求极高的场景(如金融),考虑同步复制方案(Redis 本身不支持强同步,可评估 Redis Enterprise 或改用数据库)。核心认知:Redis 复制是异步的,主从切换存在秒级数据丢失窗口,这是 AP 系统的固有取舍,架构设计时必须接受并在业务层兜底。

相关推荐
海绵宝宝转agent1 小时前
2026-10-14算法+面经总结
java·笔记·算法·缓存
旋生万物2 小时前
分形自相似:微服务、缓存、Prompt 模板为什么长一样
人工智能·缓存·微服务·云原生·prompt
计算机就业还在扩大2 小时前
Redis中的setnx是原子性的?
数据库·redis·缓存
rannn_1113 小时前
项目缓存实战:热点 Key 击穿、缓存穿透与多级缓存架构
java·缓存·架构
Ruiery4 小时前
Linux 6.6内核内存管理深度解析(五):page cache(页缓存)— 读缓存在内存、写延迟回写
linux·运维·服务器·缓存
Java爱好狂.4 小时前
Java小白如何三天速通redis?
java·数据库·redis·架构师·分布式锁·java面试·java程序员
一条泥憨鱼20 小时前
【Redis | 黑马点评】短信登录功能 源码深度分析
java·redis·后端·session·黑马点评
垆边人似月.1 天前
华为题:LRU 缓存
缓存·华为
m0_752035631 天前
Redis 数据结构总结
数据结构·数据库·redis