Redis 学习笔记
目录
- 核心概念与设计哲学
- 五种数据结构底层实现
- 持久化机制:RDB vs AOF
- 主从复制与哨兵
- Cluster 集群
- 内存淘汰与过期策略
- 事务与 Lua 脚本
- 管道(Pipeline)与批处理
- 性能优化全景
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 桌,哪桌举手了就去哪桌服务——一个线程管所有连接。
多个客户端连接
│
▼
┌─────────────┐
│ 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 的结构(概念版):
struct sdshdr {
int len; // 已用长度
int free; // 剩余空间
char buf[]; // 实际数据
}预分配策略:修改后的长度 < 1MB,分配 len + free(len = free);>= 1MB,额外分配 1MB。这样连续追加时不需要每次都重新分配内存。
2.2 dict(字典)——Hash 的底层
类比:HashMap 的结构,但有两个 table 用于渐进式 rehash。
┌─────────────────────────┐
│ dictht[0] │ ← 当前使用的哈希表
│ [0] → entry → entry │
│ [1] → null │
│ [2] → entry → null │
│ [3] → entry → entry → ..│
│ dictht[1] │ ← rehash 时的新表(空表时不用)
│ (rehash 中使用) │
└─────────────────────────┘渐进式 rehash——最精妙的设计:
普通 HashMap rehash 时一次性把所有数据迁移到新数组,数据量大时卡死。Redis 的做法:
- 需要扩容时,创建
dictht[1](大小是dictht[0]的 2 倍) - 每次对该 dict 操作(增删改查)时,顺便迁移一小部分数据
- 直到
dictht[0]变空,释放它,dictht[1]变成新的dictht[0]
这意味着:不管哈希表有多大,每次操作都不会被 rehash 阻塞。
// 对应到 Java 中如何使用 Hash
// 底层是 dict,元素少时用 ziplist 压缩存储(后面讲)
stringRedisTemplate.opsForHash().put("user:10001", "name", "张三");
// 这个过程在 Redis 内部:
// 1. 找一个 bucket
// 2. 遍历链表找到 field
// 3. 更新值
// 4. 如果正在 rehash,顺便搬一小批数据2.3 ziplist(压缩列表)——小数据时的内存优化
设计思想:当数据量小的时候,用一块连续内存存储所有元素,省掉了指针开销。
内存布局:
┌────────┬────────┬────────┬────────┬──────────┐
│zlbytes │zltail │zllen │entry1 │entry2... │
│(总字节)│(尾偏移)│(元素数)│ │ │
└────────┴────────┴────────┴────────┴──────────┘连锁更新问题(面试重点):
每个 entry 都记录前一个 entry 的长度。如果前面是 253 字节,用 1 字节记录;如果超过 254,要升级为 5 字节。一旦某个 entry 从 253 变成 254,后面所有 entry 的前置长度字段都得跟着改——连锁更新。最坏情况是 O(N²)。
但在实际中极少发生:需要恰好有大量 253~254 字节的 entry,而且频繁在中间插入——Redis 官方认为这点缺点的代价远小于 ziplist 带来的内存节省。
在什么条件下 Hash 用 ziplist?
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。
┌───────┐ ┌───────┐ ┌───────┐
│ziplist│◄──►│ziplist│◄──►│ziplist│
│(节点1)│ │(节点2)│ │(节点3)│
└───────┘ └───────┘ └───────┘配置:
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),但实现更复杂 |
| 并发友好 | 容易实现无锁/少锁 | 旋转影响范围大 |
跳表结构:
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 查找。两者指向同一份元素,通过指针共享内存。
// 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)——快照
机制:把某一时刻的全量内存数据写入磁盘。
触发方式:
save 900 1 # 900 秒内至少 1 次修改
save 300 10 # 300 秒内至少 10 次修改
save 60 10000 # 60 秒内至少 10000 次修改核心流程:
┌────────┐
│ Redis │──fork()──► 子进程
│ 主进程 │ │
└────────┘ ▼
┌──────────┐
│ 写 RDB 文件│ ← 子进程写磁盘,主进程继续处理请求
└──────────┘COW(Copy On Write)——关键机制:
父进程 fork 子进程时,子进程共享父进程的内存页。只有当父进程修改某页数据时,操作系统才会把那一页复制一份给子进程。所以:
- 数据越少被修改,fork 越快,内存占用越小
- 如果全量写入,fork 期间内存可能翻倍
RDB 的优劣:
| 优点 | 缺点 |
|---|---|
| 文件紧凑,适合备份 | 两次快照之间的数据会丢 |
| 恢复速度快(直接加载) | fork 会阻塞主进程(大数据量时可能秒级) |
| 对性能影响小(子进程写) |
3.2 AOF(Append Only File)——日志
机制:把每一条写命令追加到日志文件末尾。
appendonly yes
appendfsync everysec # 每秒刷盘一次(推荐)
# appendfsync always # 每条都刷(最安全但最慢)
# appendfsync no # 交给操作系统决定(有丢失风险)AOF 重写:AOF 文件会越来越大,Redis 会 fork 子进程对 AOF 重写——根据当前内存状态生成最小命令集。
原 AOF: SET count 1 → INCR count → INCR count → INCR count
重写后: SET count 43.3 生产环境策略——混合持久化(Redis 4.0+)
aof-use-rdb-preamble yes # 开启混合持久化混合持久化文件结构:
┌──────────────┬─────────────────┐
│ RDB 格式的快照│ AOF 格式的增量命令│
│ (全量数据) │ (快照后的写操作) │
└──────────────┴─────────────────┘恢复时先加载 RDB 部分(快),再重放 AOF 增量(少),兼顾恢复速度和数据安全性。
一句话总结:RDB 是定期全量备份(快但可能丢数据),AOF 是持续增量日志(安全但文件大),生产用混合模式最好。
4. 主从复制与哨兵
4.1 主从复制——读写分离 + 数据冗余
架构:
┌─────────┐
│ Master │ ← 处理所有写请求
└────┬────┘
┌─────┴─────┐
▼ ▼
┌──────┐ ┌──────┐
│Slave1│ │Slave2│ ← 处理读请求,分担压力
└──────┘ └──────┘复制全流程:
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 里就增量复制,否则全量。
repl-backlog-size 64mb # 缓冲区大小,越大越能容忍断线
repl-backlog-ttl 3600 # 所有 Slave 都断开后,多久释放 backlog4.2 哨兵(Sentinel)——自动故障转移
哨兵的作用:监控、通知、自动故障转移。
┌──────────┐ ┌──────────┐ ┌──────────┐
│Sentinel 1│ │Sentinel 2│ │Sentinel 3│ ← 至少 3 台,互相通信
└────┬─────┘ └────┬─────┘ └────┬─────┘
│ │ │
└─────────────┼─────────────┘
│ 监控
┌─────────┴─────────┐
│ Master + Slave │
└───────────────────┘故障转移流程:
1. Sentinel 定期 PING Master
2. 超过 down-after-milliseconds 没响应 → 主观下线(SDOWN)
3. 多个 Sentinel 达成共识(quorum)→ 客观下线(ODOWN)
4. Sentinel 选举出一个 Leader
5. Leader 从 Slave 中选一个升级为新 Master
6. 通知其他 Slave 复制新 Master
7. 通知客户端连接新 MasterSlave 选举优先级:
1. 优先级高的(slave-priority 值小的)
2. 复制偏移量大的(数据最新的)
3. runid 小的(兜底规则)客户端连接哨兵:
// 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?
CRC16(key) % 16384 → 落在哪个节点16384 是 2^14,刚好够用,心跳包比 65536 小得多。集群间用 Gossip 协议交换状态,每发一个心跳包就要携带槽位信息,16384 个槽位的位图只要 2KB,65536 就要 8KB。
5.2 请求重定向
客户端 → 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
迁移一个槽的全流程:
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 客户端
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 检查 | 折中方案 |
定期删除的具体策略:
每秒执行 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 | 淘汰最快要过期的 | 期望尽快淘汰将过期数据 |
生产环境推荐:
maxmemory 4gb # 设为物理内存的 75%
maxmemory-policy allkeys-lru # 通用缓存场景6.3 Redis 的 LRU 不是真正 LRU
真正的 LRU 需要维护一个链表,每次访问都要移动节点——代价太高。Redis 用的是近似 LRU:
随机取 N 个 key(默认 5,可配置 maxmemory-samples)
淘汰其中 idle 时间最长的那个N 越大越接近真正 LRU,但 CPU 消耗越大。默认 5 已经足够好了。
一句话总结:过期是定时扫描 + 访问时检查的双保险,淘汰是内存满了之后的兜底机制。
allkeys-lru能覆盖 90% 的缓存场景。
7. 事务与 Lua 脚本
7.1 事务的局限性
Redis 事务(MULTI/EXEC)和数据库事务有本质区别:
| MySQL 事务 | Redis 事务 | |
|---|---|---|
| 原子性 | 全部成功或全部回滚 | 中间某条失败不影响其他条 |
| 隔离性 | 有隔离级别 | 执行期间其他命令不能插队(但不是隔离) |
| 回滚 | 支持 | 不支持 |
Redis 事务只保证两件事:隔离(执行期间不被打断)+ 顺序(一次发送、一次性执行)。不保证原子性。
// 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 里的优势:
- 原子执行——整个脚本作为一条命令执行,不会被其他命令插入
- 减少 RTT——多次操作一次网络往返
- 服务端逻辑——可以在服务端做判断、循环
扣库存——用 Lua 保证原子性:
@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 问题:命令太多的时候,瓶颈在网络
客户端 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——打包发送,打包接收
客户端 Redis
│ │
│── SET a 1 │
│── SET b 2 ────────────►│ 一次网络发送
│── SET c 3 │ 一次网络接收
│◄── OK ────────────────── │
│◄── OK ─── │
│◄── OK ────── │
10000 次 ≈ 10msPipeline 不是原子操作——中间可能插入其他客户端的命令。它只是把多次 RTT 压缩成一次。
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 慢查询排查
# 查看慢查询配置
redis-cli CONFIG GET slowlog*
# 设置慢查询阈值(微秒),超过这个时间的命令会被记录
redis-cli CONFIG SET slowlog-log-slower-than 10000 # 10ms
# 查看最近 10 条慢查询
redis-cli SLOWLOG GET 10输出示例:
1) 1) (integer) 12345 # 日志 ID
2) (integer) 1700000000 # 时间戳
3) (integer) 15678 # 执行耗时(微秒)
4) 1) "KEYS" # 命令
2) "user:*"⚠️ 生产环境禁用 KEYS *,用 SCAN 代替:
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 |
输出所有命令,压垮网络 | 仅调试用,生产禁掉 |
生产环境配置:
rename-command FLUSHALL ""
rename-command FLUSHDB ""
rename-command KEYS ""
rename-command CONFIG "CONFIG_b3e2a1" # 改名,防止被随意调用9.3 Key 设计优化
# 错误:key 太长的全路径
user:detail:info:by:id:10001:with:full:attributes:version:v2
# 正确:简洁有层次
user:10001- key 长度每多 10 字节,1 亿个 key 就多 1GB 内存
- 但不要太短导致不可读——
u:10001别人看不懂
9.4 连接池优化
@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 内存优化三板斧
- 小 Hash 用 ziplist 省内存:确保 hash-max-ziplist-entries 和 hash-max-ziplist-value 合适
- 整数用共享对象池:0~9999 的整数 Redis 会预分配,多次使用指向同一对象
- 过期时间不要集中:加随机偏移,避免雪崩同时过期
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 优化链路全景
你的业务代码
│
▼
┌─────────────┐
│ 本地缓存 │ ← 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
评论
游客无需注册即可评论。
你提交的昵称、邮箱、网址和评论内容会保存在服务端,用于展示评论身份、接收回复及必要的安全审计。
浏览器会本地保存已填游客信息和评论草稿,方便下次免填。
回复提醒会通过站内消息和邮件通知。