在高并发的应用中,Redis 缓存击穿可以导致数据库压力增大,甚至引起系统崩溃。为了解决这个问题,我们可以使用互斥锁和逻辑过期等策略
一、互斥锁方案
应用场景
- 适用于请求量大的热点数据,例如电商的热销商品详情、社交媒体的用户信息等。
- 数据频繁访问且更新,不适合使用静态缓存。
详细解决方案步骤
- 请求到来时查缓存:
- 应用程序首先尝试从 Redis 中获取数据。
- 缓存未命中时加锁:
- 使用 Redis 的
SETNX
命令尝试创建互斥锁。锁的 key 可以命名为lock:{key}
。 - 如果锁成功设置,表示获得了锁,可以继续下面的步骤;如果未成功,说明已有其他请求正在处理该数据。
- 查询数据库:
- 如果获得了锁,查询数据库并获取数据。
- 将获取到的数据写入 Redis,并设置过期时间。
- 释放锁:
- 数据写入缓存后,释放获取的锁。
- 释放锁时需确保只释放自己获取的锁,避免误删。
- 其他请求等待:
- 对于未获得锁的请求,可以设置重试机制,稍等后重试获取数据。
C# 通用缓存类示例(使用 CSRedisCore)--先安装包StackExchange.Redis
使用互斥锁示例(通用类):
使用逻辑过期方案获取数据
完整通用类代码:
public class RedisCache
{
private readonly IDatabase _cache;
private readonly IDatabase _database; // 连接到数据库的实例
public RedisCache(IDatabase cache, IDatabase database)
{
_cache = cache;
_database = database;
}
// 使用互斥锁方案获取数据
public async Task<string> GetWithMutexAsync(string key, Func<Task<string>> fetchDataFromDb, TimeSpan cacheExpiry, TimeSpan lockExpiry)
{
// 尝试从缓存获取数据
var cachedValue = await _cache.StringGetAsync(key);
if (cachedValue.HasValue)
{
return cachedValue;
}
// 创建互斥锁
string lockKey = $"lock:{key}";
string lockValue = Guid.NewGuid().ToString();
bool isLocked = await _cache.StringSetAsync(lockKey, lockValue, lockExpiry, When.NotExists);
if (isLocked)
{
try
{
// 再次确认缓存
cachedValue = await _cache.StringGetAsync(key);
if (cachedValue.HasValue)
{
return cachedValue;
}
// 从数据库获取数据
string dataFromDb = await fetchDataFromDb();
if (!string.IsNullOrEmpty(dataFromDb))
{
// 将数据存入缓存
await _cache.StringSetAsync(key, dataFromDb, cacheExpiry);
}
return dataFromDb;
}
finally
{
// 释放锁
var currentLockValue = await _cache.StringGetAsync(lockKey);
if (currentLockValue == lockValue)
{
await _cache.KeyDeleteAsync(lockKey);
}
}
}
else
{
// 当前请求未获得锁,等待一段时间后重试
await Task.Delay(50);
return await GetWithMutexAsync(key, fetchDataFromDb, cacheExpiry, lockExpiry);
}
}
// 使用逻辑过期方案获取数据
public async Task<string> GetWithLogicalExpirationAsync(string key, Func<Task<string>> fetchDataFromDb, TimeSpan cacheExpiry)
{
var cachedValueWithExpiry = await _cache.StringGetAsync(key);
if (!cachedValueWithExpiry.HasValue)
{
return null;
}
// 解析数据和过期时间
var (data, expiryTime) = ParseValue(cachedValueWithExpiry);
// 如果未过期,直接返回
if (expiryTime > DateTime.UtcNow)
{
return data;
}
// 标记正在更新
if (!await _cache.StringSetAsync($"updating:{key}", "1", TimeSpan.FromSeconds(10), When.NotExists))
{
// 其他请求正在处理,返回旧数据
return data;
}
try
{
// 查询数据库
var newData = await fetchDataFromDb();
if (!string.IsNullOrEmpty(newData))
{
// 更新缓存
await _cache.StringSetAsync(key, newData, cacheExpiry);
}
return newData;
}
finally
{
// 清理标志位
await _cache.KeyDeleteAsync($"updating:{key}");
}
}
// 解析缓存值,分离数据和过期时间
private (string data, DateTime expiryTime) ParseValue(RedisValue value)
{
var parts = value.ToString().Split('|'); // 注意分隔符
return (parts[0], DateTime.Parse(parts[1]));
}
// 用于存储带有过期时间的数据
public async Task SetValueWithExpiryAsync(string key, string value, TimeSpan cacheExpiry)
{
var expiryTime = DateTime.UtcNow.Add(cacheExpiry);
string valueWithExpiry = $"{value}|{expiryTime:u}"; // 使用 UTC 格式
await _cache.StringSetAsync(key, valueWithExpiry, cacheExpiry);
}
}
控制器或者服务使用:
private readonly IDatabase _dbCache;
private readonly IDatabase _dbDatabase;
public RedisController()
{
// 使用 Redis 连接
var redis = ConnectionMultiplexer.Connect("localhost");
_dbCache = redis.GetDatabase(); // 缓存数据库
_dbDatabase = redis.GetDatabase(); // 连接你的数据库实例
}
[HttpGet("{key}")]
public async Task<IActionResult> GetData(string key)
{
var cache = new RedisCache(_dbCache, _dbDatabase);
// 获取数据(互斥锁策略)
var result = await cache.GetWithMutexAsync(key, async () =>
{
// 这里编写从数据库获取数据的逻辑
return await _dbDatabase.StringGetAsync("your_database_key"); // 替换为正确的数据库逻辑
}, TimeSpan.FromMinutes(5), TimeSpan.FromSeconds(10));
if (result == null)
{
return NotFound(); // 如果未找到结果,返回404
}
return Ok(result); // 返回数据
}
[HttpGet("logical/{key}")]
public async Task<IActionResult> GetDataWithLogicalExpiration(string key)
{
var cache = new RedisCache(_dbCache, _dbDatabase);
// 或者使用逻辑过期策略
var resultLogical = await cache.GetWithLogicalExpirationAsync(key, async () =>
{
// 数据库获取逻辑
return await _dbDatabase.StringGetAsync("your_database_key"); // 替换为正确的数据库逻辑
}, TimeSpan.FromMinutes(5));
if (resultLogical == null)
{
return NotFound(); // 如果未找到结果,返回404
}
return Ok(resultLogical); // 返回数据
}