缓存穿透和缓存雪崩的处理方案

缓存穿透和缓存雪崩,相信使用过缓存的同学多少都有接触过。缓存穿透,顾名思义就是请求直接穿过了缓存访问了数据库,缓存形同虚设不起作用。缓存雪崩则是某一时刻因缓存失效导致大量请求打到数据库,影响服务稳定性,甚至影响到下游的服务。

出现场景

缓存穿透出现的情况一般都是数据无中生有,正常处理缓存的逻辑是:服务器接收到用户请求,先查缓存中是否存在数据,存在直接返回,不存在则到数据库中查询数据添加数据到缓存中,同时将数据返回。反映到代码里则是:

public Product getDetailInfo(int id) {
    String keys = CACHE_KEY + id;
    Product info = (Product)redisTemplate.opsForValue().get(id);
    if (info != null){
        //滑动过期
        redisTemplate.expire(keys, 20000, TimeUnit.MILLISECONDS);
        return info;
    }
    /**
     * TODO 数据查询
     */
    if (info != null){
        //添加数据到缓存
        redisTemplate.opsForValue().set(keys, info);
        //设置过期
        redisTemplate.expire(keys, 20000, TimeUnit.MILLISECONDS);
    }
    return info;
}

这里便存在一个问题,如果用户请求的数据是数据中不存在的,那么则会绕过缓存直接查数据库,就像穿过了缓存一样。如果这时候用户一直发送请求,则会给数据库带来很大的压力甚至数据库假死,从而影响服务稳定性。

缓存雪崩则是某一个时刻,刚好有个热点数据缓存失效,刚好这个时刻访问这个热点数据的用户特别多,导致有大量的请求访问了数据库,给数据库造成了巨大的压力。

解决方案

对于缓存穿透的问题,如果是少量不存在的key可以直接缓存一个空值,如果不存在的key比较多,可采用加入一个布隆过滤器进行校验,这里采用布隆过滤器的方式。对于项目启动之前将数据预先加到布隆过滤器里,下次查询的时候则先判断布隆过滤器里数据是否存在,存在则说明数据正常,不存在则直接返回。

@Service("productService")
public class ProductServiceImpl implements ProductService{

    @Autowired
    private RedisTemplate<String, Object> redisTemplate;

    @PostConstruct
    public void init(){
        ArrayList<Product> infoList = new ArrayList<Product>();
        /**
         * TODO 查询数据库中所有数据
         */
        bloomFilter = BloomFilter.create(Funnels.stringFunnel(Charsets.UTF_8), infoList.size());
        for (Product info : infoList) {
            bloomFilter.put(CACHE_KEY + info.getId());
        }
    }

    @Override
    public Product getDetailInfo(int id) throws Exception{
        String keys = CACHE_KEY + id;
        //判断bloom过滤器中是否存在
        if(!bloomFilter.mightContain(keys)){
            return null;
        }
        //查询缓存
        Product info = (Product)redisTemplate.opsForValue().get(keys);
        if (info != null){
            //滑动过期
            redisTemplate.expire(keys,20000, TimeUnit.MILLISECONDS);
            return info;
        }
        /**
         * TODO 数据查询
         */
        if (info != null){
            //添加数据到缓存
            redisTemplate.opsForValue().set(keys, info);
            //设置过期
            redisTemplate.expire(keys,20000, TimeUnit.MILLISECONDS);
        }
        return info;
    }

    private BloomFilter<String> bloomFilter = null;
    private static final String CACHE_KEY = "PRODUCT::";
}

对于缓存雪崩,则可以基于锁实现,可以对请求的key在数据库访问阶段加锁,让同一时刻同一类型的请求只有一个能访问到数据库,后续请求则访问缓存就可获取,避免了大量请求访问数据库,这里采用ConcurrentHashMap<String, Lock>实现不同类型锁的绑定。

@Service("productService")
public class ProductServiceImpl implements ProductService{

    @Autowired
    private RedisTemplate<String, Object> redisTemplate;

    @PostConstruct
    public void init(){
        //在bean初始化完成后,实例化bloomFilter,并加载数据
        ArrayList<Product> infoList = new ArrayList<Product>();
        bloomFilter = BloomFilter.create(Funnels.stringFunnel(Charsets.UTF_8), infoList.size());// 32个
        for (Product info : infoList) {
            bloomFilter.put(CACHE_KEY + info.getId());
        }
    }

    @Override
    public Product getDetailInfo(int id) {
        String keys = CACHE_KEY + id;
        //判断bloom过滤器中是否存在
        if(!bloomFilter.mightContain(keys)){
            return null;
        }
        //查询缓存
        Product info = (Product)redisTemplate.opsForValue().get(keys);
        if (info != null){
            //滑动过期
            redisTemplate.expire(keys,20000, TimeUnit.MILLISECONDS);
            return info;
        }
        doLock(keys);
        try {
            /**
             * TODO 数据查询
             * 这里也可以在查询前再查一次缓存,不加会查多几次db,也没太大关系
             */
            if (info != null){
                //添加数据到缓存
                redisTemplate.opsForValue().set(keys, info);
                //设置过期
                redisTemplate.expire(keys,20000, TimeUnit.MILLISECONDS);
            }
        }catch (Exception e){
            LOGGER.error("info err;exp={};id={}", e.getMessage(), id);
        }finally {
            releaseLock(keys);
        }
        return info;
    }
    
    private void doLock(String keys) {
        ReentrantLock newLock = new ReentrantLock();
        //putIfAbsent 若已存在,则newLock会被直接丢弃
        Lock oldLock = locks.putIfAbsent(keys, newLock);
        if(oldLock == null){
            newLock.lock();
        }else{
            oldLock.lock();
        }
    }

    private void releaseLock(String keys) {
        ReentrantLock oldLock = (ReentrantLock)locks.get(keys);
        if(oldLock != null && oldLock.isHeldByCurrentThread()){
            oldLock.unlock();
        }
    }

    private BloomFilter<String> bloomFilter = null;
    private ConcurrentHashMap<String, Lock> locks = new ConcurrentHashMap<>();
    private static final Logger LOGGER = LoggerFactory.getLogger(ProductService.class);
    private static final String CACHE_KEY = "PRODUCT::";
}

代码在原来穿透的基础上添加了锁操作,这里keys不同才会创建锁,实现同一种类型的keys阻塞,不同类型的keys之间互不影响,能够一定程度提高系统的性能。

小结

以上便是”缓存穿透和缓存雪崩的处理方案“的所有内容,内容基于Redis缓存讲解,但思想其实也适合其他缓存应用。如果您有什么疑问或者文章有什么问题,欢迎私信或留言交流~

猜你喜欢

转载自blog.csdn.net/hsf15768615284/article/details/104593888