redis缓存穿透和 缓存雪崩

在使用Redis作为缓存系统时,缓存穿透(Cache Penetration) 和 缓存雪崩(Cache Avalanche) 是两种常见的问题。它们会影响缓存系统的性能和稳定性。以下是这两种问题的详细解释及其解决方法。

缓存穿透(Cache Penetration)

缓存穿透是指查询一个在缓存和数据库中都不存在的数据,导致请求直接穿透到数据库,增加了数据库的负载。

成因

  1. 恶意查询:
    • 攻击者故意查询一些在缓存和数据库中都不存在的数据。
  2. 误操作:
    • 用户或系统误操作查询了不存在的数据。
      示例
      假设有一个用户查询接口,用户ID 123456 不存在于缓存和数据库中。

      public async Task GetUserAsync(int userId)
      {
      // 尝试从缓存中获取用户
      string userJson = await _redisCache.GetStringAsync($"user:{userId}");
      if (userJson != null)
      {
      return JsonSerializer.Deserialize(userJson);
      }

      // 从数据库中获取用户
      User user = await _userRepository.GetByIdAsync(userId);
      if (user != null)
      {
      // 将用户存入缓存
      await _redisCache.SetStringAsync($"user:{userId}", JsonSerializer.Serialize(user));
      }

      return user;
      }

如果查询的用户ID 123456 不存在,每次查询都会直接穿透到数据库,增加数据库负载。

解决缓存穿透的方法

  1. 缓存空值:
    • 将查询结果为空的数据也缓存起来,设置一个较短的过期时间。
  2. 布隆过滤器(Bloom Filter):
    • 使用布隆过滤器来预先判断一个请求是否可能命中缓存,减少对数据库的无效查询。
  3. 参数校验:
    • 在查询缓存之前,对查询参数进行校验,确保参数的有效性。
  4. 限流和熔断:
    • 使用限流(Rate Limiting)和熔断(Circuit Breaking)机制来控制对数据库的请求。

示例:缓存空值

public async Task GetUserAsync(int userId)
{
// 尝试从缓存中获取用户
string userJson = await _redisCache.GetStringAsync($"user:{userId}");
if (userJson != null)
{
if (userJson == "null")
{
return null;
}
return JsonSerializer.Deserialize(userJson);
}

// 从数据库中获取用户
User user = await _userRepository.GetByIdAsync(userId);
if (user != null)
{
// 将用户存入缓存
await _redisCache.SetStringAsync($"user:{userId}", JsonSerializer.Serialize(user), TimeSpan.FromMinutes(10));
}
else
{
// 缓存空值
await _redisCache.SetStringAsync($"user:{userId}", "null", TimeSpan.FromMinutes(1));
}

return user;
}

示例:布隆过滤器

public class BloomFilterExample
{
private readonly BloomFilter _bloomFilter;
private readonly IRedisCache _redisCache;
private readonly IUserRepository _userRepository;

public BloomFilterExample(BloomFilter bloomFilter, IRedisCache redisCache, IUserRepository userRepository)
{
_bloomFilter = bloomFilter;
_redisCache = redisCache;
_userRepository = userRepository;
}

public async Task GetUserAsync(int userId)
{
// 使用布隆过滤器预先判断用户ID是否存在
if (!_bloomFilter.Contains(userId))
{
return null;
}

// 尝试从缓存中获取用户
string userJson = await _redisCache.GetStringAsync($"user:{userId}");
if (userJson != null)
{
return JsonSerializer.Deserialize(userJson);
}

// 从数据库中获取用户
User user = await _userRepository.GetByIdAsync(userId);
if (user != null)
{
// 将用户存入缓存
await _redisCache.SetStringAsync($"user:{userId}", JsonSerializer.Serialize(user), TimeSpan.FromMinutes(10));
}
else
{
// 缓存空值
await _redisCache.SetStringAsync($"user:{userId}", "null", TimeSpan.FromMinutes(1));
}

return user;
}
}

缓存雪崩(Cache Avalanche)

缓存雪崩是指在某个时间点,大量的缓存数据同时过期,导致大量请求直接穿透到数据库,增加数据库负载,甚至可能导致数据库崩溃。

成因

  1. 缓存过期时间一致:
    • 大量缓存数据设置相同的过期时间,导致同时过期。
  2. 缓存预热失败:
    • 缓存预热(Cache Warm-up)机制失败,导致缓存数据在短时间内大量过期。
  3. 系统重启或故障:
    • 系统重启或故障导致缓存数据被清除,大量请求直接穿透到数据库。

解决缓存雪崩的方法

  1. 设置不同的过期时间:
    • 将缓存数据的过期时间设置为不同的随机值,避免同时过期。
  2. 分批过期:
    • 将缓存数据分批设置不同的过期时间,减少短时间内大量数据过期的情况。
  3. 缓存预热:
    • 系统启动时或定期预热缓存,确保缓存中有足够的数据,减少缓存数据被清除后的压力。
  4. 限流和熔断:
    • 使用限流和熔断机制来控制对数据库的请求,防止数据库过载。
  5. 本地缓存:
    • 使用本地缓存(如内存缓存)来临时存储数据,减少对数据库的直接访问。
  6. 双缓存:
    • 使用两级缓存,如Redis和本地缓存,提高缓存的稳定性和可靠性。

示例:设置不同的过期时间

public async Task GetUserAsync(int userId)
{
// 尝试从缓存中获取用户
string userJson = await _redisCache.GetStringAsync($"user:{userId}");
if (userJson != null)
{
return JsonSerializer.Deserialize(userJson);
}

// 从数据库中获取用户
User user = await _userRepository.GetByIdAsync(userId);
if (user != null)
{
// 设置随机的过期时间
Random random = new Random();
int randomMinutes = random.Next(5, 15); // 随机过期时间在5到15分钟之间
await _redisCache.SetStringAsync($"user:{userId}", JsonSerializer.Serialize(user), TimeSpan.FromMinutes(randomMinutes));
}
else
{
// 缓存空值
await _redisCache.SetStringAsync($"user:{userId}", "null", TimeSpan.FromMinutes(1));
}

return user;
}

示例:缓存预热

public class CacheWarmupExample
{
private readonly IRedisCache _redisCache;
private readonly IUserRepository _userRepository;

public CacheWarmupExample(IRedisCache redisCache, IUserRepository userRepository)
{
_redisCache = redisCache;
_userRepository = userRepository;
}

public async Task WarmupCacheAsync()
{
// 获取所有用户ID
List userIds = await _userRepository.GetAllUserIdsAsync();

foreach (int userId in userIds)
{
User user = await _userRepository.GetByIdAsync(userId);
if (user != null)
{
// 设置随机的过期时间
Random random = new Random();
int randomMinutes = random.Next(5, 15); // 随机过期时间在5到15分钟之间
await _redisCache.SetStringAsync($"user:{userId}", JsonSerializer.Serialize(user), TimeSpan.FromMinutes(randomMinutes));
}
else
{
// 缓存空值
await _redisCache.SetStringAsync($"user:{userId}", "null", TimeSpan.FromMinutes(1));
}
}
}

public async Task GetUserAsync(int userId)
{
// 尝试从缓存中获取用户
string userJson = await _redisCache.GetStringAsync($"user:{userId}");
if (userJson != null)
{
if (userJson == "null")
{
return null;
}
return JsonSerializer.Deserialize(userJson);
}

// 从数据库中获取用户
User user = await _userRepository.GetByIdAsync(userId);
if (user != null)
{
// 设置随机的过期时间
Random random = new Random();
int randomMinutes = random.Next(5, 15); // 随机过期时间在5到15分钟之间
await _redisCache.SetStringAsync($"user:{userId}", JsonSerializer.Serialize(user), TimeSpan.FromMinutes(randomMinutes));
}
else
{
// 缓存空值
await _redisCache.SetStringAsync($"user:{userId}", "null", TimeSpan.FromMinutes(1));
}

return user;
}
}

总结

  1. 缓存穿透(Cache Penetration):
    成因:查询在缓存和数据库中都不存在的数据。
    解决方法:
    • 缓存空值
    • 布隆过滤器
    • 参数校验
    • 限流和熔断
  2. 缓存雪崩(Cache Avalanche):
    成因:大量缓存数据同时过期,导致大量请求直接穿透到数据库。
    解决方法:
    • 设置不同的过期时间
    • 分批过期
    • 缓存预热
    • 限流和熔断
    • 本地缓存
    • 双缓存

参考资源

posted @ 2024-12-27 15:19  似梦亦非梦  阅读(253)  评论(0)    收藏  举报