缓存穿透、缓存雪崩、缓存击穿、缓存污染

Cache Lock 面试 大约 2872 字

缓存穿透

场景描述

缓存和数据库中都没有的数据,用户不断发起请求。如发起id=-1等不合理数据,导致数据库压力过大。

解决方法

  1. 增加传参校验,如Validation中的@NotNull@MinValue等。
  2. 从数据库中取不到数据,则在缓存层设置value=null的键值对,并设置一个过期时间,比如30秒。可防止反复请求无效数据的请求。

缓存雪崩

场景描述

缓存中数据大批量过期,不同关键字的查询都直接到数据库层,导致数据库压力过大甚至宕机。

解决方法

  1. 热点数据设置永不过期。
  2. 缓存数据过期时间分散设置,防止同一时间大量数据过期。
  3. 将热点数据均匀分布在不同Cluster上。

缓存击穿

也叫缓存踩踏。

场景描述

缓存中数据过期,从数据库中读取,由于并发用户特别多,同时读缓存都没有,请求全部打到数据库,导致数据库压力过大。

解决方法

  1. 设置热点数据永不过期。
  2. 添加互斥锁,一般并发量大都采用分布式锁。

互斥锁代码

可使用递归或自旋,等待一定时间再次获取,若超过指定阈值则返回为空。

getData()为递归实现,getData2()为自旋实现,示例代码采用Java Lock实现,真实分布式场景替换为分布式锁即可。

@Component
public class CacheUtil {

    private static ConcurrentHashMap<String, String> CACHE = new ConcurrentHashMap<>();

    private static final ReentrantLock lock = new ReentrantLock();

    public String getData(String keywords) throws InterruptedException {
        String data = getDataFromCache(keywords);
        if (data == null) {
            if (lock.tryLock()) {
                try {
                    data = getDataFromDB(keywords);
                    saveToCache(keywords, data);
                } finally {
                    lock.unlock();
                }
            } else {
                TimeUnit.SECONDS.sleep(300);
                data = getData(keywords);
            }
        }
        return data;
    }

    public String getData2(String keywords) throws InterruptedException {
        String data = getDataFromCache(keywords);
        if (data == null) {
            int count = 0;
            while (!lock.tryLock()) {
                TimeUnit.SECONDS.sleep(300);
                if (++count >= 5) {
                    return null;
                }
            }
            try {
                data = getDataFromCache(keywords);
                if (data == null) {
                    data = getDataFromDB(keywords);
                    saveToCache(keywords, data);
                }
            } finally {
                lock.unlock();
            }
        }
        return data;
    }

    private String getDataFromCache(String keywords) {
        return CACHE.get(keywords);
    }

    private String getDataFromDB(String keywords) throws InterruptedException {
        TimeUnit.SECONDS.sleep(1);
        return null;
    }

    private void saveToCache(String keywords, String data) {
        CACHE.put(keywords, data);
    }

}

自旋和 sleep 问题

自旋可能导致CPU飙升,而引入线程sleep后,可能导致惊群效应,而且也存在不同线程重复请求资源的可能。可使用FutureTask解决,下一篇具体讲FutureTask解决缓存击穿(缓存踩踏)问题。

缓存污染

场景描述

有些数据访问次数非常少,可能访问一次就再也不访问了,一直滞留在缓存中,没有应用使用,这样的key多了就可能会占据大部分的缓存空间。

解决方法

  • 方法一:设置过期时间。
  • 方法二:设置volatile-ttl缓存淘汰策略,当内存满时,选择淘汰剩余存活时间最短的key
  • 方法三:设置volatile-lru缓存淘汰策略,当内存满时,选择淘汰设置了过期时间的key中最近最少使用的key
  • 方法四:设置allkeys-lru缓存淘汰策略,当内存满时,选择淘汰最近最少使用的key(全部key参与lru淘汰)。
  • 方法五:Redis 4.0后设置volatile-lfu缓存淘汰策略,当内存满时,选择淘汰设置了过期时间的key中最不经常使用的key
  • 方法六:Redis 4.0后设置allkeys-lfu缓存淘汰策略,当内存满时,选择淘汰最不经常使用的key(全部key参与lfu淘汰)。

备注

获取热点数据,可使用Redis中的redis-cli --hotkeys命令查看。

阅读 552 · 发布于 2021-03-22

————        END        ————

扫描下方二维码关注公众号和小程序↓↓↓

扫描二维码关注我
昵称:
随便看看 换一批