缓存穿透/击穿/雪崩实战
大约 10 分钟约 3143 字
缓存穿透/击穿/雪崩实战
简介
缓存是提升系统性能的核心手段,但不当使用会引发穿透、击穿和雪崩三大问题。本文深入讲解三种缓存问题的原理、解决方案和实战代码,涵盖内存缓存、分布式缓存和多级缓存架构。
特点
缓存穿透
原理与解决
// 缓存穿透:查询不存在的数据,缓存无法命中,请求直达数据库
// 例如:恶意请求 userId=-1 或不存在的 ID
// 方案 1:缓存空值
public class CacheService
{
private readonly IMemoryCache _cache;
private readonly AppDbContext _db;
public async Task<User?> GetUserAsync(int userId)
{
string key = $"user:{userId}";
// 缓存命中(包括空值)
if (_cache.TryGetValue<User?>(key, out var cached))
return cached; // 可能返回 null(空值缓存)
// 查询数据库
var user = await _db.Users.FindAsync(userId);
// 缓存结果(包括空值)
_cache.Set(key, user, new MemoryCacheEntryOptions
{
AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(5), // 空值短过期
SlidingExpiration = TimeSpan.FromMinutes(1)
});
return user;
}
}
// 方案 2:布隆过滤器(前置过滤)
public class BloomFilterCache
{
private readonly IMemoryCache _cache;
private readonly AppDbContext _db;
private readonly BloomFilter _bloomFilter;
public BloomFilterCache()
{
// 初始化布隆过滤器(从数据库加载所有有效 ID)
_bloomFilter = new BloomFilter(expectedElements: 1_000_000, errorRate: 0.01);
}
public async Task<User?> GetUserAsync(int userId)
{
// 布隆过滤器判断:如果不存在,肯定不存在
if (!_bloomFilter.MightContain(userId))
return null; // 直接返回,不查库
string key = $"user:{userId}";
if (_cache.TryGetValue<User>(key, out var user))
return user;
user = await _db.Users.FindAsync(userId);
if (user != null)
_cache.Set(key, user, TimeSpan.FromMinutes(30));
return user;
}
}缓存击穿
热点 Key 过期
// 缓存击穿:热点 key 过期瞬间,大量请求同时查库
// 例如:热门商品信息缓存过期,瞬间大量请求打到数据库
// 方案 1:互斥锁(Mutex)
public class MutexCacheService
{
private readonly IMemoryCache _cache;
private readonly IDistributedCache _distCache;
private readonly SemaphoreSlim _lock = new(1, 1);
private readonly AppDbContext _db;
public async Task<Product?> GetProductAsync(int productId)
{
string key = $"product:{productId}";
// 第一层:内存缓存
if (_cache.TryGetValue(key, out Product? product))
return product;
// 互斥锁:只允许一个请求重建缓存
await _lock.WaitAsync();
try
{
// Double-check(其他线程等待期间可能已重建)
if (_cache.TryGetValue(key, out product))
return product;
// 查询数据库
product = await _db.Products.FindAsync(productId);
if (product != null)
{
// 重建缓存
_cache.Set(key, product, TimeSpan.FromMinutes(30));
}
return product;
}
finally
{
_lock.Release();
}
}
}
// 方案 2:分布式锁(RedLock)
public class DistributedLockCacheService
{
private readonly IConnectionMultiplexer _redis;
private readonly AppDbContext _db;
public async Task<Product?> GetProductWithLockAsync(int productId)
{
var db = _redis.GetDatabase();
string cacheKey = $"product:{productId}";
string lockKey = $"lock:product:{productId}";
// 尝试获取缓存
var cached = await db.StringGetAsync(cacheKey);
if (cached.HasValue)
return JsonSerializer.Deserialize<Product>(cached!);
// 获取分布式锁
var lockValue = Guid.NewGuid().ToString();
bool acquired = await db.StringSetAsync(lockKey, lockValue,
TimeSpan.FromSeconds(10), When.NotExists);
if (acquired)
{
try
{
// Double-check
cached = await db.StringGetAsync(cacheKey);
if (cached.HasValue)
return JsonSerializer.Deserialize<Product>(cached!);
// 查库并缓存
var product = await _db.Products.FindAsync(productId);
if (product != null)
{
await db.StringSetAsync(cacheKey,
JsonSerializer.Serialize(product),
TimeSpan.FromMinutes(30));
}
return product;
}
finally
{
// Lua 脚本安全释放锁(检查值是否匹配)
var lua = "if redis.call('get',KEYS[1]) == ARGV[1] then return redis.call('del',KEYS[1]) else return 0 end";
await db.ScriptEvaluateAsync(lua,
new RedisKey[] { lockKey },
new RedisValue[] { lockValue });
}
}
else
{
// 未获取锁,等待后重试
await Task.Delay(100);
return await GetProductWithLockAsync(productId);
}
}
}
// 方案 3:永不过期 + 异步刷新
public class AsyncRefreshCache
{
private readonly IMemoryCache _cache;
public async Task<T> GetOrRefreshAsync<T>(string key, Func<Task<T>> factory, TimeSpan expiration)
{
if (_cache.TryGetValue<CacheEntry<T>>(key, out var entry))
{
// 如果接近过期,异步刷新
if (entry!.ExpireAt - DateTime.UtcNow < TimeSpan.FromMinutes(1))
{
_ = Task.Run(async () =>
{
try
{
var fresh = await factory();
_cache.Set(key, new CacheEntry<T>(fresh, expiration), expiration);
}
catch { /* 静默失败,使用旧值 */ }
});
}
return entry.Value;
}
var value = await factory();
_cache.Set(key, new CacheEntry<T>(value, expiration), expiration);
return value;
}
record CacheEntry<T>(T Value, TimeSpan Lifetime)
{
public DateTime ExpireAt { get; } = DateTime.UtcNow + Lifetime;
}
}缓存雪崩
批量过期解决
// 缓存雪崩:大量 key 在同一时刻过期,请求全部打到数据库
// 原因:缓存使用相同的过期时间(如:全部30分钟)
// 方案 1:随机过期时间
public class AvalancheSafeCache
{
private readonly IMemoryCache _cache;
private readonly Random _random = new();
public void Set<T>(string key, T value, TimeSpan baseExpiration)
{
// 在基础过期时间上添加随机偏移(±20%)
double jitter = baseExpiration.TotalSeconds * (_random.NextDouble() * 0.4 - 0.2);
var expiration = baseExpiration.Add(TimeSpan.FromSeconds(jitter));
_cache.Set(key, value, new MemoryCacheEntryOptions
{
AbsoluteExpirationRelativeToNow = expiration
});
}
}
// 方案 2:多级缓存(L1 + L2)
public class MultiLevelCache
{
private readonly IMemoryCache _l1Cache; // L1: 内存缓存
private readonly IDistributedCache _l2Cache; // L2: Redis
private readonly AppDbContext _db;
public async Task<T?> GetAsync<T>(string key, Func<Task<T?>> factory,
TimeSpan l1Expiration, TimeSpan l2Expiration)
{
// L1: 内存缓存(最快)
if (_l1Cache.TryGetValue<T>(key, out var l1Value))
return l1Value;
// L2: 分布式缓存
var l2Data = await _l2Cache.GetStringAsync(key);
if (l2Data != null)
{
var l2Value = JsonSerializer.Deserialize<T>(l2Data);
// 回填 L1
_l1Cache.Set(key, l2Value, l1Expiration);
return l2Value;
}
// 数据库
var dbValue = await factory();
if (dbValue != null)
{
// 回填 L2 和 L1
await _l2Cache.SetStringAsync(key,
JsonSerializer.Serialize(dbValue),
new DistributedCacheEntryOptions
{
AbsoluteExpirationRelativeToNow = l2Expiration
});
_l1Cache.Set(key, dbValue, l1Expiration);
}
return dbValue;
}
}
// 方案 3:熔断降级
public class CircuitBreakerCache
{
private readonly IMemoryCache _cache;
private int _failureCount;
private DateTime _circuitOpenTime;
private bool _isCircuitOpen;
public async Task<T?> GetWithCircuitBreakerAsync<T>(
string key, Func<Task<T?>> factory, TimeSpan expiration)
{
// 熔断状态检查
if (_isCircuitOpen)
{
if (DateTime.UtcNow - _circuitOpenTime > TimeSpan.FromSeconds(30))
_isCircuitOpen = false; // 尝试半开
else
return _cache.TryGetValue<T>(key, out var fallback) ? fallback : default;
}
try
{
if (_cache.TryGetValue<T>(key, out var cached))
return cached;
var value = await factory();
if (value != null)
{
_cache.Set(key, value, expiration);
_failureCount = 0;
}
return value;
}
catch
{
_failureCount++;
if (_failureCount >= 5)
{
_isCircuitOpen = true;
_circuitOpenTime = DateTime.UtcNow;
}
return _cache.TryGetValue<T>(key, out var fallback) ? fallback : default;
}
}
}缓存一致性策略
缓存与数据库一致性
// 缓存一致性是一个核心难题,常见策略对比:
// +------------------+----------+-----------+----------+
// | 策略 | 一致性 | 性能 | 复杂度 |
// +------------------+----------+-----------+----------+
// | Cache Aside | 最终一致 | 高 | 低 |
// | Write Through | 强一致 | 中 | 中 |
// | Write Behind | 最终一致 | 最高 | 高 |
// +------------------+----------+-----------+----------+
// Cache Aside(旁路缓存)— 最常用
public class CacheAsideService
{
private readonly IMemoryCache _cache;
private readonly AppDbContext _db;
private readonly ILogger _logger;
// 读:先缓存,后数据库
public async Task<User?> GetUserAsync(int userId)
{
string key = $"user:{userId}";
if (_cache.TryGetValue<User>(key, out var cached))
return cached;
var user = await _db.Users.FindAsync(userId);
if (user != null)
_cache.Set(key, user, TimeSpan.FromMinutes(30));
return user;
}
// 写:先更新数据库,再删除缓存
public async Task UpdateUserAsync(User user)
{
await _db.SaveChangesAsync();
// 删除缓存(而不是更新缓存)
_cache.Remove($"user:{user.Id}");
}
}
// Double Delete(双删策略)— 解决并发场景下的脏数据
public class DoubleDeleteCacheService
{
private readonly IMemoryCache _cache;
private readonly AppDbContext _db;
public async Task UpdateWithDoubleDeleteAsync(User user)
{
string key = $"user:{user.Id}";
// 第一次删除缓存
_cache.Remove(key);
// 更新数据库
_db.Users.Update(user);
await _db.SaveChangesAsync();
// 延迟第二次删除(防止旧读回填)
_ = Task.Run(async () =>
{
await Task.Delay(500); // 延迟大于一次主从同步时间
_cache.Remove(key);
});
}
}
// 基于 Redis Pub/Sub 的缓存失效通知
public class CacheInvalidationPublisher
{
private readonly IConnectionMultiplexer _redis;
public async Task InvalidateAsync(string cacheKey)
{
var subscriber = _redis.GetSubscriber();
await subscriber.PublishAsync("cache:invalidate", cacheKey);
}
}
public class CacheInvalidationSubscriber
{
private readonly IMemoryCache _localCache;
public CacheInvalidationSubscriber(IConnectionMultiplexer redis, IMemoryCache localCache)
{
_localCache = localCache;
var subscriber = redis.GetSubscriber();
subscriber.Subscribe("cache:invalidate", (channel, key) =>
{
_localCache.Remove(key.ToString());
});
}
}缓存 Key 设计规范
// 良好的 Key 设计是缓存系统可维护性的基础
public static class CacheKeyBuilder
{
// 基本格式:{业务域}:{实体}:{标识}
// 示例:
// user:info:12345
// product:detail:67890
// order:list:user:12345:page:1
public static string User(int userId) => $"user:info:{userId}";
public static string UserPermissions(int userId) => $"user:perms:{userId}";
public static string Product(int productId) => $"product:detail:{productId}";
public static string ProductList(int categoryId, int page, int size)
=> $"product:list:cat:{categoryId}:p:{page}:s:{size}";
// 带版本的 Key(用于批量失效)
public static string VersionedKey(string prefix, string version)
=> $"{prefix}:v:{version}";
}
// 缓存标签(批量失效)
public class TaggedCache
{
private readonly IMemoryCache _cache;
// 为缓存条目关联标签
public void SetWithTags<T>(string key, T value, TimeSpan expiration, params string[] tags)
{
// 缓存数据
_cache.Set(key, value, expiration);
// 为每个标签记录 key
foreach (var tag in tags)
{
var tagKey = $"tag:{tag}";
var keys = _cache.TryGetValue<HashSet<string>>(tagKey, out var existing)
? existing! : new HashSet<string>();
keys.Add(key);
_cache.Set(tagKey, keys, expiration);
}
}
// 按标签批量失效
public void InvalidateTag(string tag)
{
var tagKey = $"tag:{tag}";
if (_cache.TryGetValue<HashSet<string>>(tagKey, out var keys))
{
foreach (var key in keys)
_cache.Remove(key);
_cache.Remove(tagKey);
}
}
}
// 使用示例:
// taggedCache.SetWithTags("product:123", product, TimeSpan.FromMinutes(30), "products", "category:5");
// taggedCache.InvalidateTag("category:5"); // 使该分类所有产品缓存失效缓存监控与指标
// 缓存命中率监控
public class MonitoredCache
{
private readonly IMemoryCache _cache;
private long _hits;
private long _misses;
public async Task<T?> GetOrCreateAsync<T>(string key, Func<Task<T?>> factory, TimeSpan expiration)
{
if (_cache.TryGetValue<T>(key, out var value))
{
Interlocked.Increment(ref _hits);
return value;
}
Interlocked.Increment(ref _misses);
var result = await factory();
if (result != null)
_cache.Set(key, result, expiration);
return result;
}
public CacheStats GetStats()
{
var total = _hits + _misses;
return new CacheStats
{
Hits = _hits,
Misses = _misses,
HitRate = total > 0 ? (double)_hits / total : 0,
TotalRequests = total
};
}
public record CacheStats
{
public long Hits { get; init; }
public long Misses { get; init; }
public double HitRate { get; init; }
public long TotalRequests { get; init; }
}
}
// 健康阈值建议:
// 命中率 > 90%:健康
// 命中率 70-90%:需优化 Key 和过期策略
// 命中率 < 70%:可能存在穿透或 Key 设计问题优点
缺点
总结
缓存穿透:查询不存在的数据,解决用布隆过滤器(前置过滤)或缓存空值(短过期)。缓存击穿:热点 key 过期瞬间大量请求查库,解决用互斥锁(只允许一个请求重建)、永不过期+异步刷新。缓存雪崩:大量 key 同时过期,解决用随机过期时间(±20%抖动)、多级缓存(L1内存+L2 Redis)、熔断降级。分布式锁缓存重建注意使用 Lua 脚本安全释放锁。多级缓存架构确保 L1 → L2 → DB 的逐级回填。
关键知识点
- 先分清这个主题位于请求链路、后台任务链路还是基础设施链路。
- 服务端主题通常不只关心功能正确,还关心稳定性、性能和可观测性。
- 任何框架能力都要结合配置、生命周期、异常传播和外部依赖一起看。
- 缓存与开关类主题都在处理“配置/数据与运行时行为之间的解耦”。
项目落地视角
- 画清请求进入、业务执行、外部调用、日志记录和错误返回的完整路径。
- 为关键链路补齐超时、重试、熔断、追踪和结构化日志。
- 把配置与敏感信息分离,并明确不同环境的差异来源。
- 明确 Key 设计、过期策略、回源逻辑和降级方案。
常见误区
- 只会堆中间件或组件,不知道它们在链路中的执行顺序。
- 忽略生命周期和线程池、连接池等运行时资源约束。
- 没有监控和测试就对性能或可靠性下结论。
- 只加缓存,不设计失效与一致性策略。
进阶路线
- 继续向运行时行为、可观测性、发布治理和微服务协同深入。
- 把主题和数据库、缓存、消息队列、认证授权联动起来理解。
- 沉淀团队级模板,包括统一异常处理、配置约定和基础设施封装。
- 继续补齐多级缓存、缓存预热、分布式缓存治理和旗标管理平台。
适用场景
- 当你准备把《缓存穿透/击穿/雪崩实战》真正落到项目里时,最适合先在一个独立模块或最小样例里验证关键路径。
- 适合 API 服务、后台任务、实时通信、认证授权和微服务协作场景。
- 当需求开始涉及稳定性、性能、可观测性和发布流程时,这类主题会成为基础设施能力。
落地建议
- 先定义请求链路与失败路径,再决定中间件、过滤器、服务边界和依赖方式。
- 为关键链路补日志、指标、追踪、超时与重试策略。
- 环境配置与敏感信息分离,避免把生产参数写死在代码或镜像里。
排错清单
- 先确认问题发生在路由、模型绑定、中间件、业务层还是基础设施层。
- 检查 DI 生命周期、配置来源、序列化规则和认证上下文。
- 查看线程池、连接池、缓存命中率和外部依赖超时。
复盘问题
- 如果把《缓存穿透/击穿/雪崩实战》放进你的当前项目,最先要验证的输入、输出和失败路径分别是什么?
- 《缓存穿透/击穿/雪崩实战》最容易在什么规模、什么边界条件下暴露问题?你会用什么指标或日志去确认?
- 相比默认实现或替代方案,采用《缓存穿透/击穿/雪崩实战》最大的收益和代价分别是什么?
