在高并发的应用中,Redis 缓存击穿可以导致数据库压力增大,甚至引起系统崩溃。为了解决这个问题,我们可以使用互斥锁和逻辑过期等策略

一、互斥锁方案

应用场景

  • 适用于请求量大的热点数据,例如电商的热销商品详情、社交媒体的用户信息等。
  • 数据频繁访问且更新,不适合使用静态缓存。

详细解决方案步骤

  1. 请求到来时查缓存
  • 应用程序首先尝试从 Redis 中获取数据。
  1. 缓存未命中时加锁
  • 使用 Redis 的 SETNX 命令尝试创建互斥锁。锁的 key 可以命名为 lock:{key}
  • 如果锁成功设置,表示获得了锁,可以继续下面的步骤;如果未成功,说明已有其他请求正在处理该数据。
  1. 查询数据库
  • 如果获得了锁,查询数据库并获取数据。
  • 将获取到的数据写入 Redis,并设置过期时间。
  1. 释放锁
  • 数据写入缓存后,释放获取的锁。
  • 释放锁时需确保只释放自己获取的锁,避免误删。
  1. 其他请求等待
  • 对于未获得锁的请求,可以设置重试机制,稍等后重试获取数据。

C# 通用缓存类示例(使用 CSRedisCore)--先安装包StackExchange.Redis

使用互斥锁示例(通用类):

互斥锁或逻辑过期方案解决redis缓存击穿_逻辑过期


使用逻辑过期方案获取数据

互斥锁或逻辑过期方案解决redis缓存击穿_Redis互斥锁_02

完整通用类代码:
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);
    }
}

控制器或者服务使用:

互斥锁或逻辑过期方案解决redis缓存击穿_缓存_03

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); // 返回数据
}