做Web开发的朋友应该都遇到过这样的场景:数据库查询越来越慢,接口响应时间越来越长,服务器压力越来越大。特别是在电商大促期间,商品详情页的访问量暴增,数据库扛不住压力直接宕机。这时候缓存就像救命稻草一样出现了。
我经历过一个真实案例:某电商平台的商品详情页在双11期间每秒请求量超过1万次,如果每次都去查数据库,MySQL服务器直接崩溃。后来我们引入了内存缓存,把热点商品信息缓存起来,数据库压力瞬间下降80%,页面加载时间从2秒降到200毫秒。
缓存的核心价值在于三点:
在ASP.NET Core中使用内存缓存非常简单,IMemoryCache已经内置在DI容器中。我们来看个电商商品缓存的例子:
csharp复制public class ProductController : Controller
{
private readonly IMemoryCache _cache;
private readonly AppDbContext _db;
public ProductController(IMemoryCache cache, AppDbContext db)
{
_cache = cache;
_db = db;
}
[HttpGet("/products/{id}")]
public async Task<IActionResult> GetProduct(int id)
{
// 尝试从缓存获取
if (_cache.TryGetValue($"product_{id}", out Product product))
{
return Ok(product);
}
// 缓存不存在则查询数据库
product = await _db.Products.FindAsync(id);
if (product == null) return NotFound();
// 存入缓存,设置1小时绝对过期
_cache.Set($"product_{id}", product, TimeSpan.FromHours(1));
return Ok(product);
}
}
上面的代码可以简化为更优雅的GetOrCreate形式:
csharp复制public async Task<IActionResult> GetProduct(int id)
{
var product = await _cache.GetOrCreateAsync($"product_{id}", async entry =>
{
entry.AbsoluteExpirationRelativeToNow = TimeSpan.FromHours(1);
return await _db.Products.FindAsync(id);
});
return product != null ? Ok(product) : NotFound();
}
这种方法会自动处理"先查缓存,不存在再查库"的逻辑,代码更简洁。我在实际项目中发现,使用GetOrCreate可以减少30%的样板代码。
不同类型的商品适合不同的过期策略:
csharp复制// 爆款商品:滑动过期+绝对过期双保险
var hotProductOptions = new MemoryCacheEntryOptions()
.SetSlidingExpiration(TimeSpan.FromMinutes(5)) // 5分钟内有人访问就续期
.SetAbsoluteExpiration(TimeSpan.FromHours(1)); // 最多缓存1小时
// 普通商品:仅绝对过期
var normalProductOptions = new MemoryCacheEntryOptions()
.SetAbsoluteExpiration(TimeSpan.FromHours(6));
// 秒杀商品:短时间绝对过期
var flashSaleOptions = new MemoryCacheEntryOptions()
.SetAbsoluteExpiration(TimeSpan.FromSeconds(30));
当内存不足时,系统会根据优先级清理缓存。电商场景可以这样设置:
csharp复制var options = new MemoryCacheEntryOptions()
.SetPriority(CacheItemPriority.High) // 高优先级商品
.SetSize(1); // 设置相对大小
_cache.Set($"product_{id}", product, options);
优先级建议:
所有商品设置相同过期时间会导致缓存同时失效,数据库压力骤增。解决方案是添加随机抖动:
csharp复制var random = new Random();
var baseExpiration = TimeSpan.FromMinutes(30);
var actualExpiration = baseExpiration.Add(TimeSpan.FromSeconds(random.Next(0, 300)));
_cache.Set(key, value, actualExpiration);
恶意请求不存在的商品ID会导致大量数据库查询。解决方案:
csharp复制public async Task<Product> GetProduct(int id)
{
if (!_cache.TryGetValue(key, out Product product))
{
product = await _db.Products.FindAsync(id);
// 空值也缓存,但设置较短过期时间
_cache.Set(key, product ?? (object)string.Empty,
TimeSpan.FromMinutes(5));
}
return product == null && _cache.Get(key) is string ? null : product;
}
商品信息变更时如何更新缓存?常见方案:
csharp复制public async Task UpdateProduct(Product product)
{
_db.Products.Update(product);
await _db.SaveChangesAsync();
// 立即更新缓存
_cache.Set($"product_{product.Id}", product);
}
被动淘汰:设置较短的过期时间,等待自然失效
消息队列:通过事件通知各服务更新缓存
添加中间件记录缓存统计:
csharp复制app.Use(async (context, next) =>
{
var cache = context.RequestServices.GetRequiredService<IMemoryCache>();
var watch = Stopwatch.StartNew();
await next();
watch.Stop();
var cacheStats = new
{
TotalRequests = Interlocked.Increment(ref _totalRequests),
CacheHits = Interlocked.Increment(ref _cacheHits),
Duration = watch.ElapsedMilliseconds
};
// 可以输出到日志或监控系统
});
在Startup中配置全局缓存限制:
csharp复制services.AddMemoryCache(options =>
{
options.SizeLimit = 1024 * 1024; // 设置总大小
options.CompactionPercentage = 0.2; // 内存不足时清理20%
});
使用时指定条目大小:
csharp复制_cache.Set(key, value, new MemoryCacheEntryOptions
{
Size = GetSizeInBytes(value), // 需要自己实现大小计算
SlidingExpiration = TimeSpan.FromMinutes(30)
});
当单机内存不够或需要多实例共享缓存时,可以考虑Redis等分布式缓存。IMemoryCache和IDistributedCache可以组合使用:
csharp复制// 先查本地内存缓存
if (_memoryCache.TryGetValue(key, out Product product))
{
return product;
}
// 再查分布式缓存
var bytes = await _distributedCache.GetAsync(key);
if (bytes != null)
{
product = JsonSerializer.Deserialize<Product>(bytes);
// 回填本地缓存
_memoryCache.Set(key, product, TimeSpan.FromMinutes(5));
return product;
}
// 最后查数据库
product = await _db.Products.FindAsync(id);
if (product != null)
{
// 同时存入两级缓存
await _distributedCache.SetAsync(key,
JsonSerializer.SerializeToUtf8Bytes(product),
new DistributedCacheEntryOptions { AbsoluteExpiration = DateTimeOffset.Now.AddHours(1) });
_memoryCache.Set(key, product, TimeSpan.FromMinutes(5));
}
这种多级缓存架构既能保证速度,又能确保数据一致性。我在一个日活百万的电商系统中采用这种方案,QPS提升了3倍。