第 8 篇:缓存策略 —— 快,但别错
在前面的文章中,我们探讨了事务管理等重要话题。现在,让我们关注另一个关键的横切关注点:缓存策略。
缓存是提升系统性能的重要手段,但错误的缓存策略可能导致数据不一致、缓存穿透、缓存雪崩等问题。正确实现缓存策略需要在性能和一致性之间找到平衡。
缓存穿透/雪崩/击穿防护
让我们首先了解缓存的三种常见问题及其解决方案:
typescript
// 缓存服务
@Injectable()
export class CacheService {
constructor(
@Inject('CACHE_MANAGER') private readonly cacheManager: Cache,
) {}
// 基础缓存获取方法,包含穿透防护
async get<T>(key: string, loader: () => Promise<T>, ttl?: number): Promise<T> {
// 尝试从缓存获取
let value = await this.cacheManager.get<T>(key);
if (value !== undefined && value !== null) {
return value;
}
// 缓存未命中,从数据源加载
try {
value = await loader();
// 加载成功后写入缓存
if (value !== undefined && value !== null) {
await this.cacheManager.set(key, value, ttl);
}
return value;
} catch (error) {
// 数据加载失败,返回默认值或抛出异常
throw error;
}
}
// 带空值缓存的获取方法(防止缓存穿透)
async getWithNullProtection<T>(
key: string,
loader: () => Promise<T | null>,
ttl?: number,
nullTtl: number = 60, // 空值缓存时间较短
): Promise<T | null> {
// 尝试从缓存获取
const cached = await this.cacheManager.get<{value: T | null, isEmpty: boolean}>(key);
if (cached !== undefined) {
// 如果是空值标记,返回null
if (cached.isEmpty) {
return null;
}
return cached.value;
}
// 缓存未命中,从数据源加载
try {
const value = await loader();
if (value !== undefined && value !== null) {
// 正常值缓存
await this.cacheManager.set(key, {value, isEmpty: false}, ttl);
} else {
// 空值也缓存,但时间较短
await this.cacheManager.set(key, {value: null, isEmpty: true}, nullTtl);
}
return value;
} catch (error) {
throw error;
}
}
// 带分布式锁的获取方法(防止缓存击穿)
async getWithLock<T>(
key: string,
loader: () => Promise<T>,
ttl?: number,
lockTimeout: number = 5000, // 锁超时时间
): Promise<T> {
// 尝试从缓存获取
const value = await this.cacheManager.get<T>(key);
if (value !== undefined && value !== null) {
return value;
}
// 获取分布式锁
const lockKey = `lock:${key}`;
const lock = await this.acquireLock(lockKey, lockTimeout);
if (!lock) {
// 获取锁失败,可能是热点数据,短暂等待后重试
await this.delay(100);
return this.get(key, loader, ttl);
}
try {
// 再次检查缓存(双重检查)
const cachedValue = await this.cacheManager.get<T>(key);
if (cachedValue !== undefined && cachedValue !== null) {
return cachedValue;
}
// 从数据源加载
const loadedValue = await loader();
if (loadedValue !== undefined && loadedValue !== null) {
await this.cacheManager.set(key, loadedValue, ttl);
}
return loadedValue;
} finally {
// 释放锁
await this.releaseLock(lockKey);
}
}
// 简单的分布式锁实现
private async acquireLock(key: string, timeout: number): Promise<boolean> {
try {
// 使用 Redis 的 SET 命令实现分布式锁
const result = await this.cacheManager.store.getClient().set(
key,
'1',
'PX',
timeout,
'NX'
);
return result === 'OK';
} catch (error) {
return false;
}
}
private async releaseLock(key: string): Promise<void> {
try {
// 使用 Lua 脚本安全释放锁
const script = `
if redis.call("get", KEYS[1]) == ARGV[1] then
return redis.call("del", KEYS[1])
else
return 0
end
`;
await this.cacheManager.store.getClient().eval(script, 1, key, '1');
} catch (error) {
// 忽略释放锁的错误
}
}
private delay(ms: number): Promise<void> {
return new Promise(resolve => setTimeout(resolve, ms));
}
}多租户下的缓存 key 设计:order:{tenantId}:{id}
在多租户系统中,缓存 key 的设计需要考虑租户隔离:
typescript
// 多租户缓存键生成器
@Injectable()
export class TenantCacheKeyGenerator {
generateKey(
tenantId: string,
resourceType: string,
resourceId?: string | number,
...additionalParts: (string | number)[]
): string {
const parts = [resourceType, tenantId];
if (resourceId !== undefined) {
parts.push(String(resourceId));
}
if (additionalParts.length > 0) {
parts.push(...additionalParts.map(String));
}
return parts.join(':');
}
// 生成用户相关缓存键
userKey(tenantId: string, userId: string): string {
return this.generateKey(tenantId, 'user', userId);
}
// 生成订单相关缓存键
orderKey(tenantId: string, orderId?: string): string {
return this.generateKey(tenantId, 'order', orderId);
}
// 生成订单列表缓存键
orderListKey(tenantId: string, userId: string, status?: string): string {
const parts = ['order-list', tenantId, userId];
if (status) {
parts.push(status);
}
return parts.join(':');
}
// 生成统计数据缓存键
statsKey(tenantId: string, type: string, period: string): string {
return this.generateKey(tenantId, 'stats', type, period);
}
}
// 在服务中使用缓存键生成器
@Injectable()
export class OrderService {
constructor(
@InjectRepository(Order)
private readonly orderRepository: Repository<Order>,
private readonly cacheService: CacheService,
private readonly cacheKeyGenerator: TenantCacheKeyGenerator,
) {}
async getOrder(tenantId: string, orderId: string): Promise<Order> {
const cacheKey = this.cacheKeyGenerator.orderKey(tenantId, orderId);
return this.cacheService.getWithLock(
cacheKey,
() => this.orderRepository.findOne({
where: { id: orderId, tenantId }
}),
300, // 5分钟缓存
);
}
async getOrdersByUser(tenantId: string, userId: string, status?: string): Promise<Order[]> {
const cacheKey = this.cacheKeyGenerator.orderListKey(tenantId, userId, status);
return this.cacheService.getWithNullProtection(
cacheKey,
() => this.orderRepository.find({
where: {
tenantId,
userId,
...(status ? { status } : {})
}
}),
120, // 2分钟缓存
);
}
// 清除相关缓存
async invalidateOrderCache(tenantId: string, orderId: string): Promise<void> {
const orderKey = this.cacheKeyGenerator.orderKey(tenantId, orderId);
const orderListKeyPattern = this.cacheKeyGenerator.generateKey(tenantId, 'order-list', '*');
await this.cacheService.del(orderKey);
// 注意:通配符删除需要 Redis 支持
// await this.cacheService.delPattern(orderListKeyPattern);
}
}缓存与数据库一致性(Cache-Aside vs Write-Through)
不同的缓存策略适用于不同的场景:
typescript
// Cache-Aside 模式(旁路缓存)
@Injectable()
export class CacheAsideService {
constructor(
private readonly cacheService: CacheService,
private readonly tenantCacheKeyGenerator: TenantCacheKeyGenerator,
) {}
async getCachedData<T>(
tenantId: string,
key: string,
loader: () => Promise<T>,
ttl?: number,
): Promise<T> {
return this.cacheService.getWithLock(
`${tenantId}:${key}`,
loader,
ttl,
);
}
async invalidateCache(tenantId: string, key: string): Promise<void> {
await this.cacheService.del(`${tenantId}:${key}`);
}
}
// Write-Through 模式(直写缓存)
@Injectable()
export class WriteThroughService {
constructor(
private readonly cacheService: CacheService,
private readonly tenantCacheKeyGenerator: TenantCacheKeyGenerator,
) {}
async saveData<T>(
tenantId: string,
key: string,
data: T,
saver: () => Promise<void>,
ttl?: number,
): Promise<void> {
// 先写入数据库
await saver();
// 再写入缓存
await this.cacheService.set(`${tenantId}:${key}`, data, ttl);
}
}
// Write-Behind 模式(回写缓存)
@Injectable()
export class WriteBehindService {
private readonly writeQueue: Array<{
tenantId: string;
key: string;
data: any;
saver: () => Promise<void>;
ttl?: number;
}> = [];
constructor(
private readonly cacheService: CacheService,
private readonly tenantCacheKeyGenerator: TenantCacheKeyGenerator,
) {
// 定期处理写队列
setInterval(() => this.processWriteQueue(), 1000);
}
async updateData<T>(
tenantId: string,
key: string,
data: T,
saver: () => Promise<void>,
ttl?: number,
): Promise<void> {
// 立即更新缓存
await this.cacheService.set(`${tenantId}:${key}`, data, ttl);
// 加入写队列,异步写入数据库
this.writeQueue.push({ tenantId, key, data, saver, ttl });
}
private async processWriteQueue(): Promise<void> {
while (this.writeQueue.length > 0) {
const item = this.writeQueue.shift();
if (item) {
try {
await item.saver();
} catch (error) {
console.error('Failed to write to database:', error);
// 可以重新加入队列或记录错误
}
}
}
}
}
// 实际应用示例
@Injectable()
export class UserService {
constructor(
@InjectRepository(User)
private readonly userRepository: Repository<User>,
private readonly cacheAsideService: CacheAsideService,
private readonly writeThroughService: WriteThroughService,
private readonly tenantCacheKeyGenerator: TenantCacheKeyGenerator,
) {}
// 读多写少的场景使用 Cache-Aside
async getUser(tenantId: string, userId: string): Promise<User> {
const cacheKey = this.tenantCacheKeyGenerator.userKey(tenantId, userId);
return this.cacheAsideService.getCachedData(
tenantId,
cacheKey,
() => this.userRepository.findOne({ where: { id: userId, tenantId } }),
300, // 5分钟缓存
);
}
// 写操作使用 Write-Through 确保一致性
async updateUser(tenantId: string, userId: string, updateData: Partial<User>): Promise<User> {
const cacheKey = this.tenantCacheKeyGenerator.userKey(tenantId, userId);
const user = await this.userRepository.findOne({ where: { id: userId, tenantId } });
if (!user) {
throw new ResourceNotFoundException('User', userId);
}
Object.assign(user, updateData);
return this.writeThroughService.saveData(
tenantId,
cacheKey,
user,
() => this.userRepository.save(user),
300,
);
}
// 批量更新使用 Write-Behind 提升性能
async batchUpdateUsers(
tenantId: string,
updates: Array<{ userId: string; data: Partial<User> }>,
): Promise<void> {
for (const update of updates) {
const cacheKey = this.tenantCacheKeyGenerator.userKey(tenantId, update.userId);
const user = await this.userRepository.findOne({ where: { id: update.userId, tenantId } });
if (user) {
Object.assign(user, update.data);
await this.writeBehindService.updateData(
tenantId,
cacheKey,
user,
() => this.userRepository.save(user),
300,
);
}
}
}
}Redis 命名空间隔离(防测试污染生产)
在多环境部署中,需要确保不同环境的缓存数据隔离:
typescript
// 环境感知的缓存服务
@Injectable()
export class EnvironmentAwareCacheService {
private readonly namespace: string;
constructor(
@Inject('CACHE_MANAGER') private readonly cacheManager: Cache,
private readonly configService: ConfigService,
) {
// 根据环境生成命名空间
const env = this.configService.get<string>('NODE_ENV', 'development');
const serviceName = this.configService.get<string>('SERVICE_NAME', 'saas-app');
this.namespace = `${serviceName}:${env}`;
}
private getNamespacedKey(key: string): string {
return `${this.namespace}:${key}`;
}
async get<T>(key: string): Promise<T> {
return this.cacheManager.get(this.getNamespacedKey(key));
}
async set(key: string, value: any, ttl?: number): Promise<void> {
await this.cacheManager.set(this.getNamespacedKey(key), value, ttl);
}
async del(key: string): Promise<void> {
await this.cacheManager.del(this.getNamespacedKey(key));
}
async delPattern(pattern: string): Promise<void> {
// 注意:这个方法需要 Redis 客户端支持
const namespacedPattern = this.getNamespacedKey(pattern);
const keys = await this.cacheManager.store.getClient().keys(namespacedPattern);
if (keys.length > 0) {
await this.cacheManager.store.getClient().del(...keys);
}
}
async clearAll(): Promise<void> {
// 清除当前环境的所有缓存
const pattern = this.getNamespacedKey('*');
await this.delPattern(pattern);
}
}
// 缓存预热服务
@Injectable()
export class CacheWarmupService {
constructor(
private readonly environmentAwareCacheService: EnvironmentAwareCacheService,
private readonly userService: UserService,
private readonly orderService: OrderService,
) {}
@Cron('0 2 * * *') // 每天凌晨2点执行
async warmupCache(): Promise<void> {
try {
// 预热热点数据
console.log('Starting cache warmup...');
// 预热用户统计数据
await this.warmupUserStats();
// 预热订单统计数据
await this.warmupOrderStats();
console.log('Cache warmup completed');
} catch (error) {
console.error('Cache warmup failed:', error);
}
}
private async warmupUserStats(): Promise<void> {
// 实现用户统计数据预热逻辑
}
private async warmupOrderStats(): Promise<void> {
// 实现订单统计数据预热逻辑
}
}
// 缓存监控服务
@Injectable()
export class CacheMonitoringService {
constructor(
@Inject('CACHE_MANAGER') private readonly cacheManager: Cache,
) {}
async getCacheStats(): Promise<any> {
try {
// 获取 Redis 统计信息
const info = await this.cacheManager.store.getClient().info();
return this.parseRedisInfo(info);
} catch (error) {
return { error: error.message };
}
}
private parseRedisInfo(info: string): any {
const stats: any = {};
const lines = info.split('\n');
for (const line of lines) {
if (line.includes(':')) {
const [key, value] = line.split(':');
stats[key.trim()] = value.trim();
}
}
return stats;
}
@Cron('*/30 * * * *') // 每30分钟执行一次
async reportCacheStats(): Promise<void> {
const stats = await this.getCacheStats();
console.log('Cache Stats:', stats);
// 可以发送到监控系统
}
}创业团队行动清单
立即行动:
- 实现基础的缓存服务,包含穿透、击穿防护
- 设计多租户环境下的缓存键命名规范
- 选择合适的缓存策略(Cache-Aside 为主)
一周内完成:
- 实现缓存失效机制
- 添加环境隔离的缓存命名空间
- 建立缓存监控和统计
一月内完善:
- 实现缓存预热机制
- 添加复杂的缓存策略(Write-Through、Write-Behind)
- 建立缓存相关的性能测试和调优
总结
缓存策略是提升系统性能的关键,但需要谨慎设计:
- 防护机制:实现缓存穿透、击穿、雪崩的防护
- 多租户隔离:设计合理的缓存键命名规范
- 一致性策略:根据业务场景选择合适的缓存策略
- 环境隔离:确保不同环境的缓存数据不会相互污染
- 监控运维:建立缓存监控和维护机制
在下一篇文章中,我们将探讨可观测性,这是保障系统稳定运行的重要手段。