缓存穿透的解决方案

方案一: 缓存获取失败后直接访问数据库

方案

对于热点数据,如果直接访问数据库获取数据就会造成数据库压力过大,很可能直接把数据库打宕机。所以我们通常会用redis加一层缓存,就有了以下代码

public String getData(Long key) {
    // 优先从缓存获取
    String cacheKey = Constants.RedisKey.KEY_NAME + key;
    String data = redisService.getValue(cacheKey);
    if (null != data) return data;
    String dbData = yourDao.query(key);
    if (null != dbData) {
        data = dbData;
    }
    redisService.setValue(cacheKey, data);
    return data;
}

先尝试从redis获取数据,如果不存在则从数据中获取,然后将数据写入redis,最后将数据返回。

潜在问题

在海量并发的情况下,大量的请求会在拿不到redis中的数据后,访问数据库。哪怕第一个请求将数据写入缓存,别的请求也走到访问数据库的流程了,这就是缓存击穿

方案二:使用分布式锁

方案

通过使用分布式锁,获取到锁的请求才能去访问数据库

public String getData(Long key) {
    // 优先从缓存获取
    String cacheKey = Constants.RedisKey.KEY_NAME + key;
    String data = redisService.getValue(cacheKey);
    if (null != data) return data;
    RLock lock = redisService.getLock(Constants.RedisKey.KEY_NAME + key);
    lock.lock();
    try {
        String dbData = yourDao.query(key);
        if (null != dbData) {
            data = dbData;
        }
        redisService.setValue(cacheKey, data);
    } finally {
        lock.unlock();
    }
    return data;
}

潜在问题

虽然加了锁,但是每一个请求还是会在等到锁之后去访问数据库。这样造成了严重的性能问题:多余的查表操作,和请求之间的锁竞争。第一个请求将数据写入缓存之后,后续请求应该从缓存中查询,没必要查表

方案三:双重检查锁

方案

使用单例模式双重检查锁的思想,两次判断确定数据确实为空后再去查表

public String getData(Long key) {
    // 优先从缓存获取
    String cacheKey = Constants.RedisKey.KEY_NAME + key;
    String data = redisService.getValue(cacheKey);
    if (null != data) return data;
    RLock lock = redisService.getLock(Constants.RedisKey.KEY_NAME + key);
    lock.lock();
    try {
        // 双重检查
        data = redisService.getValue(cacheKey);
        if (null != data) return data;
        String dbData = yourDao.query(key);
        if (null != dbData) {
            data = dbData;
        }
        redisService.setValue(cacheKey, data);
        return data;
    } finally {
        lock.unlock();
    }
}

第一个请求将数据写入缓存后,之后的请求可以直接从缓存中读取数据

潜在问题

虽然避免了多余的查表操作,但是任然后很多请求会遇到锁竞争的问题,只有等之前的请求将锁释放之后才能执行后续流程。最后一个请求的接口响应时间会非常长

方案四:tryLock减少锁竞争

方案

设置锁的等待时间,超过等待时间后,继续执行后续流程

public String getData(Long key) {
    // 优先从缓存获取
    String cacheKey = Constants.RedisKey.KEY_NAME + key;
    String data = redisService.getValue(cacheKey);
    if (null != data) return data;
    RLock lock = redisService.getLock(Constants.RedisKey.KEY_NAME + key);
    boolean lockResult = false;
    try {
        // 等待1秒,超过等待时间后继续执行
        lockResult = lock.tryLock(1, TimeUnit.SECONDS);
    } catch (InterruptedException e) {
        // 处理异常 线程中断异常
    }
    try {
        // 双重检查
        data = redisService.getValue(cacheKey);
        if (null != data) return data;
        String dbData = yourDao.query(key);
        if (null != dbData) {
            data = dbData;
        }
        redisService.setValue(cacheKey, data);
        return data;
    } finally {
        if (lockResult) {
            lock.unlock();
        }
    }
}

第一个请求获得了锁,将数据从数据库写入缓存。其余的请求等待1秒,如果超过1秒就不再竞争锁,继续执行。因为第一个请求已经写数据到缓存,所以可以直接读缓存返回。

潜在问题

如果第一个请求执行的时间超过了别的请求的等待时间,没来得及写入数据到redis,别的请求就会继续访问数据库。所以需要根据业务来决定合适的等待锁时间。这个方案牺牲了绝对安全来优化锁竞争的性能问题。设计方案时需要根据业务进行评估,决定应该使用哪一个方案。

posted @ 2025-07-11 17:12  庚申码上仙  阅读(13)  评论(0)    收藏  举报