Redis 全面解析:从数据结构到主从/哨兵/集群、分布式锁与面试高频考点,代码到原理一次讲透

这篇不是"Redis 命令速查表",而是按**「为什么快 → 数据怎么存 → 怎么保证不丢 → 怎么保证高可用 → 怎么扛住并发 → 怎么用对场景」**这条主线,把 Redis 的核心知识体系串起来。全文含 40+ 段可执行代码(Lua / Java / redis-cli),读完可以直接上手生产配置和应付面试深挖。


目录

  1. [Redis 为什么这么快:六层原因拆解](#Redis 为什么这么快:六层原因拆解)
  2. 数据类型与底层数据结构(面试必考对照表)
  3. [持久化:RDB / AOF / 混合,到底怎么选](#持久化:RDB / AOF / 混合,到底怎么选)
  4. [内存管理:过期删除 + 8 种淘汰策略](#内存管理:过期删除 + 8 种淘汰策略)
  5. 主从复制:第一次全量,之后增量
  6. [哨兵 Sentinel:自动故障转移是怎么做到的](#哨兵 Sentinel:自动故障转移是怎么做到的)
  7. [Redis Cluster:16384 个槽是怎么分的](#Redis Cluster:16384 个槽是怎么分的)
  8. [分布式锁:从 SETNX 到 Redisson 看门狗](#分布式锁:从 SETNX 到 Redisson 看门狗)
  9. [九大典型使用场景 + 完整代码](#九大典型使用场景 + 完整代码)
  10. 缓存三兄弟与一致性方案
  11. [面试高频 30 问快答](#面试高频 30 问快答)
  12. [生产上线 Checklist](#生产上线 Checklist)

一、Redis 为什么这么快

官方 benchmark:单机 10 万 QPS 级别。原因不是单一的"内存",而是六层叠加:

复制代码
┌─────────────────────────────────────────────────────────┐
│ ① 纯内存操作        纳秒级访问,无磁盘随机 IO             │
│ ② 单线程命令执行    无锁、无上下文切换、无竞态             │
│ ③ IO 多路复用      epoll/kqueue,一个线程处理万级连接      │
│ ④ 高效数据结构      SDS/跳表/listpack/哈希表定制优化       │
│ ⑤ 网络模型优化      6.0 起多线程 IO(只负责读写+协议解析)  │
│ ⑥ 零拷贝与批处理    pipeline / MGET / Lua 减少 RTT        │
└─────────────────────────────────────────────────────────┘

1.1 单线程到底"单"在哪

很多人答错的点 :Redis 的"单线程"指的是命令执行(内存操作)是单线程,不是整个进程只有一个线程。

  • Redis 4.0:引入 lazy free (UNLINK、FLUSHALL ASYNC),用后台线程释放大对象内存;
  • Redis 6.0:引入 IO 多线程 (io-threads),网络读、写、协议解析交给多线程,命令执行仍然是单线程;
  • Redis 的 bgsave、bgrewriteaof 靠 fork 子进程完成。
ini 复制代码
# redis.conf(6.0+)
io-threads 4                 # 建议:4 核设 2-3,8 核设 6;超过 8 核收益递减
io-threads-do-reads yes      # 开启读多线程(默认只写多线程)

面试追问 :既然单线程,为什么还需要分布式锁? 答:单线程保证的是单条命令的原子性,而业务层的"读-判断-写"是多条命令,中间可能被其他客户端插入,所以仍需分布式锁或 Lua 脚本。

1.2 单线程的代价:必须知道的"慢命令"

单线程意味着一个命令卡顿,所有请求排队。生产禁用清单:

bash 复制代码
KEYS *                      # O(N) 全量遍历,百万 key 直接卡死 → 用 SCAN
FLUSHALL / FLUSHDB          # → 用 ASYNC 版本
DEL bigkey                  # 释放大对象阻塞 → 用 UNLINK
SMEMBERS bigset             # → 用 SSCAN
HGETALL bighash             # → 用 HSCAN
LRANGE biglist 0 -1         # → 分页
# 对 bigkey 操作前先探测
redis-cli --bigkeys
redis-cli --memkeys         # Redis 7.4+,按内存排序找大 key

二、数据类型与底层数据结构

2.1 五种基本类型 → 底层编码对照表(必背)

对外类型 底层编码(低版本) 底层编码(Redis 7) 转换条件
String int / embstr / raw 同左 整数用 int;≤44 字节用 embstr;更大用 raw
List ziplist / linkedlist quicklist(listpack 节点) 3.2 起统一 quicklist
Hash ziplist / hashtable listpack / hashtable 元素数 ≤512 且所有值 ≤64 字节
Set intset / hashtable 同左 元素都是整数且 ≤512 个
ZSet ziplist / skiplist+dict listpack / skiplist+dict 元素 ≤128 且 member ≤64 字节
bash 复制代码
# 实际观察编码
127.0.0.1:6379> SET a 100
127.0.0.1:6379> OBJECT ENCODING a
"int"
127.0.0.1:6379> SET b hello
127.0.0.1:6379> OBJECT ENCODING b
"embstr"
127.0.0.1:6379> HSET h f1 v1
127.0.0.1:6379> OBJECT ENCODING h
"listpack"        # Redis 7;6.x 是 ziplist

Redis 7 最重要的底层变化 :ziplist 被 listpack 全面取代。原因:ziplist 每个节点存 prevlen(前一节点长度),前一节点从 253 字节变成 254 字节会触发连锁更新(cascade update),最坏 O(N²);listpack 的节点只记录自身长度,彻底消除连锁更新。

2.2 核心结构原理速览

SDS(Simple Dynamic String)
c 复制代码
struct sdshdr {
    uint32_t len;      // 已用长度,O(1) 获取 strlen
    uint32_t alloc;    // 总分配空间(free 计算:alloc - len)
    unsigned char flags; // 头类型:sdshdr5/8/16/32/64
    char buf[];        // 字节数组,仍以 \0 结尾(兼容 C 函数)
};

三个设计价值:O(1) 取长度 、空间预分配 + 惰性释放 (减少内存重分配)、二进制安全 (用 len 判断结束而非 \0,所以能存图片/protobuf)。

跳表(Skip List)------ ZSet 的灵魂
复制代码
Level 3:      1 ────────────────────────▶ 21
Level 2:      1 ─────────▶ 9 ───────────▶ 21
Level 1:      1 ───▶ 5 ──▶ 9 ───▶ 15 ───▶ 21
Level 0:      1 → 3 → 5 → 7 → 9 → 12 → 15 → 18 → 21
                (每一层都是有序链表,上层是下层的"快速通道")

ZSet 用 跳表 + 哈希表 双结构:跳表支持范围查询(ZRANGEBYSCORE),哈希表支持 O(1) 单点查询(ZSCORE)。层数是随机生成的(ZSKIPLIST_P = 0.25,最高 32 层)。

为什么不用红黑树? 跳表实现更简单、范围查询只需遍历链表、且可以通过调整节点指针做局部更新,不需要像红黑树那样 rebalance。

哈希表渐进式 rehash

Redis 的字典有 ht0、ht1 两个哈希表。扩容时不是一次性搬迁(会阻塞),而是:

复制代码
1. 为 ht[1] 分配空间(大小为 > ht[0].used*2 的第一个 2^n)
2. rehashidx = 0
3. 每次增删改查时,顺带把 ht[0].rehashidx 位置的桶搬到 ht[1](分摊成本)
4. 全部搬完后 ht[0] = ht[1],rehashidx = -1

注意:渐进式 rehash 期间,查询要同时查两张表 ,且新增只写 ht1。

2.3 三种扩展类型

类型 底层 典型场景 误差/限制
Bitmap SDS(按位操作) 用户签到、活跃标记 1 亿用户 1 天仅 12.5MB
HyperLogLog 概率算法(分桶调和平均) UV 统计 标准误差 0.81%,单 key 12KB
GEO ZSet(Geohash 编码为 score) 附近的人/门店 底层就是 ZSet
Stream Radix Tree + listpack 消息队列(Redis 5.0+) 支持消费组、ACK、PEL
Bloom Filter 需 RedisBloom 模块 缓存穿透防护 有误判率,不可删除(Cuckoo 可删)
bash 复制代码
# HyperLogLog:UV 统计,1 亿用户只占 12KB
PFADD uv:20261102 user1 user2 user3
PFCOUNT uv:20261102              # 3
PFMERGE uv:202611 uv:20261101 uv:20261102   # 合并多天(注意:是去重后的并集)

# Bitmap:签到
SETBIT sign:202611:1001 1 1      # 用户 1001 在 11-02 签到(offset=1 表示第 2 天)
BITCOUNT sign:202611:1001        # 本月签到天数
BITFIELD sign:202611:1001 GET u30 0   # 一次性取 30 天的位图(u30 = 无符号 30 位整数)

# GEO
GEOADD shops 116.397 39.909 "王府井店" 121.473 31.230 "上海店"
GEORADIUS shops 116.40 39.90 5 km WITHDIST COUNT 10 ASC   # Redis 7 推荐用 GEOSEARCH
GEOSEARCH shops FROMLONLAT 116.40 39.90 BYRADIUS 5 km ASC COUNT 10

# Stream:真正可用的消息队列
XADD mystream * order_id 1001 status created
XGROUP CREATE mystream g1 $ MKSTREAM       # 创建消费组
XREADGROUP GROUP g1 c1 COUNT 10 BLOCK 5000 STREAMS mystream >   # 消费
XACK mystream g1 1698892800000-0           # 确认(不 ACK 会留在 PEL 里)
XPENDING mystream g1                       # 查看未确认消息

三、持久化

3.1 RDB:内存快照

bash 复制代码
# 触发方式
SAVE              # 主进程同步做,阻塞所有命令(生产禁用)
BGSAVE            # fork 子进程做,主进程继续服务
# 自动触发(redis.conf)
save 900 1        # 900 秒内至少 1 次修改
save 300 10
save 60 10000

核心原理:fork + Copy-On-Write

复制代码
┌─────────── 主进程 ───────────┐        ┌──── 子进程 ────┐
│ 内存页 A  B  C  D            │ fork() │ 共享同一份内存 │
│  修改页 B  ─────────────────▶│ 页 B 被复制一份副本给子进程
│  (COW:主进程复制原页再改)    │        │ 子进程写 RDB 的是修改前的快照
└──────────────────────────────┘        └───────────────┘

关键风险 :fork 瞬间如果内存 50GB,理论上 COW 最坏需要额外 50GB 内存。生产建议:maxmemory 设为物理内存的 60%-70%,并留足 swap 空间;同时要关闭 THP(Transparent Huge Page):

bash 复制代码
echo never > /sys/kernel/mm/transparent_hugepage/enabled

RDB 的缺点:两次快照之间宕机,会丢失这期间的全部数据。

3.2 AOF:追加写命令

ini 复制代码
appendonly yes
appendfilename "appendonly.aof"
appendfsync everysec      # always | everysec | no
auto-aof-rewrite-percentage 100   # 比上次重写后增大 100% 触发
auto-aof-rewrite-min-size 64mb
策略 行为 数据安全性 性能
always 每条命令 fsync 最多丢 1 条 差
everysec(默认) 每秒 fsync 最多丢 1 秒 好
no 交给 OS(通常 30s) 最多丢 30 秒 最好

AOF 重写(BGREWRITEAOF) :AOF 会越来越大(对同一个 key 操作 100 次就有 100 条记录)。重写不是读旧 AOF,而是直接读当前内存数据,生成"能重建当前状态"的最小命令集。

复制代码
# 重写前 AOF
SET counter 1 ... SET counter 100    (100 条)
# 重写后 AOF
SET counter 100                      (1 条)

Redis 7.0 起 AOF 拆成三部分:appendonly.aof.1.base.rdb(基础全量,RDB 格式)+ incr.aof(增量)+ manifest(清单),不再需要在内存中做协议转换,重写效率大幅提升。

3.3 混合持久化(推荐)

ini 复制代码
aof-use-rdb-preamble yes     # 4.0+,7.0 默认开启

AOF 重写时,前半段用 RDB 二进制格式,后半段用 AOF 增量命令。兼顾 RDB 的快速加载与 AOF 的低丢失。

3.4 生产怎么选

场景 推荐方案
纯缓存、可丢失 关闭持久化,或只开 RDB
一般业务(推荐) RDB + AOF(everysec) + 混合持久化
金融/不可丢 AOF always + RDB + 从库备份(性能代价大)
最佳实践 一定要有从库做持久化备份,主库可关闭持久化以换取性能

血的教训 :Redis 宕机重启后,如果 AOF 损坏,可用 redis-check-aof --fix 修复(会丢弃损坏部分);如果用 redis-check-aof 直接改,可能丢数据。


四、内存管理

4.1 过期删除策略:惰性 + 定期

ini 复制代码
hz 10      # 每秒执行 10 次后台任务(含过期扫描)
  • 惰性删除:访问 key 时才检查是否过期,过期就删。优点省 CPU,缺点过期 key 一直占内存。
  • 定期删除 :每秒 hz 次,每次从 expires 字典随机抽 20 个 key ,删除其中已过期的;如果过期比例超过 25%,则重复该过程(上限 25ms/次,避免阻塞)。

面试常问 :如果一个 key 过期了但一直没被访问,也没被随机抽中,会怎样? 答:它会一直留在内存里,直到被惰性删除或内存淘汰。所以不要以为设了 TTL 内存就一定释放。

4.2 八种内存淘汰策略

ini 复制代码
maxmemory 8gb
maxmemory-policy allkeys-lru
maxmemory-samples 5       # LRU/LFU 采样数量,越大越精确越耗 CPU
策略 范围 算法 适用
noeviction(默认) - 不淘汰,写请求报错 数据绝不能丢(但有隐患)
allkeys-lru 所有 key LRU 纯缓存首选
volatile-lru 设了 TTL 的 key LRU 需保留持久化数据
allkeys-lfu 所有 key LFU 热点稳定、有"爆款"的场景
volatile-lfu 设了 TTL 的 key LFU 同上
allkeys-random 所有 key 随机 访问均匀
volatile-random 设了 TTL 的 key 随机 -
volatile-ttl 设了 TTL 的 key 剩余 TTL 最短优先 希望快过期的先走

Redis 的 LRU 是"近似 LRU" :不是维护全局链表(太耗内存),而是每个对象记录 24 位 lru 时钟,淘汰时随机采样 N 个,淘汰其中最久未访问的。N 越大越接近真 LRU。

LFU 的实现(Redis 4.0+) :24 位拆成 16 位频率计数器 + 8 位衰减时间。

复制代码
┌─────────────────┬──────────────┐
│  16 bits LDT    │  8 bits CTR  │
│  (上次衰减时间) │  (访问频次)  │
└─────────────────┴──────────────┘
# CTR 的递增是对数型的(不是每次+1),
# lfu-log-factor 10 时,要达到 255 需约 1000 万次访问
# lfu-decay-time 1 表示每分钟衰减 1
bash 复制代码
CONFIG SET maxmemory-policy allkeys-lfu
CONFIG SET lfu-log-factor 10
CONFIG SET lfu-decay-time 1
OBJECT FREQ mykey       # 查看某个 key 的访问频次(0-255)

4.3 内存碎片

bash 复制代码
INFO memory
# mem_fragmentation_ratio = used_memory_rss / used_memory
# 1.0-1.5 正常;>1.5 碎片严重;<1.0 说明用了 swap(危险!)
ini 复制代码
activedefrag yes                        # 4.0+ 开启自动碎片整理
active-defrag-ignore-bytes 100mb
active-defrag-threshold-lower 10

五、主从复制

5.1 第一次全量同步

复制代码
① Slave: REPLICAOF 10.0.0.1 6379
② Slave ──── PSYNC ? -1 ─────────▶ Master
③ Master ── FULLRESYNC {replid} {offset} ──▶ Slave
④ Master: BGSAVE 生成 RDB ─────────▶ Slave(同时新命令写入 replication buffer)
⑤ Slave: 清空旧数据 → 加载 RDB
⑥ Master ──── 发送 replication buffer 中的增量命令 ────▶ Slave
⑦ 之后进入命令传播阶段:Master 每执行一条写命令,异步发给 Slave

5.2 断线重连的增量同步(关键优化点)

Redis 2.8 前断线重连必须全量重传 (SYNC),代价巨大。2.8 后用 PSYNC:

复制代码
Master 端:
  ┌────────────────────────────────────────┐
  │  replication backlog (环形缓冲区)       │
  │  ┌──┬──┬──┬──┬──┬──┬──┬──┬──┬──┐      │
  │  │  │  │  │  │  │  │  │  │  │  │      │
  │  └──┴──┴──┴──┴──┴──┴──┴──┴──┴──┘      │
  │      ↑ master_repl_offset              │
  │   slave 的 offset 在这个环内 → 增量同步  │
  │   slave 的 offset 已被覆盖 → 全量同步!  │
  └────────────────────────────────────────┘

生产必调 :repl-backlog-size 默认只有 1MB,主库写入一忙,从库断线几秒就被覆盖,触发全量同步(RDB 生成 + 传输,几 GB 数据直接把主库打挂)。

ini 复制代码
# 经验公式:backlog 大小 ≥ 平均每秒写命令字节数 × 最大可容忍断线秒数 × 2
repl-backlog-size 256mb       # 生产建议 128MB-512MB
repl-backlog-ttl 3600         # 从库断开后 backlog 保留时间
repl-diskless-sync yes        # 无盘复制:RDB 直接走 socket 不落盘(SSD 环境可选 no)
repl-diskless-sync-delay 5    # 等待 5s 聚合多个从库的请求,共用一份 RDB
replica-serve-stale-data yes  # 从库同步中断时继续服务旧数据(no 则报错)
replica-read-only yes         # 从库只读(默认)

5.3 复制的三个常见问题

① 主从数据不一致 :复制是异步的。INFO replication 里的 master_repl_offset 与 slave_repl_offset 差值就是延迟字节数。

bash 复制代码
# 监控脚本
redis-cli -h master INFO replication | grep master_repl_offset
redis-cli -h slave  INFO replication | grep slave_repl_offset
# 差值持续增长 = 从库跟不上(写入压力大 / 从库有慢命令 / 网络问题)

② 主从切换丢数据 :主库挂了,还没同步到从库的数据永久丢失。Redis 本身不保证,只能靠: - min-replicas-to-write 1 + min-replicas-max-lag 10(至少 1 个从库延迟 <10s 才允许写,否则主库拒绝写)------代价是可用性下降; - 或者用 WAIT 命令(会阻塞):WAIT 1 1000 表示等 1 个副本确认,最多 1 秒。

③ 复制风暴 :主库挂掉重启后,所有从库同时发起全量同步,主库 fork 多个子进程 → 内存/CPU 打满。解决:级联复制(树形拓扑,从库再挂从库),或增大 backlog。


六、哨兵 Sentinel

Sentinel 解决的是"主库挂了谁来顶上"的问题,它本身不存数据,只做监控与故障转移。

6.1 三个定时任务

复制代码
① 每 10s:向主/从库发 INFO,发现从库列表、主从关系
② 每 2s :通过 __sentinel__:hello 频道广播自己的信息(发现其他哨兵、交换节点看法)
③ 每 1s :向所有节点和其他哨兵发 PING,做心跳检测

6.2 故障转移完整流程

复制代码
① 主观下线(sdown)
   某个哨兵 PING 主库超过 down-after-milliseconds 无响应 → 标记 sdown

② 客观下线(odown)
   该哨兵向其他哨兵发 SENTINEL is-master-down-by-addr 询问
   同意票数 ≥ quorum → 标记 odown(真正的"确认挂了")

③ 选举 Leader 哨兵(Raft 算法)
   多个哨兵间选出一个 Leader 来执行故障转移
   每个哨兵只有一票,先到先得,需获得多数票

④ Leader 选新主库(按优先级排序)
   ┌──────────────────────────────────────────┐
   │ 1. 过滤掉已下线/断连的从库                 │
   │ 2. 选 slave-priority 最小的(可手动配权重) │
   │ 3. priority 相同 → 选复制 offset 最大的    │
   │    (数据最全)                            │
   │ 4. 还相同 → 选 runid 最小的(字典序)       │
   └──────────────────────────────────────────┘

⑤ 执行切换
   SLAVEOF no one → 新主库
   其他从库 SLAVEOF 新主库
   旧主库恢复后自动降级为新主库的从库
   通过 pub/sub 发布 +switch-master 事件通知客户端

6.3 最小配置

conf 复制代码
# sentinel.conf(三个哨兵节点配置相同)
port 26379
sentinel monitor mymaster 10.0.0.1 6379 2      # 2 = quorum
sentinel down-after-milliseconds mymaster 5000
sentinel parallel-syncs mymaster 1             # 切换后同时同步的从库数(1 避免压垮新主)
sentinel failover-timeout mymaster 180000
sentinel auth-pass mymaster xxx
bash 复制代码
redis-sentinel /path/to/sentinel.conf
# 或 redis-server /path/to/sentinel.conf --sentinel

# 查看状态
redis-cli -p 26379 SENTINEL masters
redis-cli -p 26379 SENTINEL get-master-addr-by-name mymaster
redis-cli -p 26379 SENTINEL ckquorum mymaster    # 检查 quorum 是否健康

quorum 怎么定? 哨兵节点数 N(建议 3 或 5,奇数),quorum = N/2 + 1。注意区分两个"多数": - quorum:判定 odown 需要的票数; - 选举 Leader 需要 max(quorum, N/2+1) 票,且每个哨兵每轮只能投一票。

6.4 脑裂问题(面试高频)

复制代码
          网络分区发生
   ┌─────────────┐        ✂         ┌─────────────┐
   │  旧主库 M    │                  │  从库 S1 S2  │
   │  客户端还在写│                  │  哨兵选出新主│
   └─────────────┘                  └─────────────┘
   分区期间旧主库接收到的数据 → 恢复后被全量清空 → 数据永久丢失!

Redis 的防线:

ini 复制代码
min-replicas-to-write 1      # 主库必须有至少 1 个可用从库才接受写
min-replicas-max-lag 10      # 且从库延迟(最后一次 ACK)不超过 10 秒

这个配置是拿可用性换一致性:不满足条件时旧主库直接拒绝写,客户端会报错,但不会丢数据。需要根据业务权衡。

6.5 客户端怎么感知切换

java 复制代码
// Spring Boot + Lettuce(推荐)
@Configuration
public class RedisSentinelConfig {
    @Bean
    public RedisConnectionFactory connectionFactory() {
        RedisSentinelConfiguration cfg = new RedisSentinelConfiguration()
                .master("mymaster")
                .sentinel("10.0.0.1", 26379)
                .sentinel("10.0.0.2", 26379)
                .sentinel("10.0.0.3", 26379);
        cfg.setPassword(RedisPassword.of("xxx"));
        return new LettuceConnectionFactory(cfg);
    }
}

客户端订阅 Sentinel 的 +switch-master 频道获取新主地址。Jedis 需要显式配置哨兵集合;Lettuce 默认支持拓扑刷新 ,生产建议开启: ClusterTopologyRefreshOptions / SocketOptions(哨兵模式 Lettuce 会自动重连)。


七、Redis Cluster

Sentinel 解决高可用,但所有节点存的都是全量数据 ,单机内存上限就是集群上限。Cluster 用分片解决容量与并发问题。

7.1 哈希槽:16384 是怎么来的

复制代码
key → CRC16(key) mod 16384 → 槽位 → 节点

┌────────────────────────────────────────────────┐
│  0-5460      → Node A (Master) + Node A' (Slave)│
│  5461-10922  → Node B (Master) + Node B' (Slave)│
│  10923-16383 → Node C (Master) + Node C' (Slave)│
└────────────────────────────────────────────────┘
  每个节点持有一部分槽,槽可以在节点间迁移(扩容/缩容)

为什么是 16384(2¹⁴)而不是 65536(2¹⁶)? 作者 antirez 的原话:

  1. 集群节点数不会超过 1000 个,16384 个槽够分了;
  2. 心跳消息需要携带节点槽位Bitmap ,16384 个槽的 bitmap 是 2KB(16384/8/1024),65536 就是 8KB,每秒的 ping/pong 消息会占用太多带宽;
  3. 槽数少,bitmap 压缩率高(槽位稀疏时)。

7.2 集群通信:Gossip 协议

消息 作用
MEET 邀请节点加入集群
PING 每秒随机向 5 个节点发 PING,携带自身 + 已知节点状态
PONG 响应 PING/MEET
FAIL 广播某节点已下线
PUBLISH 广播 pub/sub 消息

故障检测 :节点 A 标记 B 为 PFAIL(疑似下线),当半数以上持有槽的主节点 都认为 B 下线时,标记为 FAIL 并广播。

7.3 请求重定向:MOVED 与 ASK

bash 复制代码
$ redis-cli -c -p 7001 GET user:1001
# 假设 user:1001 的槽在 7002
(error) MOVED 9842 10.0.0.2:7002
# 客户端应更新本地槽映射表,后续直接访问 7002

# 迁移中的临时重定向
(error) ASK 9842 10.0.0.2:7002
# ASK 是一次性的,客户端不更新映射表(因为槽还在迁移中)

正确流程 :ASK → 客户端先发 ASKING 命令 → 再发原命令。

7.4 搭建与运维

bash 复制代码
# 快速创建 3 主 3 从(Redis 5.0+ 一条命令)
redis-cli --cluster create \
  10.0.0.1:7001 10.0.0.2:7002 10.0.0.3:7003 \
  10.0.0.1:7004 10.0.0.2:7005 10.0.0.3:7006 \
  --cluster-replicas 1

# 检查集群
redis-cli --cluster check 10.0.0.1:7001
redis-cli --cluster info 10.0.0.1:7001

# 扩容:加入新节点 + reshard
redis-cli --cluster add-node 10.0.0.4:7007 10.0.0.1:7001
redis-cli --cluster reshard 10.0.0.1:7001 --cluster-from all --cluster-to <new-node-id> --cluster-slots 4096

# 缩容:先迁移槽,再删节点
redis-cli --cluster del-node 10.0.0.1:7001 <node-id>

# 客户端查看
redis-cli -c -p 7001 CLUSTER NODES
redis-cli -c -p 7001 CLUSTER SLOTS
ini 复制代码
# redis.conf 集群必配
cluster-enabled yes
cluster-config-file nodes-7001.conf
cluster-node-timeout 15000        # 15s 无响应判定为 FAIL
cluster-require-full-coverage no  # 建议 no:部分槽不可用时其余仍可服务
cluster-replica-validity-factor 10
cluster-migration-barrier 1       # 主节点至少有 1 个从库才允许从库迁移

7.5 Cluster 的限制(面试常问)

限制 说明 规避
不支持多 key 跨槽操作 MSET 多 key 在不同槽会报错 用 Hash Tag :{user1001}.name / {user1001}.age 强制同槽
不支持多数据库 只有 db0 -
事务/Lua 跨槽受限 所有 key 必须在同一槽 同上 Hash Tag
批量操作需分组 MGET 要按槽分组再发 客户端(Lettuce/Jedis)已自动处理
bash 复制代码
# Hash Tag 示例:{} 内的内容才参与 hash
MSET {order:1001}.id 1001 {order:1001}.amount 99.9   # ✅ 同槽
MSET order:1001:id 1001 order:1001:amount 99.9       # ❌ 可能跨槽

八、分布式锁

这是 Redis 面试最高频、最能看出深浅的考点。按演进顺序讲。

8.1 演进史:五个版本

❌ V1:SETNX + EXPIRE(非原子)
java 复制代码
// 错误示范
if (setnx("lock", "1") == 1) {
    expire("lock", 30);        // 👈 如果这行执行前宕机 → 死锁,锁永不释放
    try { doBiz(); } finally { del("lock"); }
}
❌ V2:SETNX + 把过期时间写进 value(依赖客户端时钟)
java 复制代码
// 用时间戳做值,判断时依赖所有客户端时钟一致 ------ 分布式下不可靠
✅ V3:SET key value NX PX(原子加锁,2.6.12+)
bash 复制代码
SET lock:order:1001 <uuid> NX PX 30000
# NX = 仅当不存在才设置(原子)
# PX 30000 = 30 秒过期(原子)
# 一条命令解决 V1 的原子性问题
java 复制代码
String lockKey = "lock:order:" + orderId;
String requestId = UUID.randomUUID().toString();   // 唯一标识,防止误删别人的锁
Boolean ok = redisTemplate.opsForValue()
        .setIfAbsent(lockKey, requestId, Duration.ofSeconds(30));
if (!Boolean.TRUE.equals(ok)) {
    throw new BizException("系统繁忙,请稍后重试");
}
✅ V4:释放锁必须原子(Lua 脚本)
java 复制代码
// 错误示范:两步判断 + 删除,中间可能被插入
if (requestId.equals(redis.get(lockKey))) {   // 判断时还是自己的锁
    // 👇 此时锁恰好过期,且被别的线程获取,这里就会删掉别人的锁!
    redis.del(lockKey);
}

正确做法:Lua 脚本保证判断 + 删除的原子性

java 复制代码
private static final DefaultRedisScript<Long> UNLOCK_SCRIPT = new DefaultRedisScript<>();
static {
    UNLOCK_SCRIPT.setScriptText(
        "if redis.call('get', KEYS[1]) == ARGV[1] then " +
        "    return redis.call('del', KEYS[1]) " +
        "else " +
        "    return 0 " +
        "end");
    UNLOCK_SCRIPT.setResultType(Long.class);
}

public void unlock(String lockKey, String requestId) {
    redisTemplate.execute(UNLOCK_SCRIPT, Collections.singletonList(lockKey), requestId);
}
✅ V5:Redisson ------ 自动续期(看门狗)

V3/V4 还有个致命问题:业务执行时间 > 锁过期时间,锁提前释放,导致并发。

Redisson 的 Watchdog(看门狗) 机制:如果没显式指定 leaseTime,默认锁 30 秒,每 10 秒(1/3 周期)自动续期,业务不结束锁就不会过期;业务完成或线程挂掉(看门狗线程随之停止)后自动释放。

java 复制代码
// Maven: org.redisson:redisson-spring-boot-starter
@Resource
private RedissonClient redissonClient;

public void createOrder(OrderDTO dto) {
    RLock lock = redissonClient.getLock("lock:order:" + dto.getUserId());
    try {
        // 默认 30s + 自动续期;waitTime=10s 最多等 10 秒
        boolean locked = lock.tryLock(10, TimeUnit.SECONDS);
        if (!locked) {
            throw new BizException("操作过于频繁");
        }
        doCreateOrder(dto);
    } finally {
        // 必须判断:只有当前线程持有锁才释放
        if (lock.isHeldByCurrentThread()) {
            lock.unlock();
        }
    }
}

底层 Lua(加锁,支持可重入):

lua 复制代码
-- KEYS[1]=锁名 ARGV[1]=租约时间 ARGV[2]=线程标识(uuid:threadId)
if (redis.call('exists', KEYS[1]) == 0) then
    redis.call('hincrby', KEYS[1], ARGV[2], 1);   -- Hash 结构:field=线程标识,value=重入次数
    redis.call('pexpire', KEYS[1], ARGV[1]);
    return 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]);               -- 返回剩余时间,客户端订阅后自旋等待

8.2 主从架构下的锁失效(Redlock 的由来)

复制代码
① 客户端向 Master 加锁成功
② Master 还没把锁同步给 Slave 就宕机
③ Sentinel 把 Slave 提升为新 Master
④ 新 Master 上没有这把锁 → 另一个客户端也能加锁成功 → 两个客户端同时持锁 ❌

Redlock 算法(antirez 提出) :部署 N 个(通常 5 个)独立的 Redis Master(无主从、无集群),加锁时:

复制代码
1. 获取当前毫秒级时间 T1
2. 依次向 5 个实例发加锁请求(相同的 key/value,超时时间远小于锁 TTL)
3. 统计成功数:≥ N/2 + 1(即 3 个)且 总耗时 < 锁有效时间 → 加锁成功
4. 锁的真实有效时间 = TTL - (T2 - T1) - 时钟漂移
5. 释放锁时向所有实例发删除请求(包括失败的)
java 复制代码
// Redisson 的 Redlock(3.19+ 已标记为 deprecated,官方建议用普通锁或改用其他方案)
RLock lock1 = redisson1.getLock("myLock");
RLock lock2 = redisson2.getLock("myLock");
RLock lock3 = redisson3.getLock("myLock");
RLock redLock = redissonClient.getMultiLock(lock1, lock2, lock3);  // 联锁
redLock.lock();

重要观点(面试加分项) :分布式系统专家 Martin Kleppmann 曾撰文批评 Redlock(主要质疑点:依赖时钟、GC STW/网络延迟导致锁失效后不自知)。antirez 也做了回应。业界共识是:Redlock 性能差、运维复杂,且并不能 100% 保证安全 。 实际落地建议 : - 99% 的业务用单 Redis 实例 + Redisson 看门狗 就够了; - 要求极高时,不要用 Redis 做锁,用 etcd / ZooKeeper / 数据库悲观锁 ; - 更根本的思路:把业务改造成无锁(乐观锁 + CAS / 幂等 + 状态机) ,比如扣库存直接 UPDATE stock SET num=num-1 WHERE id=? AND num>0,靠数据库的行锁保证原子性。

8.3 分布式锁的正确使用姿势(总结)

要点 做法
加锁原子性 SET key value NX PX ttl 或 Redisson
释放锁原子性 Lua 脚本(判断 value 再删)
防误删 value 用 UUID/线程标识
防业务超时 看门狗自动续期
可重入 Hash 结构存重入计数
释放前判断 lock.isHeldByCurrentThread()
加锁必须有超时 避免永久阻塞线程
兜底 数据库层面仍需做 CAS / 唯一约束做最后防线

九、典型使用场景与代码

场景 1:缓存(最基础)

java 复制代码
public Order getOrder(Long orderId) {
    String key = "order:" + orderId;
    Order order = (Order) redisTemplate.opsForValue().get(key);
    if (order != null) {
        return order;
    }
    order = orderMapper.selectById(orderId);
    if (order != null) {
        // 随机 TTL 防雪崩
        long ttl = 30 + ThreadLocalRandom.current().nextInt(10);
        redisTemplate.opsForValue().set(key, order, Duration.ofMinutes(ttl));
    } else {
        // 缓存空值防穿透(短 TTL)
        redisTemplate.opsForValue().set(key, NullValue.INSTANCE, Duration.ofMinutes(2));
    }
    return order;
}

场景 2:分布式限流(Lua 原子实现滑动窗口)

lua 复制代码
-- rate_limit.lua:ZSet 实现滑动窗口
-- KEYS[1]=限流key  ARGV[1]=窗口(ms)  ARGV[2]=阈值  ARGV[3]=当前时间(ms)
local key = KEYS[1]
local window = tonumber(ARGV[1])
local limit = tonumber(ARGV[2])
local now = tonumber(ARGV[3])

redis.call('ZREMRANGEBYSCORE', key, 0, now - window)   -- 清理窗口外记录
local count = redis.call('ZCARD', key)
if count < limit then
    redis.call('ZADD', key, now, now .. '-' .. math.random(1000000))
    redis.call('PEXPIRE', key, window)
    return 1      -- 放行
end
return 0          -- 限流
java 复制代码
@Around("@annotation(rateLimit)")
public Object doRateLimit(ProceedingJoinPoint pjp, RateLimit rateLimit) throws Throwable {
    String key = "rl:" + rateLimit.key() + ":" + getUserId();
    Long pass = redisTemplate.execute(rateLimitScript,
            List.of(key),
            String.valueOf(rateLimit.windowMs()),
            String.valueOf(rateLimit.limit()),
            String.valueOf(System.currentTimeMillis()));
    if (!Long.valueOf(1L).equals(pass)) {
        throw new BizException("操作过于频繁,请稍后再试");
    }
    return pjp.proceed();
}

场景 3:排行榜(ZSet)

java 复制代码
// 更新分数
redisTemplate.opsForZSet().add("rank:game:1", playerId, score);

// 查 Top 10(带分数)
Set<ZSetOperations.TypedTuple<String>> top10 = redisTemplate.opsForZSet()
        .reverseRangeWithScores("rank:game:1", 0, 9);

// 查某玩家的排名(从 0 开始,+1 变成名次)
Long rank = redisTemplate.opsForZSet().reverseRank("rank:game:1", playerId);

// 查分数区间(如"青铜段位")
Set<String> bronze = redisTemplate.opsForZSet().rangeByScore("rank:game:1", 0, 1000);

大排行榜优化:Top N 用 Redis,全量排名定期落库(避免 ZSet 过大)。

场景 4:延迟队列(订单 30 分钟未支付自动取消)

java 复制代码
// 方案 A:ZSet + 定时扫(简单可靠)
// 下单时
redisTemplate.opsForZSet().add("delay:order",
        orderId.toString(), System.currentTimeMillis() + 30 * 60 * 1000);

// 每 10 秒扫描到期任务
@Scheduled(fixedDelay = 10_000)
public void scanDelayQueue() {
    long now = System.currentTimeMillis();
    Set<ZSetOperations.TypedTuple<String>> tasks = redisTemplate.opsForZSet()
            .rangeByScoreWithScores("delay:order", 0, now, 0, 100);
    for (var task : tasks) {
        // 用 Lua 保证"取出即删除"的原子性,避免重复消费
        if (removeIfExists("delay:order", task.getValue())) {
            orderService.cancelIfUnpaid(Long.valueOf(task.getValue()));
        }
    }
}

// 方案 B:Redisson 的 RDelayedQueue(基于 ZSet + 阻塞队列封装)
RQueue<OrderTask> queue = redissonClient.getQueue("order:delay");
RDelayedQueue<OrderTask> delayedQueue = redissonClient.getDelayedQueue(queue);
delayedQueue.offer(new OrderTask(orderId), 30, TimeUnit.MINUTES);
// 消费端阻塞take
OrderTask task = queue.take();

场景 5:分布式 Session

java 复制代码
// Spring Session + Redis(零侵入)
// pom: spring-session-data-redis
@EnableRedisWebSession  // 或 @EnableRedisHttpSession
public class SessionConfig { }
yaml 复制代码
spring:
  session:
    store-type: redis
    redis:
      namespace: myapp:session
    timeout: 30m

场景 6:分布式 ID 生成器

java 复制代码
// 利用 Redis 原子自增,配合时间戳,每天一个 key
public long nextId(String bizTag) {
    String key = "id:" + bizTag + ":" + LocalDate.now();
    Long seq = redisTemplate.opsForValue().increment(key);
    if (seq == 1) {
        redisTemplate.expire(key, Duration.ofDays(2));
    }
    return Long.parseLong(LocalDate.now().format(DateTimeFormatter.BASIC_ISO_DATE)) * 100000 + seq;
}
// 更高性能:一次取一段(号段模式),本地内存发号,减少 Redis 交互

场景 7:秒杀扣库存(Lua + 数据库兜底)

lua 复制代码
-- seckill.lua
-- KEYS[1]=库存key  ARGV[1]=购买数量
local stock = tonumber(redis.call('get', KEYS[1]))
if stock == nil then return -1 end           -- 库存未预热
if stock < tonumber(ARGV[1]) then return 0 end   -- 库存不足
redis.call('decrby', KEYS[1], ARGV[1])
return 1                                     -- 扣减成功
java 复制代码
// 关键:Redis 只做"前置拦截",最终一致性由数据库保证
public boolean seckill(Long productId, int num) {
    Long r = redisTemplate.execute(seckillScript, List.of("stock:" + productId), String.valueOf(num));
    if (r == null || r == 0) return false;

    // 数据库 CAS 兜底(幂等 + 防超卖最后一道防线)
    int updated = productMapper.deductStock(productId, num);
    if (updated == 0) {
        // 回滚 Redis(极少发生)
        redisTemplate.opsForValue().increment("stock:" + productId, num);
        return false;
    }
    return true;
}
sql 复制代码
-- 数据库兜底 SQL:靠行锁保证原子
UPDATE t_product SET stock = stock - #{num}
WHERE id = #{productId} AND stock >= #{num};

场景 8:布隆过滤器防穿透

java 复制代码
RBloomFilter<Long> bloomFilter = redissonClient.getBloomFilter("order:bloom");
bloomFilter.tryInit(10_000_000L, 0.01);   // 预期 1000 万元素,1% 误判
// 数据写入 DB 时同步加入
bloomFilter.add(orderId);

// 查询前
if (!bloomFilter.contains(orderId)) {
    return null;    // 一定不存在
}

场景 9:消息队列(Stream,Redis 5.0+)

java 复制代码
// 生产
MapRecord<String, String, String> record = StreamRecords.newRecord()
        .ofMap(Map.of("orderId", "1001", "amount", "99.9"))
        .withStreamKey("stream:order");
redisTemplate.opsForStream().add(record);

// 消费(消费组 + ACK,支持消息重试、PEL 未确认列表)
@Bean
public StreamMessageListenerContainer<String, MapRecord<String,String,String>> container(
        RedisConnectionFactory factory) {
    var options = StreamMessageListenerContainer.StreamMessageListenerContainerOptions
            .builder().pollTimeout(Duration.ofSeconds(5)).build();
    var container = StreamMessageListenerContainer.create(factory, options);
    container.receive(Consumer.from("group1", "consumer1"),
            StreamOffset.create("stream:order", ReadOffset.lastConsumed()),
            msg -> {
                try {
                    handle(msg.getValue());
                    redisTemplate.opsForStream().acknowledge("stream:order", "group1", msg.getId());
                } catch (Exception e) {
                    // 不 ACK,消息留在 PEL,等待重新投递
                }
            });
    container.start();
    return container;
}

Stream vs List vs PubSub: - List:无 ACK,消息丢失风险; - PubSub:无持久化,断连即丢 (只适合实时通知); - Stream:有持久化 + 消费组 + ACK + PEL,是 Redis 里唯一"认真"的 MQ(但仍不如 Kafka/RocketMQ,重要业务别用 Redis 当 MQ)。


十、缓存三兄弟与一致性

10.1 三大问题与解法

问题 根因 解法
穿透 查不存在的 key 布隆过滤器 / 缓存空值 / 参数校验 / 限流
击穿 单个热点 key 过期 互斥锁重建 / 逻辑过期(永不过期 + 异步刷新)
雪崩 大量 key 同时过期 TTL 加随机值 / 多级缓存 / 熔断降级 / 高可用集群

10.2 一致性方案(三选一,按业务严格度)

复制代码
方案 A(90% 业务):Cache Aside
   读:命中返回;未命中读 DB → 写缓存
   写:先更新 DB → 再删缓存(不是更新缓存!)
   兜底:TTL

方案 B(较高要求):延迟双删
   更新 DB → 删缓存 → sleep 500ms → 再删缓存

方案 C(最高要求):binlog 订阅 + MQ 重试删除
   业务只改 DB → Canal 监听 binlog → 投递 MQ → 消费者删缓存(失败自动重试)

10.3 大 Key / 热 Key 治理

bash 复制代码
# 发现
redis-cli --bigkeys                    # 找大 key
redis-cli --hotkeys                    # 找热 key(需先设 maxmemory-policy 为 lfu)
redis-cli INFO commandstats            # 命令耗时统计
redis-cli SLOWLOG GET 10               # 慢日志(默认 10ms)
CONFIG SET slowlog-log-slower-than 10000   # 微秒
CONFIG SET slowlog-max-len 1000

大 Key 治理 : - 拆分:user:orders:1001 这个大 Hash → 按月份拆成 user:orders:1001:202611; - 压缩:value 用 protobuf / gzip(权衡 CPU); - 删除:用 UNLINK(异步)而不是 DEL。

热 Key 治理 : - 本地缓存(Caffeine)扛住热点; - 读写分离,多副本分摊读; - 客户端缓存(Redis 6 的 Client-side caching,CLIENT TRACKING ON); - key 打散 :hot:product:1001 → hot:product:1001:1..10 分片,代价是更新要改多次。


十一、面试高频 30 问快答

基础原理 1. Redis 为什么快? 内存 + 单线程无锁 + IO 多路复用 + 高效数据结构 + 6.0 多线程 IO。 2. Redis 是单线程吗? 命令执行单线程;4.0 起有 lazy free 线程,6.0 起有 IO 多线程,持久化靠 fork。 3. Redis 6 多线程会不会有并发安全问题? 不会,多线程只做网络读写与协议解析,命令执行仍单线程。 4. IO 多路复用是什么? 一个线程通过 epoll 监听多个 socket,有事件才处理,避免阻塞等待。 5. SDS 相比 C 字符串好在哪? O(1) 取长度、二进制安全、空间预分配减少重分配、杜绝缓冲区溢出。

数据结构 6. 跳表为什么不用红黑树? 实现简单、范围查询高效、无需 rebalance。 7. ZSet 为什么用两个结构? 跳表支持范围查询,哈希表支持 O(1) 单点查询,空间换时间。 8. ziplist 为什么被 listpack 取代? ziplist 的 prevlen 会引发连锁更新(最坏 O(N²)),listpack 只记录自身长度。 9. 渐进式 rehash 是什么? 扩容时不是一次搬迁,而是每次操作顺带搬一个桶,分摊阻塞时间。 10. embstr 和 raw 的区别? ≤44 字节用 embstr(一次内存分配,redisObject 与 SDS 连续),更大用 raw(两次分配)。

持久化 11. RDB 和 AOF 区别? RDB 是二进制快照(加载快、体积小、丢数据多);AOF 是命令追加(丢数据少、体积大、加载慢)。 12. bgsave 会阻塞吗? fork 瞬间会短暂阻塞(内存越大越明显),之后主进程正常服务。 13. AOF 重写会不会阻塞? 用 bgrewriteaof,fork 子进程做,主进程的写命令同时写 AOF 缓冲和重写缓冲。 14. AOF 丢了会怎样? 数据还没 fsync 到磁盘就宕机,everysec 模式最多丢 1 秒。 15. 混合持久化是什么? AOF 重写时前半段用 RDB 格式,兼顾加载速度与数据安全。

内存 16. 过期 key 一定会被立即删除吗? 不会。惰性删除 + 定期采样删除,可能长期留存。 17. LRU 是标准的吗? 不是,是近似 LRU(随机采样 N 个淘汰最久未访问的)。 18. LFU 怎么实现的? 24 位拆成 16 位访问计数(对数增长)+ 8 位衰减时间。 19. 内存满了会怎样? 按 maxmemory-policy 淘汰;如果 noeviction,写请求返回 OOM 错误。

主从与集群 20. 主从第一次同步流程? PSYNC ? -1 → FULLRESYNC → RDB 全量 → replication buffer 增量 → 命令传播。 21. 断线后为什么有时全量有时增量? 看从库 offset 是否还在 replication backlog 环形缓冲区内。 22. repl-backlog-size 为什么重要? 太小会导致频繁全量同步,拖垮主库。 23. Sentinel 怎么判断主库真挂了? 主观下线 sdown(单个哨兵判定)→ 客观下线 odown(≥quorum 个哨兵同意)。 24. Sentinel 怎么选新主? 过滤不健康的 → priority 最小 → offset 最大(数据最全)→ runid 最小。 25. 脑裂怎么防? min-replicas-to-write + min-replicas-max-lag,主库无可用从库时拒绝写。 26. Cluster 为什么是 16384 个槽? 节点数 ≤1000 够用;心跳 bitmap 2KB,65536 会占 8KB 带宽。 27. MOVED 和 ASK 区别? MOVED 是永久迁移(客户端更新映射表),ASK 是迁移中的临时重定向(不更新)。 28. Cluster 如何实现多 key 操作? 用 Hash Tag {xxx} 强制分到同一槽。

分布式锁 29. Redis 分布式锁怎么实现? SET key value NX PX 原子加锁 + Lua 判断 value 后删除 + 看门狗续期(Redisson)。 30. Redlock 有什么问题? 依赖时钟、需要处理 GC/网络停顿、性能与运维成本高,且存在安全性质疑;生产建议优先用单实例 + Redisson,或改用 etcd/ZK,或业务层改为无锁 CAS。


十二、生产上线 Checklist

配置 - maxmemory 设为物理内存的 60%-70% - maxmemory-policy:缓存场景用 allkeys-lru - 关闭 THP:echo never > /sys/kernel/mm/transparent_hugepage/enabled - repl-backlog-size 调大到 128MB+ - 开启慢日志:slowlog-log-slower-than 10000 - 设置密码 + rename-command 禁用 KEYS/FLUSHALL/CONFIG - 绑定内网 IP,protected-mode yes

高可用 - 至少 1 主 2 从 + 3 个 Sentinel(奇数部署,跨机架) - min-replicas-to-write 1 + min-replicas-max-lag 10(按业务权衡) - 集群模式开启 cluster-require-full-coverage no - 客户端开启拓扑刷新与重连(Lettuce 默认支持)

使用规范 - key 命名统一:业务:对象:ID[:属性],禁止过长(< 100 字节) - 所有 key 必须有 TTL - 禁止 KEYS,用 SCAN;禁止 DEL bigkey,用 UNLINK - 禁止在生产执行 FLUSHALL(改用 FLUSHALL ASYNC) - 批量操作用 pipeline / MGET,减少 RTT - Lua 脚本必须做参数校验,避免死循环(Redis 有 lua-time-limit 5000)

监控告警 - used_memory / mem_fragmentation_ratio > 1.5 - 命中率:keyspace_hits / (hits + misses),< 90% 需排查 - 连接数 connected_clients(接近 maxclients 告警) - 主从 offset 差值持续增长 - 慢日志条数、bigkey 定期巡检


写在最后

Redis 看起来简单,但"简单"背后全是权衡:

  • 快是因为牺牲了持久化实时性(换来异步落盘);
  • 单线程是牺牲了多核利用率(换来无锁简单性);
  • Cluster 分片是牺牲了跨槽事务(换来水平扩展);
  • 分布式锁 本质上是不完美的------Redis 从设计上就不是一个强一致的协调服务。

真正理解 Redis 的标志,不是背下多少命令,而是在每一个使用它的场景里,都能说清楚"我放弃了什么,以及我为此准备了什么兜底"。


参考资料

如果这篇文章对你有帮助,欢迎点赞收藏。你在生产中踩过的 Redis 坑,也欢迎在评论区分享,我会持续补充到本文。

相关推荐
二月夜1 小时前
Redis 高频面试题全解:缓存三大问题、大 key 治理与分布式锁之争(2026 招聘 JD 真题复盘)
redis·分布式·后端·缓存
秋名RG1 小时前
一条 SQL 语句在 MySQL 中的执行过程详解
数据库·sql·mysql
Canmag 兴隆磁性1 小时前
钕铁硼磁片取向方向检测自动化方案:从人工目检到流水线在线分选的工程实践
运维·数据库·自动化·工业自动化·钕铁硼·自动化检测·磁材料
yangmu32031 小时前
安全审批与日志审计:从“谁批准”到“谁执行”的全链路闭环
数据库
樱花落木兰1 小时前
Redis 消息队列的可靠性演进:从 List、Pub/Sub 到 Stream 消费者组,以及 8.x 版本改变了什么
java·开发语言·经验分享·redis·分布式·python·软件工程
R²AIN SUITE1 小时前
多云或私有云环境怎么选项目管理工具?RainSuite等5种产品的8项部署验证
服务器·网络·数据库
隔振降噪研究员1 小时前
裁断机一响,楼板为什么会跟着抖?
大数据·linux·数据库·人工智能
IT规划师2 小时前
Redis 数据类型全解 - Set 的三个真相:去重、标签、共同好友(四)
redis·.net
软件派3 小时前
生产动态数据智能风控:阈值判别、趋势预测、异常识别算法工程落地实战
前端·数据库·算法