Redis 学习笔记

目录

  1. 核心概念与设计哲学
  2. 五种数据结构底层实现
  3. 持久化机制:RDB vs AOF
  4. 主从复制与哨兵
  5. Cluster 集群
  6. 内存淘汰与过期策略
  7. 事务与 Lua 脚本
  8. 管道(Pipeline)与批处理
  9. 性能优化全景

1. 核心概念与设计哲学

1.1 单线程模型——为什么 Redis 选择单线程?

类比:一个超级快的收银员

一个超市有 10 个收银员,每个收银员都要跟顾客沟通、扫码、收钱、找零、叫下一个顾客——线程切换就像收银员之间的交接班,本身很费时间。

Redis 选择「一个收银员,但动作极快」。请求排队,一个一个处理,中间没有切换的开销。

Redis 单线程指的是「网络 IO 和命令处理」在一个线程里完成,但从 6.0 开始,网络 IO 部分变多线程了——读写网络包可以多线程,命令执行还是单线程。

1.2 Redis 为什么这么快?

原因 说明
纯内存操作 绝大部分操作都在内存中完成,不需要磁盘 IO
单线程无锁 没有线程切换开销,没有锁竞争开销,没有死锁问题
IO 多路复用 一个线程同时监听多个 socket,哪个有数据来了就处理哪个
高效数据结构 底层精心设计的 SDS、ziplist、skiplist 等,时间和空间都极致优化
用户态实现 不依赖内核调度器,完全自主控制

1.3 IO 多路复用

类比:餐厅的服务员

传统 BIO(一个连接一个线程):每桌客人配一个专属服务员,服务员大部分时间在等客人点菜——浪费。

IO 多路复用:一个服务员同时盯 100 桌,哪桌举手了就去哪桌服务——一个线程管所有连接。

TEXT
多个客户端连接


┌─────────────┐
│  epoll_wait │  ← 一个系统调用,同时监听所有 socket
│  (阻塞等待)  │
└──────┬──────┘
       │ 有事件到达

┌─────────────┐
│  事件分发器  │  ← 把就绪的 socket 放进队列
└──────┬──────┘


┌─────────────┐
│  命令处理器  │  ← 一个一个串行执行(单线程核心)
└─────────────┘

Redis 在 Linux 上使用 epoll,macOS 上用 kqueue,共同特点:当连接数很大时,只处理活跃的连接,不轮询所有连接。

一句话总结:Redis 快不是因为单线程,而是因为「内存 + 高效数据结构 + IO 多路复用」,单线程只是避免了锁开销。


2. 五种数据结构底层实现

每种对外暴露的数据结构,底层都可能有多种实现。Redis 会根据数据量、元素大小自动切换——这是 Redis 内存效率高的核心原因。

2.1 SDS(Simple Dynamic String)——String 的底层

为什么不用 C 语言的 char*

C 字符串 SDS
获取长度 O(N) 遍历到 \0 O(1) 读 len 字段
二进制安全 不能存 \0 可以存任意二进制
缓冲区溢出 strcat 可能溢出 自动扩容,不会溢出
内存频繁分配 每次修改都要重新分配 预分配空间,减少重分配

SDS 的结构(概念版):

TEXT
struct sdshdr {
    int len;    // 已用长度
    int free;   // 剩余空间
    char buf[]; // 实际数据
}

预分配策略:修改后的长度 < 1MB,分配 len + free(len = free);>= 1MB,额外分配 1MB。这样连续追加时不需要每次都重新分配内存。

2.2 dict(字典)——Hash 的底层

类比:HashMap 的结构,但有两个 table 用于渐进式 rehash。

TEXT
┌─────────────────────────┐
│      dictht[0]          │  ← 当前使用的哈希表
│  [0] → entry → entry    │
│  [1] → null             │
│  [2] → entry → null     │
│  [3] → entry → entry → ..│
│      dictht[1]          │  ← rehash 时的新表(空表时不用)
│      (rehash 中使用)     │
└─────────────────────────┘

渐进式 rehash——最精妙的设计:

普通 HashMap rehash 时一次性把所有数据迁移到新数组,数据量大时卡死。Redis 的做法:

  1. 需要扩容时,创建 dictht[1](大小是 dictht[0] 的 2 倍)
  2. 每次对该 dict 操作(增删改查)时,顺便迁移一小部分数据
  3. 直到 dictht[0] 变空,释放它,dictht[1] 变成新的 dictht[0]

这意味着:不管哈希表有多大,每次操作都不会被 rehash 阻塞。

JAVA
// 对应到 Java 中如何使用 Hash
// 底层是 dict,元素少时用 ziplist 压缩存储(后面讲)
stringRedisTemplate.opsForHash().put("user:10001", "name", "张三");
// 这个过程在 Redis 内部:
// 1. 找一个 bucket
// 2. 遍历链表找到 field
// 3. 更新值
// 4. 如果正在 rehash,顺便搬一小批数据

2.3 ziplist(压缩列表)——小数据时的内存优化

设计思想:当数据量小的时候,用一块连续内存存储所有元素,省掉了指针开销。

内存布局

TEXT
┌────────┬────────┬────────┬────────┬──────────┐
│zlbytes │zltail  │zllen   │entry1  │entry2... │
│(总字节)│(尾偏移)│(元素数)│        │          │
└────────┴────────┴────────┴────────┴──────────┘

连锁更新问题(面试重点):

每个 entry 都记录前一个 entry 的长度。如果前面是 253 字节,用 1 字节记录;如果超过 254,要升级为 5 字节。一旦某个 entry 从 253 变成 254,后面所有 entry 的前置长度字段都得跟着改——连锁更新。最坏情况是 O(N²)。

但在实际中极少发生:需要恰好有大量 253~254 字节的 entry,而且频繁在中间插入——Redis 官方认为这点缺点的代价远小于 ziplist 带来的内存节省。

在什么条件下 Hash 用 ziplist?

CONF
hash-max-ziplist-entries 512   # 元素数 ≤ 512
hash-max-ziplist-value 64      # 单个 value ≤ 64 字节

两个条件都满足就用 ziplist,任一不满足就升级为 dict。

2.4 quicklist——List 的底层(Redis 3.2+)

Redis 3.2 之前 List 底层是 linkedlist + ziplist,3.2 开始统一为 quicklist。

设计思想:linkedlist 每个节点都是指针,内存碎片严重;ziplist 连续存储但操作大列表慢。quicklist 取两者之长——一个双向链表,每个节点是一个 ziplist

TEXT
┌───────┐    ┌───────┐    ┌───────┐
│ziplist│◄──►│ziplist│◄──►│ziplist│
│(节点1)│    │(节点2)│    │(节点3)│
└───────┘    └───────┘    └───────┘

配置

CONF
list-max-ziplist-size -2  # 每个 ziplist 最大 8KB
list-compress-depth 1     # 两端各 1 个节点不压缩,中间压缩

一句话总结:String → SDS,Hash → dict(小数据用 ziplist),List → quicklist,Set → dict + intset,ZSet → skiplist + dict。记住这个映射就够了。

2.5 skiplist(跳表)——ZSet 的核心

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

跳表 红黑树
实现复杂度 简单 复杂(旋转、染色)
范围查询 O(log N) 找到起点后顺序遍历 O(log N + M),但实现更复杂
并发友好 容易实现无锁/少锁 旋转影响范围大

跳表结构

TEXT
Level 4:  1 ───────────────────────────────► 100
Level 3:  1 ──────────────► 50 ────────────► 100
Level 2:  1 ────► 20 ────► 50 ────► 80 ───► 100
Level 1:  1 → 10 → 20 → 30 → 50 → 60 → 80 → 90 → 100

查找 80:从 Level 4 开始 → 1 < 80,前进 → 100 > 80,下降一层 → 50 < 80,前进 → 100 > 80,下降 → ...直到在 Level 1 找到 80。

每一层都是上一层的一半元素,查询复杂度 O(log N),与二分查找同级。

ZSet 实际是 skiplist + dict 的组合:skiplist 负责按分数排序,dict 负责 O(1) 按 member 查找。两者指向同一份元素,通过指针共享内存。

JAVA
// ZSet 的 ZADD 在底层做了什么
stringRedisTemplate.opsForZSet().add("rank", "player1", 100);

// 1. 在 dict 中记录: "player1" → 100(O(1) 查找)
// 2. 在 skiplist 中插入节点(O(log N)),按 score 排序
// 3. score 相同按 member 字典序排

一句话总结:跳表是「可以跳着走的链表」,每一层都是一条快速通道,让你跳过无关元素。


3. 持久化机制:RDB vs AOF

3.1 RDB(Redis Database)——快照

机制:把某一时刻的全量内存数据写入磁盘。

触发方式

CONF
save 900 1    # 900 秒内至少 1 次修改
save 300 10   # 300 秒内至少 10 次修改
save 60 10000 # 60 秒内至少 10000 次修改

核心流程

TEXT
┌────────┐
│ Redis  │──fork()──► 子进程
│ 主进程  │            │
└────────┘            ▼
                 ┌──────────┐
                 │ 写 RDB 文件│  ← 子进程写磁盘,主进程继续处理请求
                 └──────────┘

COW(Copy On Write)——关键机制

父进程 fork 子进程时,子进程共享父进程的内存页。只有当父进程修改某页数据时,操作系统才会把那一页复制一份给子进程。所以:

  • 数据越少被修改,fork 越快,内存占用越小
  • 如果全量写入,fork 期间内存可能翻倍

RDB 的优劣

优点 缺点
文件紧凑,适合备份 两次快照之间的数据会丢
恢复速度快(直接加载) fork 会阻塞主进程(大数据量时可能秒级)
对性能影响小(子进程写)

3.2 AOF(Append Only File)——日志

机制:把每一条写命令追加到日志文件末尾。

CONF
appendonly yes
appendfsync everysec  # 每秒刷盘一次(推荐)
# appendfsync always  # 每条都刷(最安全但最慢)
# appendfsync no      # 交给操作系统决定(有丢失风险)

AOF 重写:AOF 文件会越来越大,Redis 会 fork 子进程对 AOF 重写——根据当前内存状态生成最小命令集。

TEXT
原 AOF: SET count 1 → INCR count → INCR count → INCR count
重写后: SET count 4

3.3 生产环境策略——混合持久化(Redis 4.0+)

CONF
aof-use-rdb-preamble yes  # 开启混合持久化

混合持久化文件结构

TEXT
┌──────────────┬─────────────────┐
│ RDB 格式的快照│ AOF 格式的增量命令│
│ (全量数据)    │ (快照后的写操作)  │
└──────────────┴─────────────────┘

恢复时先加载 RDB 部分(快),再重放 AOF 增量(少),兼顾恢复速度和数据安全性。

一句话总结:RDB 是定期全量备份(快但可能丢数据),AOF 是持续增量日志(安全但文件大),生产用混合模式最好。


4. 主从复制与哨兵

4.1 主从复制——读写分离 + 数据冗余

架构

TEXT
     ┌─────────┐
     │  Master  │  ← 处理所有写请求
     └────┬────┘
    ┌─────┴─────┐
    ▼           ▼
┌──────┐   ┌──────┐
│Slave1│   │Slave2│  ← 处理读请求,分担压力
└──────┘   └──────┘

复制全流程

TEXT
1. Slave 连上 Master,发送 PSYNC 命令
2. Master fork 子进程生成 RDB,同时把新命令写入 replication buffer
3. Master 把 RDB 发给 Slave
4. Slave 清空旧数据,加载 RDB
5. Master 把 buffer 里的增量命令发给 Slave,Slave 执行
6. 此后 Master 每条写命令都同步给 Slave(异步,近实时)

全量复制 vs 增量复制

全量复制 增量复制
首次同步 断线重连后
传输 RDB + buffer 只传断线期间的命令
耗时长、占用资源 轻量、快速

增量复制的关键——replication backlog:一个环形缓冲区,Master 记录最近的写命令。Slave 重连后报上 offset,如果 offset 还在 buffer 里就增量复制,否则全量。

CONF
repl-backlog-size 64mb   # 缓冲区大小,越大越能容忍断线
repl-backlog-ttl 3600    # 所有 Slave 都断开后,多久释放 backlog

4.2 哨兵(Sentinel)——自动故障转移

哨兵的作用:监控、通知、自动故障转移。

TEXT
┌──────────┐  ┌──────────┐  ┌──────────┐
│Sentinel 1│  │Sentinel 2│  │Sentinel 3│  ← 至少 3 台,互相通信
└────┬─────┘  └────┬─────┘  └────┬─────┘
     │             │             │
     └─────────────┼─────────────┘
                   │ 监控
         ┌─────────┴─────────┐
         │   Master + Slave  │
         └───────────────────┘

故障转移流程

TEXT
1. Sentinel 定期 PING Master
2. 超过 down-after-milliseconds 没响应 → 主观下线(SDOWN)
3. 多个 Sentinel 达成共识(quorum)→ 客观下线(ODOWN)
4. Sentinel 选举出一个 Leader
5. Leader 从 Slave 中选一个升级为新 Master
6. 通知其他 Slave 复制新 Master
7. 通知客户端连接新 Master

Slave 选举优先级

TEXT
1. 优先级高的(slave-priority 值小的)
2. 复制偏移量大的(数据最新的)
3. runid 小的(兜底规则)

客户端连接哨兵

JAVA
// Lettuce 连接哨兵模式
@Bean
public RedisConnectionFactory redisConnectionFactory() {
    RedisSentinelConfiguration config = new RedisSentinelConfiguration()
        .master("mymaster")
        .sentinel("192.168.1.10", 26379)
        .sentinel("192.168.1.11", 26379)
        .sentinel("192.168.1.12", 26379);
    config.setPassword("123456");
    return new LettuceConnectionFactory(config);
}

一句话总结:主从复制解决「数据不丢」和「读写分离」,哨兵解决「挂了自动切」,两者配合是高可用的标配。


5. Cluster 集群

5.1 槽位机制——16384 个哈希槽

为什么是 16384?

TEXT
CRC16(key) % 16384 → 落在哪个节点

16384 是 2^14,刚好够用,心跳包比 65536 小得多。集群间用 Gossip 协议交换状态,每发一个心跳包就要携带槽位信息,16384 个槽位的位图只要 2KB,65536 就要 8KB。

5.2 请求重定向

TEXT
客户端 → Node A: GET user:1001
Node A → 客户端: MOVED 12360 192.168.1.11:6379
客户端 → Node B: GET user:1001
Node B → 客户端: "张三"

MOVED vs ASK

MOVED ASK
槽位永久迁移到了新节点 槽位正在迁移中,这次去新节点试试
客户端收到后更新本地槽位表 客户端不更新槽位表

5.3 数据迁移——redis-cli --cluster reshard

迁移一个槽的全流程:

TEXT
1. 目标节点: CLUSTER SETSLOT <slot> IMPORTING <source_id>
2. 源节点:   CLUSTER SETSLOT <slot> MIGRATING <target_id>
3. 源节点:   CLUSTER GETKEYSINSLOT <slot> <count>  → 逐个 MIGRATE
4. 全部迁完后,向所有节点广播槽位变更

迁移过程中该槽位的请求:

  • 数据还在源节点 → 正常响应
  • 数据已迁到目标节点 → 返回 ASK 重定向

5.4 集群配置与 Java 客户端

YAML
spring:
  data:
    redis:
      cluster:
        nodes:
          - 192.168.1.10:6379
          - 192.168.1.11:6379
          - 192.168.1.12:6379
          - 192.168.1.13:6379
          - 192.168.1.14:6379
          - 192.168.1.15:6379
        max-redirects: 3  # 最多重定向次数

Lettuce 客户端会自动处理 MOVED/ASK 重定向,对业务代码透明。

一句话总结:Cluster 把数据切成 16384 个槽位分布到多台机器,解决「数据太大一台机器放不下」的问题。


6. 内存淘汰与过期策略

6.1 过期键删除策略

Redis 用的是「惰性删除 + 定期删除」的组合

策略 机制 优劣
定时删除 每个 key 设一个定时器 CPU 开销太大,不可行
惰性删除 访问 key 时检查是否过期 省 CPU,但没人访问的过期 key 永远不删
定期删除 每秒 10 次扫描,随机抽一批 key 检查 折中方案

定期删除的具体策略:

TEXT
每秒执行 10 次:
1. 从设置了过期时间的 key 中随机取 20 个
2. 删除其中已过期的
3. 如果过期比例 > 25%,重复步骤 1
4. 每次执行不超过 25ms(避免阻塞)

6.2 内存淘汰策略——8 种

当内存达到 maxmemory 时,触发淘汰:

策略 行为 适用场景
noeviction 不淘汰,写操作直接报错 纯缓存场景不推荐
allkeys-lru 所有 key 中淘汰最近最少使用的 通用缓存——最常用
volatile-lru 有过期时间的 key 中淘汰 LRU 需要持久保留部分数据
allkeys-lfu 所有 key 中淘汰最少使用的 热点数据明确,有冷数据需要淘汰
volatile-lfu 有过期时间的 key 中淘汰 LFU 同上但保护持久数据
allkeys-random 所有 key 中随机淘汰 数据访问模式均匀
volatile-random 有过期时间的 key 中随机淘汰 同上
volatile-ttl 淘汰最快要过期的 期望尽快淘汰将过期数据

生产环境推荐

CONF
maxmemory 4gb                      # 设为物理内存的 75%
maxmemory-policy allkeys-lru       # 通用缓存场景

6.3 Redis 的 LRU 不是真正 LRU

真正的 LRU 需要维护一个链表,每次访问都要移动节点——代价太高。Redis 用的是近似 LRU

TEXT
随机取 N 个 key(默认 5,可配置 maxmemory-samples)
淘汰其中 idle 时间最长的那个

N 越大越接近真正 LRU,但 CPU 消耗越大。默认 5 已经足够好了。

一句话总结:过期是定时扫描 + 访问时检查的双保险,淘汰是内存满了之后的兜底机制。allkeys-lru 能覆盖 90% 的缓存场景。


7. 事务与 Lua 脚本

7.1 事务的局限性

Redis 事务(MULTI/EXEC)和数据库事务有本质区别:

MySQL 事务 Redis 事务
原子性 全部成功或全部回滚 中间某条失败不影响其他条
隔离性 有隔离级别 执行期间其他命令不能插队(但不是隔离)
回滚 支持 不支持

Redis 事务只保证两件事:隔离(执行期间不被打断)+ 顺序(一次发送、一次性执行)。不保证原子性。

JAVA
// Redis 事务——这个代码主要是为了理解概念,实际很少用
stringRedisTemplate.setEnableTransactionSupport(true);
stringRedisTemplate.multi();
stringRedisTemplate.opsForValue().set("a", "1");
stringRedisTemplate.opsForValue().increment("a"); // a 是字符串,INCR 会报错
stringRedisTemplate.opsForValue().set("b", "2");
stringRedisTemplate.exec(); // 结果:a=1(SET 成功了),INCR 报错但 b 还是被 SET 了

7.2 Lua 脚本——事务的正确替代方案

Lua 脚本在 Redis 里的优势

  1. 原子执行——整个脚本作为一条命令执行,不会被其他命令插入
  2. 减少 RTT——多次操作一次网络往返
  3. 服务端逻辑——可以在服务端做判断、循环

扣库存——用 Lua 保证原子性

JAVA
@Component
public class StockLuaService {

    @Autowired
    private StringRedisTemplate stringRedisTemplate;

    // Lua 脚本:原子性的查询 + 扣减
    private static final String DEDUCT_SCRIPT = """
        local key = KEYS[1]
        local amount = tonumber(ARGV[1])
        local stock = tonumber(redis.call('GET', key))
        if stock == nil then
            return -1  -- key 不存在
        end
        if stock < amount then
            return -2  -- 库存不足
        end
        redis.call('DECRBY', key, amount)
        return stock - amount  -- 返回扣减后的库存
        """;

    // 预编译脚本(复用 SHA 执行,减少网络传输)
    private final RedisScript<Long> script;

    public StockLuaService() {
        this.script = new DefaultRedisScript<>(DEDUCT_SCRIPT, Long.class);
    }

    public long deduct(Long skuId, int amount) {
        String key = "stock:sku:" + skuId;
        Long result = stringRedisTemplate.execute(
            script,
            Collections.singletonList(key),
            String.valueOf(amount)
        );
        return result != null ? result : -1;
    }
}

为什么 Lua 脚本比事务好用:

  • 可以在服务端做 if/else 判断(事务只能执行命令,不能根据中间结果决定下一步)
  • 真正原子,不会出现「中间某条命令失败继续执行」的情况
  • 脚本执行期间其他客户端命令全部阻塞——所以要,不要写复杂逻辑

⚠️ Lua 脚本不要超过 5ms,否则整个 Redis 都阻塞。Redis 7.0 支持 SCRIPT KILL 中断长期运行的脚本,但这只是救急手段。

一句话总结:Redis 事务没有回滚能力,用 Lua 脚本替代——它真正原子,还能在服务端做逻辑判断。


8. 管道(Pipeline)与批处理

8.1 问题:命令太多的时候,瓶颈在网络

TEXT
客户端                Redis
  │                     │
  │───── SET a 1 ──────►│  RTT 0.1ms
  │◄───── OK ────────── │
  │───── SET b 2 ──────►│  RTT 0.1ms
  │◄───── OK ────────── │
  │───── SET c 3 ──────►│  RTT 0.1ms
  │◄───── OK ────────── │
     10000 次
     = 10000 × 0.1ms = 1 秒(光网络就 1 秒)

8.2 Pipeline——打包发送,打包接收

TEXT
客户端                      Redis
  │                           │
  │── SET a 1                 │
  │── SET b 2    ────────────►│  一次网络发送
  │── SET c 3                 │  一次网络接收
  │◄── OK ──────────────────  │
  │◄── OK ───                   │
  │◄── OK ──────                │
     10000 次 ≈ 10ms

Pipeline 不是原子操作——中间可能插入其他客户端的命令。它只是把多次 RTT 压缩成一次。

Java 代码:

JAVA
public void batchSet(Map<String, String> kvMap) {
    stringRedisTemplate.executePipelined((RedisCallback<Object>) connection -> {
        StringRedisSerializer serializer = (StringRedisSerializer) stringRedisTemplate.getStringSerializer();
        for (Map.Entry<String, String> entry : kvMap.entrySet()) {
            connection.set(
                serializer.serialize(entry.getKey()),
                serializer.serialize(entry.getValue())
            );
        }
        return null; // pipeline 模式下返回值无意义
    });
}

8.3 Pipeline vs 事务 vs Lua 对比

Pipeline 事务(MULTI/EXEC) Lua 脚本
原子性 ❌ 不保证 不被打断,但不回滚 ✅ 完全原子
RTT 优化
服务端逻辑
适用场景 批量读写,不要求原子 需要顺序执行即可 需要原子 + 判断逻辑

一句话总结:Pipeline 解决的是「网络太慢」,Lua 解决的是「需要原子 + 服务端逻辑」,事务基本被 Lua 替代了。


9. 性能优化全景

9.1 慢查询排查

BASH
# 查看慢查询配置
redis-cli CONFIG GET slowlog*

# 设置慢查询阈值(微秒),超过这个时间的命令会被记录
redis-cli CONFIG SET slowlog-log-slower-than 10000  # 10ms

# 查看最近 10 条慢查询
redis-cli SLOWLOG GET 10

输出示例:

TEXT
1) 1) (integer) 12345           # 日志 ID
   2) (integer) 1700000000      # 时间戳
   3) (integer) 15678           # 执行耗时(微秒)
   4) 1) "KEYS"                 # 命令
      2) "user:*"

⚠️ 生产环境禁用 KEYS *,用 SCAN 代替:

JAVA
public Set<String> scanKeys(String pattern) {
    return stringRedisTemplate.execute((RedisCallback<Set<String>>) connection -> {
        Set<String> keys = new HashSet<>();
        Cursor<byte[]> cursor = connection.scan(
            ScanOptions.scanOptions().match(pattern).count(100).build()
        );
        while (cursor.hasNext()) {
            keys.add(new String(cursor.next()));
        }
        return keys;
    });
}

9.2 危险的命令清单

命令 危险原因 替代方案
KEYS * 遍历所有 key,阻塞整个 Redis SCAN 游标分批
FLUSHALL 清空所有数据,不可逆 设置 rename-command 禁用
DEL bigkey 大 key 删除阻塞 UNLINK 异步删除
MONITOR 输出所有命令,压垮网络 仅调试用,生产禁掉

生产环境配置:

CONF
rename-command FLUSHALL ""
rename-command FLUSHDB ""
rename-command KEYS ""
rename-command CONFIG "CONFIG_b3e2a1"  # 改名,防止被随意调用

9.3 Key 设计优化

CONF
# 错误:key 太长的全路径
user:detail:info:by:id:10001:with:full:attributes:version:v2

# 正确:简洁有层次
user:10001
  • key 长度每多 10 字节,1 亿个 key 就多 1GB 内存
  • 但不要太短导致不可读——u:10001 别人看不懂

9.4 连接池优化

JAVA
@Bean
public LettuceClientConfigurationBuilderCustomizer lettuceCustomizer() {
    return builder -> builder
        .clientOptions(ClientOptions.builder()
            .autoReconnect(true)           // 自动重连
            .disconnectedBehavior(ClientOptions.DisconnectedBehavior.REJECT_COMMANDS)
            .socketOptions(SocketOptions.builder()
                .connectTimeout(Duration.ofSeconds(5))
                .keepAlive(true)
                .build())
            .build())
        .clientResources(ClientResources.builder()
            .ioThreadPoolSize(4)           // IO 线程数 = CPU 核数
            .computationThreadPoolSize(4)   // 计算线程数
            .build());
}

9.5 内存优化三板斧

  1. 小 Hash 用 ziplist 省内存:确保 hash-max-ziplist-entries 和 hash-max-ziplist-value 合适
  2. 整数用共享对象池:0~9999 的整数 Redis 会预分配,多次使用指向同一对象
  3. 过期时间不要集中:加随机偏移,避免雪崩同时过期

9.6 监控指标

指标 命令 关注点
命中率 INFO stats 中 keyspace_hits / (hits+misses) < 90% 需要优化
内存使用 INFO memory 中 used_memory_rss 不超过 maxmemory
连接数 INFO clients 中 connected_clients 突增可能是连接泄漏
复制延迟 INFO replication 中 offset 差值 > 1MB 注意
碎片率 INFO memory 中 mem_fragmentation_ratio > 1.5 考虑重启

9.7 优化链路全景

TEXT
你的业务代码


 ┌─────────────┐
 │ 本地缓存     │ ← Caffeine,挡住 70% 请求
 │ (Caffeine)   │
 └──────┬──────┘
        │ miss

 ┌─────────────┐
 │ 分布式缓存   │ ← Redis Cluster,扛住高并发
 │ (Redis)      │
 └──────┬──────┘
        │ miss

 ┌─────────────┐
 │ 数据库       │ ← MySQL,只有极少量请求到这里
 │ (MySQL)      │
 └─────────────┘

每一层挡住一部分请求,到 MySQL 层的压力就很小了。


总结

  • 快的原因:内存 + 单线程无锁 + IO 多路复用 + 高效数据结构
  • 底层精华:SDS、渐进式 rehash、ziplist、quicklist、skiplist——每一个都是空间和时间权衡的典范
  • 持久化:生产用 RDB + AOF 混合模式,兼顾恢复速度和数据安全
  • 高可用:主从 + 哨兵是标配,Cluster 是水平扩展方案
  • 原子操作:用 Lua 脚本,别用事务
  • 性能秘诀:Pipeline 减少 RTT、本地缓存挡流量、SCAN 替 KEYS、UNLINK 替 DEL

评论