缓存,是支撑抖音、头条等超大流量平台“毫秒级响应”的关键组件。字节跳动的缓存体系通过多级缓存架构+一致性控制+高可用设计实现了从容应对亿级访问压力。本篇文章从架构设计讲起,结合 Node.js 演示一个多级缓存系统原型,揭示其背后的设计思路。
🧠 一、为什么要构建多级缓存?
- 性能需求:数据库查询慢,访问延迟高
- 成本控制:热点数据频繁访问,数据库成本高
- 可用性保障:即使后端宕机,也能提供服务
字节典型多级缓存结构:
[Browser Cache] → [Local Cache] → [Redis/Memcached] → [DB/存储系统]
🏗️ 二、字节跳动的缓存体系设计亮点
| 组件 | 功能作用 |
|---|---|
| 浏览器缓存 | 静态资源走 CDN,页面级缓存(强缓存、协商缓存) |
| 本地缓存(Local) | 单机内存缓存热点数据,极致低延迟 |
| 分布式缓存 | Redis 作为统一热点数据共享层,支持集群+哨兵 |
| 异地缓存 | 火山引擎多地部署,减少跨区访问延迟 |
| 一致性策略 | 数据更新触发清除 + 延迟双删 + TTL 策略 |
| 旁路缓存 | 请求未命中时异步更新缓存 |
⚙️ 三、Node.js 演示:多级缓存系统原型
我们模拟一个系统,先查本地缓存,再查 Redis,最后查数据库(模拟)。
1. 安装依赖
npm install redis
2. 代码实现
const redis = require("redis");
const client = redis.createClient();
client.connect();
const localCache = new Map();
async function getUserInfo(userId) {
// 1. 本地缓存查找
if (localCache.has(userId)) {
console.log("命中本地缓存");
return localCache.get(userId);
}
// 2. Redis 查找
let redisData = await client.get(`user:${userId}`);
if (redisData) {
console.log("命中 Redis 缓存");
localCache.set(userId, redisData);
return redisData;
}
// 3. 模拟数据库查找
console.log("命中数据库");
const dbData = `user-${userId}-from-db`;
// 缓存数据
await client.set(`user:${userId}`, dbData, { EX: 60 }); // 设置 60s 过期
localCache.set(userId, dbData);
return dbData;
}
3. 使用示例:
getUserInfo(123).then(console.log); // 第一次命中 DB
setTimeout(() => getUserInfo(123).then(console.log), 2000); // 第二次命中本地缓存
🔄 四、工程中进阶策略
- 缓存穿透:空数据写入 Redis,防止频繁打到 DB
- 缓存击穿:热门 key 失效瞬间加锁防止并发打爆 DB
- 缓存雪崩:热点 key 设置随机 TTL 避免集中失效
- 延迟双删:更新 DB 时延迟删除 Redis 缓存,确保一致性
- 异步预热:提前加载热门数据,减少冷启动抖动
✍️ 五、总结与思考
- 缓存不只是 Redis,而是一个完整的分层设计
- 本地缓存是成本最低、速度最快的一层
- 字节跳动将缓存当作系统级能力在构建,而不是简单的优化手段
🎁 拓展推荐
- Redis 官方文档
- 《缓存:从入门到亿级流量》by 美团技术团队
- 字节跳动技术博客:《缓存系统在字节跳动的演进》