查询缓存击穿解决方案

查询缓存击穿解决方案

在现代分布式系统中,缓存技术是提高系统性能和降低数据库负载的重要手段。然而,当缓存中的数据过期或不可用时,可能会发生缓存击穿(Cache Breakdown)。缓存击穿会导致大量的请求直接访问数据库,从而给数据库带来较大的压力,甚至可能导致数据库崩溃。为了应对这一问题,我们需要采取一系列有效的缓存击穿解决方案。

1.什么是缓存击穿?

缓存击穿是指缓存中的某个数据在缓存过期或者被删除后,下一次请求在没有命中缓存的情况下,直接访问数据库,导致缓存失效或被清除的瞬间,数据库承受大量的并发请求。简单来说,缓存击穿是缓存不可用情况下,缓存请求集中访问数据库的现象。

示例:

  • 假设某个用户的个人信息被缓存到 Redis 中,但缓存设置了过期时间。
  • 如果在缓存过期时,突然有大量的请求同时访问该缓存,这时就会触发缓存击穿。
  • 所有请求都会绕过缓存,直接访问数据库,可能会导致数据库压力过大,响应延迟甚至崩溃。

2. 缓存击穿解决方案

2.1 预热 + 不过期

解决方案: 预热缓存是指在缓存数据过期之前提前将数据加载到缓存中。这样,在下一次请求时,即使缓存中的数据已经过期,新的数据也已被加载到缓存中,避免了缓存击穿的发生。

具体做法

  • 设置合理的缓存失效时间。
  • 在缓存即将过期时,提前通过后台任务刷新缓存。
  • 可以使用定时任务或者定时触发器在后台定期刷新缓存数据。

2.2 分布式锁之双重判定锁(旁路缓存模式)

解决方案: 双重判定锁(Double-Checked Locking)是一种减少并发访问对数据库影响的解决方案。它通过引入分布式锁,确保在缓存失效时,只有一个线程去查询数据库并更新缓存,其它线程等待缓存更新完毕后再访问缓存。旁路缓存模式是指当缓存无法查询时,先进行数据库查询,再通过分布式锁进行缓存更新。

public String selectTrain(String id) {
    // 查询缓存不存在,去数据库查询并放入到缓存
    String cacheData = cache.get(id);
    if (StrUtil.isBlank(cacheData)) {
        // 为避免大量请求同时访问数据库,通过分布式锁减少数据库访问量
        Lock lock = getLock(id);
        lock.lock();
        try {
            // 获取锁后双重判定
            cacheData = cache.get(id);
            // 理论上只有第一个请求加载数据库是有效的,因为它加载后会把数据放到缓存
            // 后面的请求再请求数据库加载缓存就没有必要了
            if (StrUtil.isBlank(cacheData)) {
                // 获取数据库中存在的数据
                String dbData = trainMapper.selectId(id);
                if (StrUtil.isNotBlank(dbData)) {
                    // 将查询到的数据放入缓存,下次查询就有数据了
                    cahce.set(id, dbData);
                    cacheData = dbData;
                }
            }
        } finally {
            lock.unlock();
        }
    }
  return cacheData;
}

3.高并发极端情况

在高并发情况下,如果请求数过多,可能会导致缓存穿透和击穿问题的恶化。有一万个请求同一时间访问触发了缓存击穿,如果用双重判定锁,逻辑是这样的:

第一个请求加锁、查询缓存是否存在、查询数据库、放入缓存、解锁,假设我们用了50毫秒;

第二个请求拿到锁查询缓存、解锁用了1毫秒;

那最后一个请求需要等待10049毫秒后才能返回,用户等待时间过长,极端情况下可能会触发应用的内存溢出。

3.1 尝试获取锁 tryLock

通过这种方式我们可以快速失败,告诉用户网络异常请稍后再试,等用户再尝试刷新的时候,其实获取锁的线程已经把数据放到了缓存。

因为这种方案对用户操作体验不友好,所以也只是适用于部分场景。在实际开发中,需要灵活变更。

public String selectTrain(String id) {
    // 查询缓存不存在,去数据库查询并放入到缓存
    String cacheData = cache.get(id);
    if (StrUtil.isBlank(cacheData)) {
        // 为避免大量请求同时访问数据库,通过分布式锁减少数据库访问量
        Lock lock = getLock(id);
        // 尝试获取锁,获取失败直接返回用户请求,并提醒用户稍后再试
        if (!lock.tryLock()) {
            throw new RuntimeException("当前访问人数过多,请稍候再试...");
        }
        try {
            // 获取数据库中存在的数据
            String dbData = trainMapper.selectId(id);
            if (StrUtil.isNotBlank(dbData)) {
                // 将查询到的数据放入缓存,下次查询就有数据了
                cahce.set(id, dbData);
                cacheData = dbData;
            }
        } finally {
            lock.unlock();
        }
    }
  return cacheData;
}
3.2 分布式锁分片

在高并发的分布式系统中,单一的分布式锁可能存在性能瓶颈,特别是在高并发请求下,所有请求都集中争夺同一个锁,可能导致阻塞和性能下降。为了避免这种情况,分布式锁分片可以通过将锁分配到多个锁空间中,使得不同请求可以同时获得不同的锁,从而减少竞争,提高系统的吞吐量。

分布式锁分片的基本原理:

分布式锁分片的核心思想是将原本的全局锁拆分为多个局部锁,每个锁对应不同的资源或数据片段。通过这种方式,可以避免所有请求都竞争同一个锁,从而提高并发性能。通常可以根据业务的特点,将数据进行分片(如通过 hashmod 操作),然后根据分片结果将请求分配到不同的锁上

3.2 分布式锁分片

在高并发的分布式系统中,单一的分布式锁可能存在性能瓶颈,特别是在高并发请求下,所有请求都集中争夺同一个锁,可能导致阻塞和性能下降。为了避免这种情况,分布式锁分片可以通过将锁分配到多个锁空间中,使得不同请求可以同时获得不同的锁,从而减少竞争,提高系统的吞吐量。

分布式锁分片的基本原理

分布式锁分片的核心思想是将原本的全局锁拆分为多个局部锁,每个锁对应不同的资源或数据片段。通过这种方式,可以避免所有请求都竞争同一个锁,从而提高并发性能。通常可以根据业务的特点,将数据进行分片(如通过 hashmod 操作),然后根据分片结果将请求分配到不同的锁上。

解决方案

  1. 将请求按某些维度分片:例如,可以按数据的 userIdproductId 等维度进行分片。每个分片拥有一个独立的锁。
  2. 分配每个请求一个分片:每个请求根据分片维度,获取一个对应的分片锁。这样,不同的请求可能会获取到不同的锁,减少锁竞争。
  3. 使用 Redis 实现分片锁:Redis 提供了 setIfAbsent 等命令,非常适合用于实现分布式锁。通过为每个分片加锁,可以实现分布式锁分片的效果。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值