Vue 3 缓存架构设计深度指南
概述
缓存是提高系统性能的重要手段,它通过将频繁访问的数据存储在高速存储介质中,减少对慢速存储或计算资源的访问次数,从而提高系统的响应速度和吞吐量。本集将深入探讨缓存架构设计,包括缓存的基本概念、常见策略、前端缓存实现、后端缓存集成以及缓存一致性保证,帮助您构建高性能的Vue 3全栈应用。
一、缓存基本概念
1. 什么是缓存
缓存是一种临时存储机制,用于存储频繁访问的数据或计算结果,以减少对原始数据源的访问。它的核心思想是利用空间换时间,通过牺牲一定的存储空间来提高系统性能。
2. 缓存的优势
- 提高响应速度:缓存数据位于高速存储介质,访问速度快
- 减少数据库压力:降低对数据库的查询次数
- 提高系统吞吐量:减少服务器处理时间,支持更多并发请求
- 降低网络延迟:缓存可以部署在靠近用户的位置,减少网络传输时间
- 提高系统可用性:当原始数据源不可用时,缓存可以作为临时替代
3. 缓存分类
按存储位置分类
- 客户端缓存:存储在用户设备上,如浏览器缓存、应用本地缓存
- 服务器端缓存:存储在服务器上,如内存缓存、Redis缓存
- CDN缓存:存储在CDN节点上,用于加速静态资源访问
按缓存层次分类
- L1缓存:CPU内部缓存,速度最快
- L2缓存:CPU外部缓存,速度次之
- L3缓存:共享缓存,速度较慢
- 内存缓存:如Redis、Memcached
- 磁盘缓存:如文件系统缓存
按缓存数据类型分类
- 静态资源缓存:HTML、CSS、JavaScript、图片等
- 动态数据缓存:API响应、数据库查询结果等
- 计算结果缓存:复杂计算的结果
二、缓存策略
1. 常见缓存替换策略
| 策略 | 全称 | 描述 | 适用场景 |
|---|---|---|---|
| LRU | Least Recently Used | 最近最少使用,移除最久未访问的数据 | 大多数Web应用 |
| LFU | Least Frequently Used | 最少使用,移除访问频率最低的数据 | 访问频率差异大的场景 |
| FIFO | First In First Out | 先进先出,移除最早进入缓存的数据 | 简单场景,如队列 |
| LIFO | Last In First Out | 后进先出,移除最近进入缓存的数据 | 栈式访问模式 |
| MRU | Most Recently Used | 最近最常使用,移除最近访问的数据 | 热点数据频繁变化的场景 |
2. 缓存更新策略
(1)Cache-Aside(旁路缓存)
- 读取流程:先从缓存读取,若未命中则从数据库读取,然后更新缓存
- 写入流程:先更新数据库,然后删除缓存
- 优点:简单易用,适用于大多数场景
- 缺点:可能存在短暂的不一致
(2)Write-Through(写穿透)
- 写入流程:同时更新缓存和数据库
- 读取流程:从缓存读取,若未命中则从数据库读取
- 优点:缓存和数据库始终一致
- 缺点:写入性能较低
(3)Write-Back(写回)
- 写入流程:只更新缓存,异步更新数据库
- 读取流程:从缓存读取,若未命中则从数据库读取
- 优点:写入性能高
- 缺点:可能导致数据丢失,实现复杂
(4)Write-Around(写旁通)
- 写入流程:只更新数据库,不更新缓存
- 读取流程:从缓存读取,若未命中则从数据库读取并更新缓存
- 优点:适用于写入频繁但读取较少的场景
- 缺点:首次读取性能较低
3. 缓存失效策略
- TTL(Time To Live):设置缓存的生存时间,过期自动失效
- 手动失效:通过代码或命令手动删除或更新缓存
- 被动失效:当缓存容量不足时,根据替换策略自动失效
三、前端缓存实现
1. 浏览器缓存
强缓存
// 在后端设置响应头
res.setHeader('Cache-Control', 'max-age=31536000, immutable'); // 缓存1年
res.setHeader('Expires', new Date(Date.now() + 31536000000).toUTCString());协商缓存
// 在后端设置响应头
const etag = generateEtag(content);
res.setHeader('ETag', etag);
res.setHeader('Last-Modified', lastModified);
// 检查请求头
if (req.headers['if-none-match'] === etag || req.headers['if-modified-since'] === lastModified) {
return res.status(304).send(); // 未修改,使用缓存
}2. Vue 3 组件缓存
Keep-Alive 组件
<template>
<div class="app">
<keep-alive :include="['Home', 'About']" :max="10">
<router-view />
</keep-alive>
</div>
</template>
<script setup>
// Keep-Alive 会缓存不活动的组件实例,而不是销毁它们
// include: 只有名称匹配的组件会被缓存
// exclude: 任何名称匹配的组件都不会被缓存
// max: 最多可以缓存多少组件实例
</script>组件内部缓存
<template>
<div class="user-profile">
<h2>用户资料</h2>
<div v-if="isLoading">加载中...</div>
<div v-else>{{ user }}</div>
</div>
</template>
<script setup>
import { ref, computed, onMounted, watch } from 'vue';
const props = defineProps(['userId']);
const isLoading = ref(false);
const userCache = ref(new Map()); // 组件内部缓存
const user = computed(() => userCache.value.get(props.userId));
const fetchUser = async (userId) => {
if (userCache.value.has(userId)) {
return; // 使用缓存
}
isLoading.value = true;
try {
const response = await fetch(`/api/users/${userId}`);
const data = await response.json();
userCache.value.set(userId, data);
} catch (error) {
console.error('获取用户失败:', error);
} finally {
isLoading.value = false;
}
};
onMounted(() => {
fetchUser(props.userId);
});
watch(() => props.userId, (newUserId) => {
fetchUser(newUserId);
});
</script>3. Pinia 状态管理缓存
// src/stores/cache-store.js
import { defineStore } from 'pinia';
export const useCacheStore = defineStore('cache', {
state: () => ({
// 使用Map作为缓存存储
cache: new Map(),
// 缓存配置
config: {
ttl: 3600000, // 默认TTL:1小时
maxSize: 100 // 最大缓存数量
}
}),
getters: {
// 获取缓存项
get: (state) => (key) => {
const item = state.cache.get(key);
if (!item) return null;
// 检查是否过期
if (Date.now() > item.expiresAt) {
state.cache.delete(key);
return null;
}
// 更新访问时间(用于LRU)
item.lastAccessedAt = Date.now();
return item.value;
},
// 获取缓存大小
size: (state) => state.cache.size
},
actions: {
// 设置缓存项
set(key, value, ttl = this.config.ttl) {
// 如果缓存已满,移除最久未访问的项(LRU)
if (this.cache.size >= this.config.maxSize) {
this.evictLRU();
}
this.cache.set(key, {
value,
expiresAt: Date.now() + ttl,
lastAccessedAt: Date.now()
});
},
// 移除缓存项
remove(key) {
this.cache.delete(key);
},
// 清空缓存
clear() {
this.cache.clear();
},
// LRU 淘汰策略
evictLRU() {
let oldestKey = null;
let oldestTime = Infinity;
for (const [key, item] of this.cache.entries()) {
if (item.lastAccessedAt < oldestTime) {
oldestKey = key;
oldestTime = item.lastAccessedAt;
}
}
if (oldestKey) {
this.cache.delete(oldestKey);
}
},
// 预加载缓存
preload(keys, fetchFn) {
keys.forEach(key => {
if (!this.get(key)) {
fetchFn(key).then(value => {
this.set(key, value);
});
}
});
}
}
});4. 自定义缓存钩子
// src/composables/useCache.js
import { ref, computed } from 'vue';
import { useCacheStore } from '@/stores/cache-store';
export function useCache(key, fetchFn, ttl = 3600000) {
const cacheStore = useCacheStore();
const isLoading = ref(false);
const error = ref(null);
// 从缓存获取数据
const data = computed(() => cacheStore.get(key));
// 刷新数据
const refresh = async () => {
isLoading.value = true;
error.value = null;
try {
const result = await fetchFn();
cacheStore.set(key, result, ttl);
return result;
} catch (err) {
error.value = err;
throw err;
} finally {
isLoading.value = false;
}
};
// 初始化数据
const initialize = async () => {
if (!data.value) {
await refresh();
}
};
return {
data,
isLoading,
error,
refresh,
initialize
};
}使用自定义缓存钩子
<template>
<div class="product-detail">
<h2>产品详情</h2>
<div v-if="isLoading">加载中...</div>
<div v-else-if="error">加载失败: {{ error.message }}</div>
<div v-else-if="data">
<h3>{{ data.name }}</h3>
<p>{{ data.description }}</p>
<p class="price">¥{{ data.price }}</p>
<button @click="refresh">刷新</button>
</div>
</div>
</template>
<script setup>
import { onMounted } from 'vue';
import { useRoute } from 'vue-router';
import { useCache } from '@/composables/useCache';
const route = useRoute();
const productId = route.params.id;
// 使用自定义缓存钩子
const { data, isLoading, error, refresh, initialize } = useCache(
`product_${productId}`,
async () => {
const response = await fetch(`/api/products/${productId}`);
if (!response.ok) {
throw new Error('获取产品失败');
}
return response.json();
},
60000 // 缓存1分钟
);
// 初始化数据
onMounted(() => {
initialize();
});
</script>四、后端缓存实现(Node.js + Redis)
1. Redis 连接配置
安装依赖
# 安装 Redis 客户端
npm install ioredis
# 安装 Redis 速率限制库
npm install ioredis-rate-limit
# 安装配置管理库
npm install dotenvRedis 连接管理
// src/infrastructure/cache/redis-connection.js
const Redis = require('ioredis');
require('dotenv').config();
class RedisConnection {
constructor() {
this.client = null;
this.config = {
host: process.env.REDIS_HOST || 'localhost',
port: parseInt(process.env.REDIS_PORT || '6379'),
password: process.env.REDIS_PASSWORD || null,
db: parseInt(process.env.REDIS_DB || '0'),
retryStrategy: (times) => {
// 指数退避策略
const delay = Math.min(times * 50, 2000);
return delay;
},
reconnectOnError: (err) => {
const targetError = 'READONLY';
return err.message.includes(targetError);
},
maxRetriesPerRequest: null, // 无限重试
enableReadyCheck: true // 启用就绪检查
};
this.connected = false;
}
// 建立连接
async connect() {
try {
this.client = new Redis(this.config);
// 连接事件
this.client.on('connect', () => {
console.log('Redis 连接成功');
this.connected = true;
});
// 错误事件
this.client.on('error', (err) => {
console.error('Redis 错误:', err);
this.connected = false;
});
// 关闭事件
this.client.on('close', () => {
console.error('Redis 连接关闭');
this.connected = false;
});
// 就绪事件
this.client.on('ready', () => {
console.log('Redis 就绪');
this.connected = true;
});
return this.client;
} catch (error) {
console.error('创建 Redis 连接失败:', error);
throw error;
}
}
// 获取客户端实例
async getClient() {
if (!this.client || !this.connected) {
await this.connect();
}
return this.client;
}
// 关闭连接
async close() {
if (this.client) {
await this.client.quit();
this.client = null;
this.connected = false;
console.log('Redis 连接已关闭');
}
}
}
// 导出单例实例
const redisConnection = new RedisConnection();
module.exports = redisConnection;2. Redis 缓存服务
// src/infrastructure/cache/redis-cache-service.js
const redisConnection = require('./redis-connection');
class RedisCacheService {
constructor() {
this.defaultTTL = 3600; // 默认TTL:1小时
}
// 设置缓存
async set(key, value, ttl = this.defaultTTL) {
const client = await redisConnection.getClient();
const serializedValue = JSON.stringify(value);
await client.set(key, serializedValue, 'EX', ttl);
}
// 获取缓存
async get(key) {
const client = await redisConnection.getClient();
const value = await client.get(key);
return value ? JSON.parse(value) : null;
}
// 删除缓存
async delete(key) {
const client = await redisConnection.getClient();
await client.del(key);
}
// 批量获取缓存
async mget(keys) {
const client = await redisConnection.getClient();
const values = await client.mget(keys);
return values.map(value => value ? JSON.parse(value) : null);
}
// 批量设置缓存
async mset(keyValuePairs, ttl = this.defaultTTL) {
const client = await redisConnection.getClient();
const pipeline = client.pipeline();
for (const [key, value] of Object.entries(keyValuePairs)) {
pipeline.set(key, JSON.stringify(value), 'EX', ttl);
}
await pipeline.exec();
}
// 检查缓存是否存在
async exists(key) {
const client = await redisConnection.getClient();
return await client.exists(key) === 1;
}
// 设置缓存过期时间
async expire(key, ttl) {
const client = await redisConnection.getClient();
await client.expire(key, ttl);
}
// 获取缓存剩余过期时间
async ttl(key) {
const client = await redisConnection.getClient();
return await client.ttl(key);
}
// 递增操作
async incr(key) {
const client = await redisConnection.getClient();
return await client.incr(key);
}
// 递减操作
async decr(key) {
const client = await redisConnection.getClient();
return await client.decr(key);
}
// 哈希表设置
async hset(key, field, value) {
const client = await redisConnection.getClient();
await client.hset(key, field, JSON.stringify(value));
}
// 哈希表获取
async hget(key, field) {
const client = await redisConnection.getClient();
const value = await client.hget(key, field);
return value ? JSON.parse(value) : null;
}
// 清空缓存
async flushDb() {
const client = await redisConnection.getClient();
await client.flushDb();
}
}
module.exports = new RedisCacheService();3. 缓存中间件
// src/middlewares/cache-middleware.js
const redisCacheService = require('../infrastructure/cache/redis-cache-service');
// 缓存中间件
const cacheMiddleware = (ttl = 3600) => {
return async (req, res, next) => {
// 生成缓存键
const cacheKey = `api_${req.method}_${req.originalUrl}`;
try {
// 尝试从缓存获取
const cachedResponse = await redisCacheService.get(cacheKey);
if (cachedResponse) {
console.log(`缓存命中: ${cacheKey}`);
return res.json(cachedResponse);
}
// 保存原始的res.json方法
const originalJson = res.json;
// 重写res.json方法,保存响应到缓存
res.json = async (data) => {
// 调用原始的json方法
originalJson.call(res, data);
// 保存响应到缓存
await redisCacheService.set(cacheKey, data, ttl);
console.log(`缓存设置: ${cacheKey}`);
};
next();
} catch (error) {
console.error('缓存中间件错误:', error);
next(); // 缓存错误不影响正常请求
}
};
};
module.exports = cacheMiddleware;使用缓存中间件
// src/api/routes/product-routes.js
const express = require('express');
const router = express.Router();
const cacheMiddleware = require('../../middlewares/cache-middleware');
const productController = require('../controllers/product-controller');
// 对GET请求应用缓存中间件,缓存10分钟
router.get('/', cacheMiddleware(600), productController.getProducts);
router.get('/:id', cacheMiddleware(300), productController.getProductById);
// 非GET请求不应用缓存
router.post('/', productController.createProduct);
router.put('/:id', productController.updateProduct);
router.delete('/:id', productController.deleteProduct);
module.exports = router;4. 缓存一致性实现
// src/api/controllers/product-controller.js
const productRepository = require('../../infrastructure/persistence/repositories/product-repository');
const redisCacheService = require('../../infrastructure/cache/redis-cache-service');
class ProductController {
// 创建产品
async createProduct(req, res) {
try {
const product = await productRepository.create(req.body);
// 清除相关缓存
await this.clearProductCache();
res.status(201).json(product);
} catch (error) {
res.status(500).json({ error: '创建产品失败' });
}
}
// 更新产品
async updateProduct(req, res) {
try {
const product = await productRepository.update(req.params.id, req.body);
// 清除相关缓存
await this.clearProductCache();
await redisCacheService.delete(`api_GET_/api/products/${req.params.id}`);
res.json(product);
} catch (error) {
res.status(500).json({ error: '更新产品失败' });
}
}
// 删除产品
async deleteProduct(req, res) {
try {
await productRepository.delete(req.params.id);
// 清除相关缓存
await this.clearProductCache();
await redisCacheService.delete(`api_GET_/api/products/${req.params.id}`);
res.status(204).send();
} catch (error) {
res.status(500).json({ error: '删除产品失败' });
}
}
// 清除产品列表缓存
async clearProductCache() {
// 清除产品列表缓存
await redisCacheService.delete('api_GET_/api/products');
// 清除所有产品相关的缓存(实际项目中应更精确)
// 这里可以使用Redis的KEYS命令,但生产环境不推荐
// 推荐使用Redis的SCAN命令或使用更精确的缓存键命名策略
}
}
module.exports = new ProductController();五、缓存常见问题及解决方案
1. 缓存穿透
问题:请求不存在的数据,导致每次都访问数据库
解决方案:
- 布隆过滤器:在缓存前添加布隆过滤器,快速判断数据是否存在
- 空值缓存:对不存在的数据设置空值缓存,设置较短的TTL
布隆过滤器实现
// src/infrastructure/cache/bloom-filter.js
class BloomFilter {
constructor(size = 1000000, hashCount = 5) {
this.size = size;
this.hashCount = hashCount;
this.bitArray = new Array(size).fill(0);
}
// 哈希函数
#hash(value, seed) {
let hash = 0;
for (let i = 0; i < value.length; i++) {
hash = (hash * seed + value.charCodeAt(i)) % this.size;
}
return hash;
}
// 添加元素
add(value) {
const strValue = JSON.stringify(value);
for (let i = 0; i < this.hashCount; i++) {
const index = this.#hash(strValue, i + 1);
this.bitArray[index] = 1;
}
}
// 检查元素是否可能存在
mightContain(value) {
const strValue = JSON.stringify(value);
for (let i = 0; i < this.hashCount; i++) {
const index = this.#hash(strValue, i + 1);
if (this.bitArray[index] === 0) {
return false;
}
}
return true;
}
}
module.exports = BloomFilter;2. 缓存击穿
问题:热点数据过期,导致大量请求同时访问数据库
解决方案:
- 互斥锁:使用Redis分布式锁,只允许一个请求更新缓存
- 热点数据永不过期:对热点数据不设置TTL,定期异步更新
- 缓存预热:提前加载热点数据到缓存
分布式锁实现
// src/infrastructure/cache/distributed-lock.js
const redisCacheService = require('./redis-cache-service');
class DistributedLock {
constructor() {
this.lockPrefix = 'lock_';
this.defaultExpire = 10; // 默认锁过期时间(秒)
}
// 获取锁
async acquire(key, expire = this.defaultExpire) {
const lockKey = `${this.lockPrefix}${key}`;
const lockValue = `${Date.now()}_${Math.random()}`;
// 使用SETNX命令获取锁
const result = await redisCacheService.client.set(lockKey, lockValue, 'NX', 'EX', expire);
return result === 'OK' ? lockValue : null;
}
// 释放锁
async release(key, value) {
const lockKey = `${this.lockPrefix}${key}`;
// 使用Lua脚本原子性释放锁
const script = `
if redis.call('GET', KEYS[1]) == ARGV[1] then
return redis.call('DEL', KEYS[1])
else
return 0
end
`;
return await redisCacheService.client.eval(script, 1, lockKey, value);
}
// 尝试获取锁,带重试
async tryAcquire(key, maxRetries = 5, retryDelay = 100, expire = this.defaultExpire) {
for (let i = 0; i < maxRetries; i++) {
const lockValue = await this.acquire(key, expire);
if (lockValue) {
return lockValue;
}
await new Promise(resolve => setTimeout(resolve, retryDelay));
}
return null;
}
}
module.exports = new DistributedLock();3. 缓存雪崩
问题:大量缓存同时过期,导致数据库压力骤增
解决方案:
- 随机过期时间:为缓存项设置随机TTL,避免同时过期
- 分层缓存:使用多级缓存,不同层级设置不同TTL
- 限流降级:对数据库请求进行限流,设置降级策略
- 缓存预热:提前加载数据到缓存
随机TTL实现
// src/infrastructure/cache/redis-cache-service.js
const redisConnection = require('./redis-connection');
class RedisCacheService {
constructor() {
this.defaultTTL = 3600; // 默认TTL:1小时
this.randomTTLRange = 300; // 随机TTL范围:±5分钟
}
// 设置缓存,带随机TTL
async set(key, value, ttl = this.defaultTTL) {
// 添加随机TTL,避免雪崩
const randomTTL = ttl + Math.floor(Math.random() * this.randomTTLRange * 2) - this.randomTTLRange;
const client = await redisConnection.getClient();
const serializedValue = JSON.stringify(value);
await client.set(key, serializedValue, 'EX', randomTTL);
}
// 其他方法...
}
module.exports = new RedisCacheService();六、缓存监控与最佳实践
1. 缓存监控指标
- 缓存命中率:缓存命中次数 / (缓存命中次数 + 缓存未命中次数)
- 缓存穿透率:缓存穿透次数 / 总请求次数
- 缓存过期率:过期缓存数量 / 总缓存数量
- 缓存大小:缓存占用的存储空间
- 缓存延迟:从缓存读取数据的时间
- 缓存错误率:缓存操作错误次数 / 总缓存操作次数
2. Redis 监控命令
# 查看Redis信息
redis-cli info
# 查看内存使用情况
redis-cli info memory
# 查看命中率
redis-cli info stats | grep keyspace_hits
redis-cli info stats | grep keyspace_misses
# 查看慢查询
redis-cli config get slowlog-log-slower-than
redis-cli slowlog get
# 查看当前连接数
redis-cli info clients | grep connected_clients3. 缓存最佳实践
(1)缓存键设计
- 使用前缀:如
user_123、product_456 - 包含版本号:如
v1_user_123,便于缓存迁移 - 包含环境信息:如
dev_user_123、prod_user_123 - 使用唯一标识符:如UUID,避免冲突
- 保持简洁:避免过长的缓存键
(2)缓存数据设计
- 只缓存必要数据:避免缓存过大的数据
- 序列化选择:JSON、MessagePack、Protocol Buffers等
- 压缩数据:对大对象进行压缩
- 避免缓存敏感数据:如密码、令牌等
(3)性能优化
- 批量操作:使用mget、mset等批量命令
- 管道操作:减少网络往返次数
- 使用连接池:避免频繁创建和关闭连接
- 选择合适的数据结构:根据业务需求选择Hash、List、Set、Sorted Set等
(4)安全性考虑
- 限制Redis访问:使用防火墙和密码认证
- 避免使用KEYS命令:生产环境使用SCAN命令
- 设置合理的内存限制:避免Redis占用过多内存
- 定期备份:确保数据安全
七、总结
本集深入探讨了缓存架构设计的核心概念、策略和实现方案,包括:
- 缓存基础:从缓存的基本概念、分类到常见的缓存策略
- 前端缓存:Vue 3组件缓存、Pinia状态管理缓存和自定义缓存钩子
- 后端缓存:Node.js + Redis的完整实现,包括连接管理、缓存服务和中间件
- 缓存问题解决方案:缓存穿透、击穿、雪崩的应对策略
- 缓存监控和最佳实践:如何监控缓存性能和遵循最佳实践
通过本集的学习,您应该掌握了如何在Vue 3全栈应用中设计和实现高效、可靠的缓存架构。缓存是提高系统性能的重要手段,但也需要谨慎设计,避免引入新的问题。在实际项目中,应根据具体业务需求选择合适的缓存策略和实现方案,并结合监控工具持续优化缓存性能。
代码仓库
本集示例代码已上传至GitHub:
- 完整项目:https://github.com/example/vue3-full-stack
- 分支:
feature/cache-architecture
下集预告
下一集将深入探讨搜索架构实现,包括搜索的基本概念、技术选型、索引设计、搜索算法以及与Elasticsearch的集成方案。我们将学习如何构建高效的搜索系统,为用户提供快速、准确的搜索体验。