在高并发的应用中,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);
    }
}
  • 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.

控制器或者服务使用:

互斥锁或逻辑过期方案解决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); // 返回数据
}
  • 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.