做高并发系统少不了缓存,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 ; } async get (key ) { const cached = await this .redis .get (key); if (cached) { console .log ('Cache hit:' , key); return JSON .parse (cached); } console .log ('Cache miss:' , key); 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; } async update (key, data ) { await this .db .query ('UPDATE users SET ? WHERE id = ?' , [data, key]); await this .redis .del (key); console .log ('Cache invalidated:' , key); } async delete (key ) { await this .db .query ('DELETE FROM users WHERE id = ?' , [key]); 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 ) { await this .redis .setex (key, this .CACHE_TTL , JSON .stringify (data)); try { await this .db .query ('UPDATE users SET ? WHERE id = ?' , [data, key]); console .log ('Write through success:' , key); } catch (err) { 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 ) { await this .redis .set (key, JSON .stringify (data)); 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; }
总结 四种缓存模式的选择:
Cache Aside :最常用,代码里自己维护缓存逻辑,先更新数据库再删缓存,大部分业务场景都能用
Read Through :缓存层自动加载,应用层无感知,适合做统一的缓存中间件
Write Through :同步写缓存和数据库,一致性最好,但写性能受影响,适合对一致性要求高的场景
Write Behind :异步批量写回,性能最高,但可能丢数据,适合日志、计数器这类能容忍延迟的场景
没有完美的方案,一致性、性能、可用性三者只能取其二。根据业务特点选合适的模式,再配合雪崩、击穿、穿透的防护,基本就能应付大部分场景了。