在高并发的应用中,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);
}
}
- 1.
- 2.
- 3.
- 4.
- 5.
- 6.
- 7.
- 8.
- 9.
- 10.
- 11.
- 12.
- 13.
- 14.
- 15.
- 16.
- 17.
- 18.
- 19.
- 20.
- 21.
- 22.
- 23.
- 24.
- 25.
- 26.
- 27.
- 28.
- 29.
- 30.
- 31.
- 32.
- 33.
- 34.
- 35.
- 36.
- 37.
- 38.
- 39.
- 40.
- 41.
- 42.
- 43.
- 44.
- 45.
- 46.
- 47.
- 48.
- 49.
- 50.
- 51.
- 52.
- 53.
- 54.
- 55.
- 56.
- 57.
- 58.
- 59.
- 60.
- 61.
- 62.
- 63.
- 64.
- 65.
- 66.
- 67.
- 68.
- 69.
- 70.
- 71.
- 72.
- 73.
- 74.
- 75.
- 76.
- 77.
- 78.
- 79.
- 80.
- 81.
- 82.
- 83.
- 84.
- 85.
- 86.
- 87.
- 88.
- 89.
- 90.
- 91.
- 92.
- 93.
- 94.
- 95.
- 96.
- 97.
- 98.
- 99.
- 100.
- 101.
- 102.
- 103.
- 104.
- 105.
- 106.
- 107.
- 108.
- 109.
- 110.
- 111.
- 112.
- 113.
- 114.
- 115.
- 116.
- 117.
- 118.
- 119.
- 120.
- 121.
- 122.
- 123.
- 124.
控制器或者服务使用:
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); // 返回数据
}
- 1.
- 2.
- 3.
- 4.
- 5.
- 6.
- 7.
- 8.
- 9.
- 10.
- 11.
- 12.
- 13.
- 14.
- 15.
- 16.
- 17.
- 18.
- 19.
- 20.
- 21.
- 22.
- 23.
- 24.
- 25.
- 26.
- 27.
- 28.
- 29.
- 30.
- 31.
- 32.
- 33.
- 34.
- 35.
- 36.
- 37.
- 38.
- 39.
- 40.
- 41.
- 42.
- 43.
- 44.
- 45.
- 46.
- 47.
- 48.
- 49.
- 50.