Skip to main content

缓存 (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 适合分布式环境
  • 合理设置缓存过期时间
  • 注意防止缓存穿透、击穿、雪崩
  • 实现缓存预热和更新策略
  • 监控缓存命中率

相关资源