Redis 缓存穿透、缓存击穿和缓存雪崩问题

一、基本概念

1、缓存穿透

缓存穿透:指缓存和数据库中都没有用户需要查询的数据(即根本不存在的数据),而用户不断发起请求,那么缓存层和存储层都不会命中,从而导致数据库压力过大。

缓存穿透将导致每次请求根本不存在的数据都要到存储层去查询,失去了缓存层保护后端存储层的意义。可能会使后端存储负载加大,由于很多后端存储不具备高并发性,甚至可能造成后端存储宕掉。

1.1 造成原因

  1. 自身业务代码或者数据出现问题
    比如,我们数据库的 id 都是 1 开始自增上去的,请求id=-1,如果不做校验。
  2. 一些恶意攻击、爬虫等造成大量空命中。

1.2 解决方案

  1. 接口层增加校验
    比如:用户鉴权校验,id做基础校验,id<=0的直接拦截;
  2. 缓存空对象
    当存储层不命中后,仍然将空对象保留到缓存层中(一般设置一个较短的过期时间,让其自动剔除),之后再访问这个数据将会从缓存中获取,这样就保护了后端数据源。

2、缓存击穿

缓存击穿:指缓存中没有但是数据库中有的数据(一般是缓存时间到期),这时持续的大并发请求同时来读缓存没读到数据,所以又直接同时去数据库查数据,从而引起数据库压力瞬间增大。

缓存击穿可能会使后端存储负载加大,甚至可能造成后端存储宕掉。

2.1 解决方案

比较常用的方案就是 使用互斥锁

3、缓存雪崩

缓存雪崩的英文原意是 stampeding herd(奔逃的野牛),指的是缓存层宕掉后,流量会像奔逃的野牛一样,打向后端存储。

缓存雪崩:指同一时间缓存中数据大批量到了过期时间,而查询请求量巨大,于是所有的请求都会达到存储层,存储层调用量会暴增,从而引起数据库压力过大甚至级联宕机的情况。

3.1 解决方案

  1. 保证缓存层服务高可用性。比如:搭建 Redis集群架构
  2. 依赖隔离组件为后端限流并降级。
  3. 将缓存失效时间分散开(在原有的失效时间基础上增加一 个随机值)。
  4. 提前演练。

4、三者的区别

缓存穿透、缓存击穿和缓存雪崩的区别:

  • 缓存穿透针对的是根本不存在的数据,穿透缓冲层和存储层,访问存储层压力加大。
  • 缓存击穿针对的是某一个 key 的缓存失效,击穿缓冲层,访问存储层压力加大。
  • 缓存雪崩针对的是同一时刻大批量 key 的缓存失效,击穿缓冲层,访问存储层压力压力过大甚至级联宕机的情况。

二、实战代码

针对上面问题,会采取不同的解决方案,这里提供一种,在开发过程中可作参考。

以商品为例,RedisUtil是简单封装的 Redis 操作方法工具类。

@Service
public class ProductService {

    @Autowired
    private ProductDao productDao;

    @Autowired
    private RedisUtil redisUtil;

    @Autowired
    private RedissonClient redissonClient;

    /**
     * 商品缓存生存时间
     */
    public static final Integer PRODUCT_CACHE_TIMEOUT = 60 * 60 * 24;
    /**
     * 空对象:解决缓存穿透问题
     */
    public static final String EMPTY_CACHE = "{}";
    /**
     * 商品查询分布式锁Key:解决热点缓存并发重建问题
     */
    public static final String LOCK_PRODUCT_HOT_CACHE_CREATE_PREFIX = "lock:product:hot_cache_create:";
    /**
     * 商品修改分布式读写锁Key:解决缓存双写不一致问题
     */
    public static final String LOCK_PRODUCT_UPDATE_PREFIX = "lock:product:update:";

    @Transactional
    public Product create(Product product) {
        //1.入库
        Product productResult = productDao.create(product);
        //2.更新缓存
        redisUtil.set(RedisKeyPrefixConst.PRODUCT_CACHE + productResult.getId(), JSON.toJSONString(productResult));
        return productResult;
    }

    @Transactional
    public Product update(Product product) {
        Product productResult = null;
        
        RReadWriteLock productUpdateLock = redissonClient.getReadWriteLock(LOCK_PRODUCT_UPDATE_PREFIX + product.getId());
        RLock writeLock = productUpdateLock.writeLock();
        //1.加分布式读写锁(写锁):解决缓存双写不一致问题
        writeLock.lock();
        try {
            //2.更新数据库
            productResult = productDao.update(product);
            //3.更新缓存
            redisUtil.set(RedisKeyPrefixConst.PRODUCT_CACHE + productResult.getId(), JSON.toJSONString(productResult),
                    genProductCacheTimeout(), TimeUnit.SECONDS);
        } finally {
            //1.解分布式读写锁(写锁)
            writeLock.unlock();
        }
        return productResult;
    }

    public Product get(Long productId) {
        Product product = null;
        String productCacheKey = RedisKeyPrefixConst.PRODUCT_CACHE + productId;

        //1.从缓存里查数据
        product = getProductFromCache(productCacheKey);
        if (product != null) {
            return product;
        }

        //2.加分布式锁:解决热点缓存并发重建问题
        RLock hotCreateCacheLock = redissonClient.getLock(LOCK_PRODUCT_HOT_CACHE_CREATE_PREFIX + productId);
        hotCreateCacheLock.lock();
        // 这个优化谨慎使用
        // hotCreateCacheLock.tryLock(1, TimeUnit.SECONDS);
        try {
            //再次从缓存里查数据
            product = getProductFromCache(productCacheKey);
            if (product != null) {
                return product;
            }

            //3.加分布式读写锁(读锁):解决缓存双写不一致问题
            RReadWriteLock productUpdateLock = redissonClient.getReadWriteLock(LOCK_PRODUCT_UPDATE_PREFIX + productId);
            RLock rLock = productUpdateLock.readLock();
            rLock.lock();
            try {
                //4.查询数据库
                product = productDao.get(productId);
                if (product != null) {//如果存在,记录放到缓存
                    redisUtil.set(productCacheKey, JSON.toJSONString(product),
                            genProductCacheTimeout(), TimeUnit.SECONDS);
                } else {//如果不存在
                    //设置空缓存:解决缓存穿透问题
                    redisUtil.set(productCacheKey, EMPTY_CACHE, genEmptyCacheTimeout(), TimeUnit.SECONDS);
                }
            } finally {
                //解分布式读写锁(读锁)
                rLock.unlock();
            }
        } finally {
            //解分布式锁
            hotCreateCacheLock.unlock();
        }
        return product;
    }

    /**
     * 将缓存失效时间分散开(在原有的失效时间基础上增加一 个随机值)
     */
    private Integer genProductCacheTimeout() {       
        return PRODUCT_CACHE_TIMEOUT + new Random().nextInt(5) * 60 * 60;
    }
    private Integer genEmptyCacheTimeout() {
    	//空对象,时间短一些
        return 60 + new Random().nextInt(30);
    }

    private Product getProductFromCache(String productCacheKey) {
        Product product = null;        
        //1.查询缓存
        String productStr = redisUtil.get(productCacheKey);
        if (!StringUtils.isEmpty(productStr)) {
            // 2.判断是不是空对象
            if (EMPTY_CACHE.equals(productStr)) {
                // 如果是空对象,key续期
                redisUtil.expire(productCacheKey, genEmptyCacheTimeout(), TimeUnit.SECONDS);
                return new Product(); //返回空实例(注意和null的区别),和前端约定好,id=null为空对象。
            }
            //3. 如果不是空对象,返回缓存数据,并将 key续期
            product = JSON.parseObject(productStr, Product.class);
            //缓存读延期
            redisUtil.expire(productCacheKey, genProductCacheTimeout(), TimeUnit.SECONDS);
        }
        return product;
    }

}

– 求知若饥,虚心若愚。

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
缓存穿透缓存击穿缓存雪崩是常见的缓存相关问题,它们可能导致缓存失效或性能下降。下面是对它们的原因和解决方法的简要说明: 1. 缓存穿透缓存穿透是指请求的数据在缓存和数据库中都不存在,导致每次请求都要访问数据库,增加了数据库负载。主要原因是恶意攻击或错误的查询。 解决方法: - 使用布隆过滤器:在查询前使用布隆过滤器检查请求是否有效,如果无效则直接返回,避免对数据库的查询。 - 设置空对象缓存:将数据库中不存在的值也缓存起来,可以防止频繁查询。 2. 缓存击穿缓存击穿是指一个热点数据失效,导致大量请求同时访问数据库,造成数据库压力过大。主要原因是热点数据过期或删除。 解决方法: - 设置热点数据永不过期:针对热点数据设置永不过期,确保即使失效也能从缓存中获取,并在后台异步更新缓存。 - 互斥锁(Mutex):当缓存失效时,只允许一个线程访问数据库并更新缓存,其他线程等待获取缓存数据。 3. 缓存雪崩缓存雪崩是指缓存中大量的数据同时失效,导致所有请求都要访问数据库,造成数据库负载过大。主要原因是缓存中的数据同时过期。 解决方法: - 设置随机过期时间:为缓存数据设置随机的过期时间,避免大量数据同时失效。 - 使用分布式缓存:将缓存分布在不同的节点上,提高系统的可用性和容错能力。 - 数据预热:提前加载热点数据到缓存中,避免在高并发时突然访问数据库。 以上是对缓存穿透缓存击穿缓存雪崩问题的原因和解决方法的简要介绍,实际应用中可能还需要结合具体场景进行调整和优化。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值