缓存 “三剑客”

缓存 "三剑客" 问题

如何保证 Redis 缓存和数据库的一致性?

1. 缓存穿透

缓存穿透是指请求一个不存在的数据,缓存层和数据库层都没有这个数据,这种请求会穿透缓存直接到数据库进行查询

解决方案:

1.1 缓存空值或特殊值

查一个不存在的数据时,给一个对应的 key 数据,存入缓存

注意,这里给出的数据不能是 null 等,不然也会被缓存判断为没有。

1.2 使用布隆过滤器

1.2.1 什么是布隆过滤器?

布隆过滤器(Bloom Filter)是一种数据结构,用于快速判断一个元素是否属于一个集合中。

它使用多个 Hash 函数将一个元素映射成一个位阵列(Bit array)中的一个点,将 Bit array 理解为一个二进制数组,数组元素是 0 或 1。

当一个元素加入集合时,通过 N 个散列函数将这个元素映射到一个 Bit array 中的 N 个点,把它们设置为 1。

1.2.2 检测原理

检索某个元素是否在缓存中有时,再通过这 N 个散列函数对这个元素进行映射,根据映射找到具体位置的元素:

  1. 一定不存在:位数组对应的下标上有一个或多个是 0,直接返回
  2. 有可能存在:位数组对应的下标上每个对应值都 1,查Redis

当数据存入缓存中时,会同时存储一个 Redis 的键到布隆过滤器中。会通过布隆过滤器提供的多个 Hash 函数对 Key 进行 Hash 运算,再对位数组长度进行取余,得到一个下标,将该下标值设置为 1。

1.2.3 实现步骤

引入依赖:

xml 复制代码
<dependency>
  <groupId>com.google.guava</groupId>
  <artifactId>guava</artifactId>
  <version>28.2-jre</version>
</dependency>

测试代码:

java 复制代码
public class BloomFilterExample {
  public static void main(String[] args) {
    // 创建一个布隆过滤器,预期元素数量为1000,误判率为0.01
    BloomFilter<String> bloomFilter = BloomFilter.create(Funnels.stringFunnel(Charset.defaultCharset()), 1000, 0.01);

    // 添加元素到布隆过滤器
    bloomFilter.put("example1");
    bloomFilter.put("example2");
    bloomFilter.put("example3");

    // 测试元素是否在布隆过滤器中
    System.out.println(bloomFilter.mightContain("example1")); // true
    System.out.println(bloomFilter.mightContain("example4")); // false
  }
}

误判率是你可以调整的一个参数,较低的误判率通常需要更多的空间和计算资源

2. 缓存击穿

某一个热点数据的 key 突然过期,造成大量请求直达数据库。

解决方案:

2.1 热点数据永不过期

其实并非真正的永不过期,而是设置定时任务,等到一个访问率较低的时候,更新缓存数据(先删除缓存中的数据,再查询数据,再写入缓存),从而实现 "永不过期" 的效果。

2.2 接口限流或者降级

2.3 分布式锁

在分布式环境下,传统锁会失效,因为传统锁是基于 JVM 的

这时就要用分布式锁来实现,

解决方案:

2.3.1 redisson 方案

执行流程:

  1. 线程一尝试去获取锁,拿到锁之后(锁的有效是 30S)
  2. 在后台开启一个子线程,定时(每过 10S)去查询当前线程是否还持有锁,如果有,则给锁续命(延长锁到 30S),直到主线程执行结束,手动释放锁
  3. 线程二尝试去获取锁,拿锁失败,会进行自旋(每隔一定时间去拿锁),直到拿锁成功后去执行 2
java 复制代码
public class RedissonDistributedLockExample {
  public static void main(String[] args) {
    // 配置 Redisson 客户端
    Config config = new Config();
    // 假设Redis地址和密码
    config.useSingleServer()
      .setAddress("redis://127.0.0.1:6379")
      .setPassword("redis");
    RedissonClient redisson = Redisson.create(config);

    // 获取锁
    RLock Lock = redisson.getLock("myLock");

    try {
      // 尝试加锁,最多等待100秒,锁持有时间为10秒
      boolean isLocked = lock.tryLock(100, 10, TimeUnit.SECONDS);
      if (isLocked) {
        System.out.printLn("成功获取到锁,开始执行临界区代码");
        // 模拟临界区代码执行
        Thread.sleep(5000);
      } else {
        System.out.printLn("未能获取到锁");
      }
    } catch (InterruptedException e) {
      e.printStackTrace();
    } finally {
      // 释放锁
      if (lock.isHeldByCurrentThread()) {
        lock.unlock();
        System.out.printLn("锁已释放");
      }
    }
    // 关闭Redisson 客户端
    redisson.shutdown();
  }
}
2.3.2 Zookeeper 方案

基于临时序号节点 + 监听机制

3. 缓存雪崩

缓存雪崩是缓存中大量 key 失效后当高并发到来时导致大量请求到数据库,瞬间耗尽数据库资源,导致数据库无法使用。

解决方案:

3.1 分布式锁

参考缓存击穿中的分布式锁

3.2 热点数据永不过期

其实这个 "永不过期" 并非真正的永不过期,而是使用定时任务等技术,在服务器压力最小时,定时更新缓存,以实现 "永不过期" 的效果。

3.3 key 随机过期时间

在向 Redis 中添加缓存,设置过期时间时,多添加一个随机时间

java 复制代码
//生成随机数
int randomNum = new Random().nextInt(6000);
RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig()
  //过期时间为基础时间加随机数
  .entryTtl(Duration.ofSeconds(24 * 60 * 60L + randomNum))
  .serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(JACKSON_SERIALIZER));

return RedisCacheManager.builder(connectionFactory)
  .cacheDefaults(config)
  .transactionAware()
  .build();

以防止在同一时间内大量 key 失效。

注意:这个随机时间不建议设置太大,如果是在是需要失效的 key 太多,可以将时间单位设置位毫秒,甚至是纳秒,同样也能实现效果。

相关推荐
小王子102439 分钟前
Django缓存机制详解:从配置到实战应用
redis·缓存·django·rbac
Antonio9151 小时前
【Redis】Redis 数据存储原理和结构
数据库·redis·缓存
problc3 小时前
大模型API和秘钥获取地址
数据库·redis·缓存
Antonio9153 小时前
【Redis】Linux 配置Redis
linux·数据库·redis
Rover.x3 小时前
内存泄漏问题排查
java·linux·服务器·缓存
木宇(记得热爱生活)4 小时前
Qt GUI缓存实现
开发语言·qt·缓存
Antonio9157 小时前
【Redis】 Redis 基础命令和原理
数据库·redis·缓存
半新半旧19 小时前
python 整合使用 Redis
redis·python·bootstrap
daixin884821 小时前
什么是缓存雪崩?缓存击穿?缓存穿透?分别如何解决?什么是缓存预热?
java·开发语言·redis·缓存