Redis 缓存设计模式实战:Cache Aside、Read/Write Through 与 Write Behind 场景分析

做高并发系统少不了缓存,Redis 和持久化存储(MySQL、MongoDB)怎么配合,选什么策略,这些问题挺让人头疼。记录一下我们项目里用过的四种缓存模式,分析一下各自的数据流和适用场景。

缓存架构基础

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
┌─────────────────────────────────────────────────────────────────────┐
│ 缓存层在系统架构中的位置 │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ 客户端 │
│ │ │
│ ▼ │
│ ┌─────────────┐ │
│ │ API 网关 │ │
│ └──────┬──────┘ │
│ │ │
│ ▼ │
│ ┌─────────────┐ ┌─────────────┐ │
│ │ 应用服务 │◄────►│ Redis │ │
│ │ (Node.js) │ │ 缓存层 │ │
│ └──────┬──────┘ └─────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────┐ │
│ │ MySQL/ │ │
│ │ MongoDB │ │
│ │ 持久层 │ │
│ └─────────────┘ │
│ │
│ 核心问题:如何保证缓存与数据库的数据一致性? │
│ │
└─────────────────────────────────────────────────────────────────────┘

模式一:Cache Aside Pattern(旁路缓存)

概述

Cache Aside(旁路缓存)是最常用的缓存模式,应用程序直接维护缓存与数据库的读写逻辑。

数据流

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
┌─────────────────────────────────────────────────────────────────────┐
│ Cache Aside 数据流 │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ 读操作(Read) │
│ ───────────── │
│ │
│ 应用 ──► 查询缓存? ──是──► 返回数据 │
│ │ │
│ 否 │
│ ▼ │
│ 查询数据库 │
│ │ │
│ ▼ │
│ 写入缓存 │
│ │ │
│ ▼ │
│ 返回数据 │
│ │
│ ──────────────────────────────────────────────────────────────── │
│ │
│ 写操作(Write) │
│ ────────────── │
│ │
│ 应用 ──► 更新数据库 ──► 删除缓存 │
│ │
│ 注意:先更新数据库,再删除缓存! │
│ │
└─────────────────────────────────────────────────────────────────────┘

代码实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
class CacheAsideService {
constructor(redis, db) {
this.redis = redis;
this.db = db;
this.CACHE_TTL = 300; // 5分钟过期
}

/**
* 读取数据
*/
async get(key) {
// 1. 先查缓存
const cached = await this.redis.get(key);
if (cached) {
console.log('Cache hit:', key);
return JSON.parse(cached);
}

// 2. 缓存未命中,查数据库
console.log('Cache miss:', key);
const data = await this.db.query('SELECT * FROM users WHERE id = ?', [key]);

// 3. 写入缓存
if (data) {
await this.redis.setex(key, this.CACHE_TTL, JSON.stringify(data));
}

return data;
}

/**
* 更新数据
*/
async update(key, data) {
// 1. 先更新数据库
await this.db.query('UPDATE users SET ? WHERE id = ?', [data, key]);

// 2. 删除缓存(不是更新缓存!)
await this.redis.del(key);

console.log('Cache invalidated:', key);
}

/**
* 删除数据
*/
async delete(key) {
// 1. 先删除数据库
await this.db.query('DELETE FROM users WHERE id = ?', [key]);

// 2. 删除缓存
await this.redis.del(key);
}
}

为什么更新时删除缓存而不是更新缓存?

操作 并发问题 结果
先更新缓存,再更新数据库 缓存更新成功,数据库失败 数据不一致
先更新数据库,再更新缓存 两个并发写操作交错 缓存与数据库不一致
先更新数据库,再删除缓存 短暂不一致,下次读会重建 最终一致,推荐

优缺点

优点 缺点
实现简单,逻辑清晰 需要维护缓存逻辑,代码侵入性强
缓存失效策略灵活 存在短暂的数据不一致窗口
适合读多写少的场景 首次读取会有缓存穿透

模式二:Read Through Pattern(读穿透)

概述

Read Through 将缓存逻辑下沉到缓存层本身。当缓存未命中时,由缓存服务自动从数据库加载数据,对应用层完全透明。

数据流

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
┌─────────────────────────────────────────────────────────────────────┐
│ Read Through 数据流 │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ 应用层 缓存层(带加载器) 数据库 │
│ │ │ │ │
│ │ get(key) │ │ │
│ │─────────────────────►│ │ │
│ │ │ │ │
│ │ ◄─────────────── │ 命中? │ │
│ │ 返回数据 │ │ │
│ │ │ │ │
│ │ │ 未命中 │ │
│ │ │ │ │
│ │ │────► query DB │ │
│ │ │ (自动加载) │ │
│ │ │◄────返回数据 │ │
│ │ │ │ │
│ │ │ 写入缓存 │ │
│ │ ◄─────────────── │ │ │
│ │ 返回数据 │ │ │
│ │
│ 应用无需关心缓存是否命中,逻辑完全透明 │
│ │
└─────────────────────────────────────────────────────────────────────┘

代码实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
class ReadThroughCache {
constructor(redis, db) {
this.redis = redis;
this.db = db;
this.CACHE_TTL = 300;
}

/**
* 注册数据加载器
*/
registerLoader(keyPattern, loaderFn) {
this.loaders = this.loaders || new Map();
this.loaders.set(keyPattern, loaderFn);
}

/**
* 读取数据(缓存层自动处理未命中)
*/
async get(key) {
const cached = await this.redis.get(key);
if (cached) {
return JSON.parse(cached);
}

// 缓存未命中,使用加载器自动从数据库加载
const data = await this.loadFromDB(key);

if (data) {
await this.redis.setex(key, this.CACHE_TTL, JSON.stringify(data));
}

return data;
}

/**
* 从数据库加载(模拟缓存层自动处理)
*/
async loadFromDB(key) {
console.log('Auto-loading from DB:', key);
return await this.db.query('SELECT * FROM users WHERE id = ?', [key]);
}
}

// 使用示例
const cache = new ReadThroughCache(redis, db);

// 应用层代码非常简洁
const user = await cache.get('user:1001');

优缺点

优点 缺点
应用层代码简洁,无缓存逻辑侵入 需要缓存框架支持(如 RedisGears、自定义中间件)
缓存加载逻辑集中管理 加载器逻辑需要单独维护
适合需要统一缓存策略的团队 调试时需要了解缓存层内部逻辑

模式三:Write Through Pattern(写穿透)

概述

Write Through 在更新数据时同步更新缓存和数据库。写操作先更新缓存,再由缓存服务同步更新数据库。

数据流

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
┌─────────────────────────────────────────────────────────────────────┐
│ Write Through 数据流 │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ 写操作: │
│ │
│ 应用 ──► 更新缓存 ──► 缓存同步更新数据库 │
│ │ │ │
│ │ ▼ │
│ │ 数据库更新成功? │
│ │ │ │
│ │ 是 ◄─┘ │
│ │ 否 │
│ ▼ ▼ │
│ 返回成功 回滚缓存 │
│ │
│ 读操作: │
│ │
│ 应用 ──► 查询缓存 ──命中──► 返回数据 │
│ │ │
│ 未命中(理论上很少发生) │
│ ▼ │
│ 从数据库加载并写入缓存 │
│ │
└─────────────────────────────────────────────────────────────────────┘

代码实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
class WriteThroughCache {
constructor(redis, db) {
this.redis = redis;
this.db = db;
this.CACHE_TTL = 300;
}

/**
* 写操作:同步更新缓存和数据库
*/
async set(key, data) {
// 1. 先更新缓存
await this.redis.setex(key, this.CACHE_TTL, JSON.stringify(data));

try {
// 2. 同步更新数据库
await this.db.query('UPDATE users SET ? WHERE id = ?', [data, key]);
console.log('Write through success:', key);
} catch (err) {
// 3. 数据库更新失败,回滚缓存
await this.redis.del(key);
console.error('Write through failed, cache rolled back:', key);
throw err;
}
}

/**
* 读操作
*/
async get(key) {
const cached = await this.redis.get(key);
if (cached) {
return JSON.parse(cached);
}

// 未命中时从数据库加载
const data = await this.db.query('SELECT * FROM users WHERE id = ?', [key]);
if (data) {
await this.redis.setex(key, this.CACHE_TTL, JSON.stringify(data));
}
return data;
}
}

优缺点

优点 缺点
读写延迟低(读总是命中缓存) 写操作延迟增加(需等数据库)
数据一致性较好 写操作失败需要回滚缓存,逻辑复杂
适合读写均衡的场景 存在缓存与数据库同时失败的风险

模式四:Write Behind Caching(异步写回)

概述

Write Behind(又称 Write Back)在更新数据时只更新缓存,不立即更新数据库。缓存会异步地、批量地将数据更新回数据库。

数据流

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
┌─────────────────────────────────────────────────────────────────────┐
│ Write Behind 数据流 │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ 写操作: │
│ │
│ 应用 ──► 更新缓存 ◄────────────────────────────────────┐ │
│ │ │ │
│ ▼ │ │
│ 立即返回成功 │ │
│ │ │
│ 异步批量刷盘: │ │
│ │ │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │ │
│ │ 定时任务 │───►│ 批量合并 │───►│ 写入数据库 │ │ │
│ │ (每5秒) │ │ 相同 key │ │ │ │ │
│ └─────────────┘ └─────────────┘ └─────────────┘ │ │
│ │ │
│ 缓存失效时: │ │
│ │ │
│ 内存不足 ──► 进程退出 ──► 强制刷盘 ────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────┘

代码实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
class WriteBehindCache {
constructor(redis, db) {
this.redis = redis;
this.db = db;
this.writeQueue = new Map(); // 待写入队列
this.isFlushing = false;

// 启动定时刷盘任务
this.flushInterval = setInterval(() => this.flush(), 5000);
}

/**
* 写操作:只更新缓存,异步写回数据库
*/
async set(key, data) {
// 1. 更新缓存
await this.redis.set(key, JSON.stringify(data));

// 2. 加入待写入队列(合并相同 key 的更新)
this.writeQueue.set(key, data);

console.log('Write behind queued:', key);
return true; // 立即返回,不等待数据库
}

/**
* 异步批量刷盘
*/
async flush() {
if (this.isFlushing || this.writeQueue.size === 0) return;

this.isFlushing = true;
const batch = new Map(this.writeQueue);
this.writeQueue.clear();

console.log('Flushing', batch.size, 'items to DB');

try {
// 批量写入数据库(使用事务)
await this.db.transaction(async (trx) => {
for (const [key, data] of batch) {
await trx.query('UPDATE users SET ? WHERE id = ?', [data, key]);
}
});
console.log('Flush success:', batch.size, 'items');
} catch (err) {
// 刷盘失败,重新放回队列
for (const [key, data] of batch) {
if (!this.writeQueue.has(key)) {
this.writeQueue.set(key, data);
}
}
console.error('Flush failed, will retry:', err.message);
} finally {
this.isFlushing = false;
}
}

/**
* 读操作
*/
async get(key) {
const cached = await this.redis.get(key);
if (cached) {
return JSON.parse(cached);
}
return null;
}

/**
* 应用关闭时强制刷盘
*/
async shutdown() {
clearInterval(this.flushInterval);
await this.flush();
}
}

优缺点

优点 缺点
写操作性能极高(纯内存操作) 数据不是强一致性
可合并相同 key 的多次更新 异常宕机可能丢失数据
大幅降低数据库写入压力 实现逻辑复杂,需跟踪脏数据
适合高并发写入场景 需要额外的持久化保障机制

四种模式对比总结

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
┌─────────────────────────────────────────────────────────────────────┐
│ 四种缓存模式对比 │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ 一致性 ◄────────────────────────────────────────► 性能 │
│ │
│ Write Through Cache Aside Read Through Write Behind │
│ │ │ │ │ │
│ │ │ │ │ │
│ ▼ ▼ ▼ ▼ │
│ 强一致性 最终一致性 最终一致性 弱一致性 │
│ 写延迟高 实现简单 读透明 写性能最高 │
│ │
│ 适用场景: │
│ • Write Through:金融交易、库存扣减 │
│ • Cache Aside: 通用业务,读多写少 │
│ • Read Through: 统一缓存中间件 │
│ • Write Behind: 日志写入、计数器、高频更新 │
│ │
└─────────────────────────────────────────────────────────────────────┘
模式 数据一致性 读性能 写性能 实现复杂度 推荐场景
Cache Aside 最终一致 通用业务,读多写少
Read Through 最终一致 需要统一缓存层
Write Through 强一致 读写均衡,一致性要求高
Write Behind 弱一致 极高 高并发写入,可容忍延迟

生产环境最佳实践

1. 缓存雪崩防护

1
2
3
4
// 随机过期时间,避免大量缓存同时失效
const CACHE_TTL = 300;
const randomTTL = CACHE_TTL + Math.floor(Math.random() * 60);
await redis.setex(key, randomTTL, data);

2. 缓存击穿防护(热点 key)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// 使用互斥锁防止缓存击穿
async function getWithLock(key) {
const cached = await redis.get(key);
if (cached) return JSON.parse(cached);

// 获取分布式锁
const lock = await redis.set(`lock:${key}`, '1', 'EX', 10, 'NX');
if (!lock) {
// 其他进程正在加载,等待后重试
await new Promise(r => setTimeout(r, 100));
return getWithLock(key);
}

try {
const data = await db.query('SELECT * FROM users WHERE id = ?', [key]);
if (data) {
await redis.setex(key, 300, JSON.stringify(data));
}
return data;
} finally {
await redis.del(`lock:${key}`);
}
}

3. 缓存穿透防护(无效 key)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 空值缓存,防止频繁查询不存在的数据
async function getWithNullCache(key) {
const cached = await redis.get(key);
if (cached === '__NULL__') return null;
if (cached) return JSON.parse(cached);

const data = await db.query('SELECT * FROM users WHERE id = ?', [key]);

if (data) {
await redis.setex(key, 300, JSON.stringify(data));
} else {
// 缓存空值,短期失效
await redis.setex(key, 60, '__NULL__');
}

return data;
}

总结

四种缓存模式的选择:

  1. Cache Aside:最常用,代码里自己维护缓存逻辑,先更新数据库再删缓存,大部分业务场景都能用
  2. Read Through:缓存层自动加载,应用层无感知,适合做统一的缓存中间件
  3. Write Through:同步写缓存和数据库,一致性最好,但写性能受影响,适合对一致性要求高的场景
  4. Write Behind:异步批量写回,性能最高,但可能丢数据,适合日志、计数器这类能容忍延迟的场景

没有完美的方案,一致性、性能、可用性三者只能取其二。根据业务特点选合适的模式,再配合雪崩、击穿、穿透的防护,基本就能应付大部分场景了。