缓存四大问题及解决方案

问题一:缓存穿透,指缓存中没有数据,数据库中也没有数据。在进行数据的访问时,通过数据的key读取数据,但是该key对应数据在数据库中没有,在缓存中也没有,造成每次通过该key读取数据都会进行数据库操作,且每次读取都为null的情况。在大型项目中,这种无效的数据库操作会增加数据库的读压力。

示例代码:

public Object getData(String key){
    if(redisTemplate.hasKey(key)){
        return redisTemplate.opsForValue().get(key);
    }
    Object object = dataDao.selectSysCompanyById(key);
    if(object != null){
        redisTemplate.opsForValue().set(key,object);
    }
    
    return  object;
}

如上代码,在项目当中读取数据时,首先都会先读取缓存,如果缓存中不存在数据,则从数据库读取数据,如果从数据库读取到数据,那么将数据缓存并返回。

当请求的key对应的数据,在缓存中不存在,同时在数据库中也不存在时,就会出现每次都会执行数据库操作,但是每次都是返回null的无效数据库操作,这种情况就是典型的缓存穿透。

解决方案一:缓存空对象,即对于在数据库中和缓存中都不存在的数据,第一次读取到为null时,在缓存中就存一个空值。读取缓存的时候,如果缓存中存在,且为空对象,直接返回即可。

public Object getData(String key){
    if(redisTemplate.hasKey(key)){
        if(redisTemplate.opsForValue().get(key) == null){
            //存在缓存且为空,直接返回
            return null;
        }
        return redisTemplate.opsForValue().get(key);
    }
    Object object = dataDao.selectSysCompanyById(key);
    if(object != null){
        redisTemplate.opsForValue().set(key,object);
    }else{
        //缓存空对象
        redisTemplate.opsForValue().set(key, null);
    }

    return  object;
}

解决方案二:使用布隆过滤器,所谓布隆过滤器,我们可以理解其为一个集合,这个集合只存储数据的key而不存储数据值,主要用来判断一个key存在还是不存在。布隆过滤器的介绍见https://zhuanlan.zhihu.com/p/72378274

google的guava实现了单机版的(即基于JVM)的布隆过滤器,我们的示例使用guava来呈现,在实际的项目中,如果已经达到了要使用布隆过滤器,那么基本上是要自己实现一个分布式的布隆过滤器,可以先研究google的guava的实现,来自行尝试实现一个自己的布隆过滤器。

使用布隆过滤器解决缓存穿透,其主要思想是,将数据的key全部存入布隆过滤器,在进行数据读取时,先判断key在布隆过滤器中是否存在,如果存在则执行读取操作,如果不存在,直接返回。

也就是说,在创建数据或者将数据同步到缓存服务时,需要将数据的key存储到布隆过滤器的集合中,用以作为读取数据时的判断依据。

public Object getDataWithBloom(String key) {
    //initBloomFilter : 初始化布隆过滤器,将数据库中所有的key都写入到布隆过滤器
    BloomFilter<String> bloomFilter = initBloomFilter();

    if(!bloomFilter.mightContain(key)){
        //存在缓存且为空,直接返回
        return null;
    }
    if(redisTemplate.hasKey(key)){
        return redisTemplate.opsForValue().get(key);
    }
    Object object = dbMapper.selectSysCompanyById(key);
    if(object != null){
        redisTemplate.opsForValue().set(key,object);
    }
    return  object;
}

问题二:缓存击穿,所谓缓存击穿,是指指定key的数据在数据库中存在,但是缓存中还没有写入该key对应的数据,在高并发场景下,多个线程同时通过该key读取数据时,会因为高并发访问的原因导致同一个key对应的数据会从数据库被多次读取。正常情况下是同一个key的数据,只能允许一个线程从数据库读取一次并缓存到缓存服务器Redis之后,其他的线程读取数据时直接从缓存服务读取。

示例代码:

public Object getData(String key){
    if(redisTemplate.hasKey(key)){
        return redisTemplate.opsForValue().get(key);
    }
    Object object = dataDao.selectSysCompanyById(key);
    if(object != null){
        redisTemplate.opsForValue().set(key,object);
    }
    
    return  object;
}

比如上述代码实现,就存在缓存击穿的风险,在高并发场景下,如果同时有100个线程请求同一个key的数据,且该数据在数据库中存在,但是在缓存中不存在。那么在某一个线程从数据库读取数据并写入到缓存之前,所有执行了到2行代码的线程都会执行一次数据库的select操作和缓存的写入操作。如第一个线程还未读取数据写入缓存,就有50个线程执行完了第2行代码,那么这50个线程都会执行一次数据库的select操作,同时也都会做一次缓存写入操作,这就是典型的缓存击穿现象。

解决方案:互斥锁,单机情况下,使用JVM中的同步代码块synchronized或者ReentrantLock,分布式情况下,使用分布式锁。
比如:

public Object getData(String key) {
    //initBloomFilter : 初始化布隆过滤器
    BloomFilter<String> bloomFilter = BloomFilter.create(Funnels.stringFunnel(Charset.defaultCharset()),10000,0.0001);

    if(!bloomFilter.mightContain(key)){
        //存在缓存且为空,直接返回
        return null;
    }

    if(redisTemplate.hasKey(key)){
        return redisTemplate.opsForValue().get(key);
    }

    //加锁
    RLock lock = redissonClient.getLock(key);
    lock.lock();
    Object object = null;
    try {
        if(redisTemplate.hasKey(key)){
            //阻塞的线程获得锁后,直接从缓存读取返回
            return redisTemplate.opsForValue().get(key);
        }

        //第一个线程读库
        object = dbMapper.selectSysCompanyById(key);
        if(object != null){
            //第一个线程写缓存
            redisTemplate.opsForValue().set(key,object);
        }
    }finally {
        //释放锁
        lock.unlock();
    }

    return  object;
}

问题三:缓存雪崩,所谓缓存雪崩,就是指缓存服务不具备高可用性导致大面积缓存失效的情况。

解决方案:缓存集群,数据量不大,小公司,采用Redis主从+哨兵模式的解决方案,海量数据,大公司,采用Redis Cluster模式搭建缓存集群。

如果发生了雪崩,应急方案为进行服务熔断,通过限流慢慢对缓存进行预热。

问题四:缓存与数据库数据一致性问题,所谓数据一致性,就是指在解决数据进行更新时,如何对数据进行操作才能保证数据库中的数据和缓存中的数据一致的问题。

发生数据一致性问题的场景一:
在这里插入图片描述
发生数据一致性问题的场景二:

在这里插入图片描述
解决方案:先删除缓存,再更新数据库。但是单纯的先删除缓存,再更新数据库,高并发场景下依然解决不了问题。如下图:
在这里插入图片描述

解决方案一:延迟双删
在这里插入图片描述
解决方案二:串行化
在这里插入图片描述

  • 3
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

RonTech

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值