缓存 (Redis & IMemoryCache)
为什么需要缓存?
缓存的价值
缓存可以显著提升应用性能,减少数据库查询,降低响应时间,提高系统吞吐量。
⚡ 提升性能 - 减少数据库查询和计算
💰 降低成本 - 减少服务器资源消耗
📈 提高并发 - 支持更多用户同时访问
IMemoryCache (内存缓存)
基础使用
Program.cs
// 注册内存缓存服务
builder.Services.AddMemoryCache();
var app = builder.Build();
UserService.cs
public class UserService
{
private readonly IMemoryCache _cache;
private readonly IUserRepository _repository;
public UserService(IMemoryCache cache, IUserRepository repository)
{
_cache = cache;
_repository = repository;
}
public async Task<User> GetUserAsync(int userId)
{
// 尝试从缓存获取
if (_cache.TryGetValue($"user:{userId}", out User cachedUser))
{
return cachedUser;
}
// 缓存未命中,从数据库查询
var user = await _repository.GetByIdAsync(userId);
if (user != null)
{
// 设置缓存,10分钟过期
_cache.Set($"user:{userId}", user, TimeSpan.FromMinutes(10));
}
return user;
}
}
缓存选项
public async Task<Product> GetProductAsync(int productId)
{
return await _cache.GetOrCreateAsync($"product:{productId}", async entry =>
{
// 设置缓存选项
entry.AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(30); // 绝对过期时间
entry.SlidingExpiration = TimeSpan.FromMinutes(10); // 滑动过期时间
entry.Priority = CacheItemPriority.Normal; // 缓存优先级
// 设置缓存依赖
entry.RegisterPostEvictionCallback((key, value, reason, state) =>
{
Console.WriteLine($"缓存被移除: {key}, 原因: {reason}");
});
// 从数据库查询
return await _repository.GetProductByIdAsync(productId);
});
}
缓存过期策略
策略 | 说明 | 使用场景 |
---|---|---|
绝对过期 | 固定时间后过期 | 数据有明确的有效期 |
滑动过期 | 一段时间内无访问则过期 | 热点数据保持缓存 |
组合过期 | 两者结合 | 既要保持热点,又要定期刷新 |
// 绝对过期:30分钟后必定过期
entry.AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(30);
// 滑动过期:10分钟内无访问则过期
entry.SlidingExpiration = TimeSpan.FromMinutes(10);
// 组合:最长30分钟,期间10分钟无访问也过期
entry.AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(30);
entry.SlidingExpiration = TimeSpan.FromMinutes(10);
Redis 分布式缓存
安装和配置
# 安装 Redis 客户端
dotnet add package StackExchange.Redis
dotnet add package Microsoft.Extensions.Caching.StackExchangeRedis
Program.cs
// 配置 Redis
builder.Services.AddStackExchangeRedisCache(options =>
{
options.Configuration = "localhost:6379";
options.InstanceName = "MyApp:";
});
// 或使用 IDistributedCache 抽象
builder.Services.AddDistributedMemoryCache(); // 开发环境
// builder.Services.AddStackExchangeRedisCache(...); // 生产环境
使用 IDistributedCache
public class ProductService
{
private readonly IDistributedCache _cache;
private readonly IProductRepository _repository;
public ProductService(IDistributedCache cache, IProductRepository repository)
{
_cache = cache;
_repository = repository;
}
public async Task<Product> GetProductAsync(int productId)
{
var cacheKey = $"product:{productId}";
// 尝试从缓存获取
var cachedData = await _cache.GetStringAsync(cacheKey);
if (!string.IsNullOrEmpty(cachedData))
{
return JsonSerializer.Deserialize<Product>(cachedData);
}
// 缓存未命中
var product = await _repository.GetByIdAsync(productId);
if (product != null)
{
// 序列化并存入缓存
var options = new DistributedCacheEntryOptions
{
AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(30),
SlidingExpiration = TimeSpan.FromMinutes(10)
};
await _cache.SetStringAsync(
cacheKey,
JsonSerializer.Serialize(product),
options
);
}
return product;
}
public async Task RemoveProductAsync(int productId)
{
await _cache.RemoveAsync($"product:{productId}");
}
}
使用 StackExchange.Redis (直接)
public class RedisService
{
private readonly IConnectionMultiplexer _redis;
private readonly IDatabase _db;
public RedisService(IConnectionMultiplexer redis)
{
_redis = redis;
_db = redis.GetDatabase();
}
// 字符串操作
public async Task<string> GetAsync(string key)
{
return await _db.StringGetAsync(key);
}
public async Task SetAsync(string key, string value, TimeSpan? expiry = null)
{
await _db.StringSetAsync(key, value, expiry);
}
// Hash 操作
public async Task<Dictionary<string, string>> GetHashAsync(string key)
{
var entries = await _db.HashGetAllAsync(key);
return entries.ToDictionary(
x => x.Name.ToString(),
x => x.Value.ToString()
);
}
public async Task SetHashAsync(string key, Dictionary<string, string> values)
{
var entries = values.Select(kv =>
new HashEntry(kv.Key, kv.Value)
).ToArray();
await _db.HashSetAsync(key, entries);
}
// List 操作
public async Task<long> PushListAsync(string key, string value)
{
return await _db.ListRightPushAsync(key, value);
}
public async Task<string> PopListAsync(string key)
{
return await _db.ListLeftPopAsync(key);
}
// Set 操作
public async Task AddToSetAsync(string key, string value)
{
await _db.SetAddAsync(key, value);
}
public async Task<string[]> GetSetMembersAsync(string key)
{
var values = await _db.SetMembersAsync(key);
return values.Select(v => v.ToString()).ToArray();
}
// Sorted Set 操作
public async Task AddToSortedSetAsync(string key, string value, double score)
{
await _db.SortedSetAddAsync(key, value, score);
}
public async Task<string[]> GetTopFromSortedSetAsync(string key, int count)
{
var values = await _db.SortedSetRangeByRankAsync(key, 0, count - 1, Order.Descending);
return values.Select(v => v.ToString()).ToArray();
}
// 发布订阅
public async Task PublishAsync(string channel, string message)
{
var subscriber = _redis.GetSubscriber();
await subscriber.PublishAsync(channel, message);
}
public async Task SubscribeAsync(string channel, Action<string> handler)
{
var subscriber = _redis.GetSubscriber();
await subscriber.SubscribeAsync(channel, (ch, message) =>
{
handler(message);
});
}
}
缓存策略
Cache-Aside (旁路缓存)
public async Task<User> GetUserAsync(int userId)
{
var cacheKey = $"user:{userId}";
// 1. 先查缓存
var cached = await _cache.GetStringAsync(cacheKey);
if (cached != null)
{
return JsonSerializer.Deserialize<User>(cached);
}
// 2. 缓存未命中,查数据库
var user = await _repository.GetByIdAsync(userId);
// 3. 写入缓存
if (user != null)
{
await _cache.SetStringAsync(
cacheKey,
JsonSerializer.Serialize(user),
new DistributedCacheEntryOptions
{
AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(10)
}
);
}
return user;
}
Read-Through (读穿透)
public class CachingUserRepository : IUserRepository
{
private readonly IUserRepository _repository;
private readonly IDistributedCache _cache;
public CachingUserRepository(IUserRepository repository, IDistributedCache cache)
{
_repository = repository;
_cache = cache;
}
public async Task<User> GetByIdAsync(int userId)
{
var cacheKey = $"user:{userId}";
// 尝试从缓存读取
var cached = await _cache.GetStringAsync(cacheKey);
if (cached != null)
{
return JsonSerializer.Deserialize<User>(cached);
}
// 从数据库读取并自动写入缓存
var user = await _repository.GetByIdAsync(userId);
if (user != null)
{
await _cache.SetStringAsync(cacheKey, JsonSerializer.Serialize(user));
}
return user;
}
}
Write-Through (写穿透)
public async Task UpdateUserAsync(User user)
{
// 1. 更新数据库
await _repository.UpdateAsync(user);
// 2. 同时更新缓存
var cacheKey = $"user:{user.Id}";
await _cache.SetStringAsync(
cacheKey,
JsonSerializer.Serialize(user),
new DistributedCacheEntryOptions
{
AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(10)
}
);
}
Write-Behind (异步写)
public async Task UpdateUserAsync(User user)
{
// 1. 先更新缓存
var cacheKey = $"user:{user.Id}";
await _cache.SetStringAsync(cacheKey, JsonSerializer.Serialize(user));
// 2. 异步更新数据库
_ = Task.Run(async () =>
{
try
{
await _repository.UpdateAsync(user);
}
catch (Exception ex)
{
_logger.LogError(ex, "异步更新数据库失败");
}
});
}
实战案例
案例1:缓存热点数据
public class ProductService
{
private const string HOT_PRODUCTS_KEY = "products:hot";
public async Task<List<Product>> GetHotProductsAsync()
{
// 尝试从缓存获取
var cached = await _cache.GetStringAsync(HOT_PRODUCTS_KEY);
if (cached != null)
{
return JsonSerializer.Deserialize<List<Product>>(cached);
}
// 从数据库查询
var products = await _repository.GetHotProductsAsync();
// 缓存1小时
await _cache.SetStringAsync(
HOT_PRODUCTS_KEY,
JsonSerializer.Serialize(products),
new DistributedCacheEntryOptions
{
AbsoluteExpirationRelativeToNow = TimeSpan.FromHours(1)
}
);
return products;
}
}
案例2:防止缓存击穿
private static readonly SemaphoreSlim _semaphore = new(1, 1);
public async Task<Product> GetProductAsync(int productId)
{
var cacheKey = $"product:{productId}";
// 先尝试从缓存获取
var cached = await _cache.GetStringAsync(cacheKey);
if (cached != null)
{
return JsonSerializer.Deserialize<Product>(cached);
}
// 使用信号量防止并发查询
await _semaphore.WaitAsync();
try
{
// 双重检查
cached = await _cache.GetStringAsync(cacheKey);
if (cached != null)
{
return JsonSerializer.Deserialize<Product>(cached);
}
// 查询数据库
var product = await _repository.GetByIdAsync(productId);
if (product != null)
{
await _cache.SetStringAsync(cacheKey, JsonSerializer.Serialize(product));
}
return product;
}
finally
{
_semaphore.Release();
}
}
案例3:缓存空对象(防止缓存穿透)
public async Task<Product> GetProductAsync(int productId)
{
var cacheKey = $"product:{productId}";
var cached = await _cache.GetStringAsync(cacheKey);
if (cached != null)
{
// 空字符串表示数据不存在
if (cached == "null")
{
return null;
}
return JsonSerializer.Deserialize<Product>(cached);
}
var product = await _repository.GetByIdAsync(productId);
if (product != null)
{
await _cache.SetStringAsync(cacheKey, JsonSerializer.Serialize(product));
}
else
{
// 缓存空对象,但过期时间设置得短一些
await _cache.SetStringAsync(
cacheKey,
"null",
new DistributedCacheEntryOptions
{
AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(5)
}
);
}
return product;
}
案例4:实现分布式锁
public class DistributedLock
{
private readonly IDatabase _db;
public async Task<bool> AcquireLockAsync(string key, string value, TimeSpan expiry)
{
return await _db.StringSetAsync(key, value, expiry, When.NotExists);
}
public async Task<bool> ReleaseLockAsync(string key, string value)
{
var script = @"
if redis.call('get', KEYS[1]) == ARGV[1] then
return redis.call('del', KEYS[1])
else
return 0
end";
var result = await _db.ScriptEvaluateAsync(script, new RedisKey[] { key }, new RedisValue[] { value });
return (int)result == 1;
}
}
// 使用
var lockKey = $"lock:order:{orderId}";
var lockValue = Guid.NewGuid().ToString();
if (await _lock.AcquireLockAsync(lockKey, lockValue, TimeSpan.FromSeconds(10)))
{
try
{
// 执行业务逻辑
await ProcessOrderAsync(orderId);
}
finally
{
await _lock.ReleaseLockAsync(lockKey, lockValue);
}
}
最佳实践
缓存最佳实践
1. 合理设置过期时间
// ✅ 好:根据数据特性设置过期时间
await _cache.SetStringAsync("config", data, TimeSpan.FromHours(1)); // 配置数据
await _cache.SetStringAsync("hot-products", data, TimeSpan.FromMinutes(30)); // 热点数据
await _cache.SetStringAsync("user-session", data, TimeSpan.FromMinutes(20)); // 用户会话
2. 使用命名空间
// ✅ 好:使用前缀区分不同类型的缓存
var userKey = $"user:{userId}";
var productKey = $"product:{productId}";
var orderKey = $"order:{orderId}";
3. 缓存预热
public async Task WarmUpCacheAsync()
{
// 应用启动时预加载热点数据
var hotProducts = await _repository.GetHotProductsAsync();
foreach (var product in hotProducts)
{
await _cache.SetStringAsync(
$"product:{product.Id}",
JsonSerializer.Serialize(product),
TimeSpan.FromHours(1)
);
}
}
4. 缓存更新策略
public async Task UpdateProductAsync(Product product)
{
// 先更新数据库
await _repository.UpdateAsync(product);
// 再删除缓存(而不是更新缓存)
await _cache.RemoveAsync($"product:{product.Id}");
// 下次查询时会重新加载最新数据
}
5. 监控缓存命中率
public class CacheMetrics
{
private long _hits;
private long _misses;
public void RecordHit() => Interlocked.Increment(ref _hits);
public void RecordMiss() => Interlocked.Increment(ref _misses);
public double HitRate => _hits + _misses == 0 ? 0 : (double)_hits / (_hits + _misses);
}
总结
关键要点
- IMemoryCache 适合单机应用
- Redis 适合分布式环境
- 合理设置缓存过期时间
- 注意防止缓存穿透、击穿、雪崩
- 实现缓存预热和更新策略
- 监控缓存命中率