为什么使用缓存?有哪些常见的使用方式和异常场景?

为什么使用缓存

  • 提升读速度,数据缓存,大量联表查询。例如,根据用户名称查询用户,获取相关的角色,操作权限等信息。
  • 降低数据库层的负载,当并发比较高时,redis缓存的每秒钟可以支持几万次的查询请求,可以有效的降低数据库查询的负载。

常见的使用方式

  • 设置最大的缓存量,以及达到最大存储量时的剔除规则。以redis为例,maxmemory 最大缓存数,maxmemory-policy 剔除策略,包括LRU策略,random随机剔除,TTL删除即将失效的key。
  • 超时剔除,设置失效时间,到达时间后自定剔除。

  • 主动更新,对于实时性要求较高的数据,在修改后主动修改缓存。

常见异常场景

缓存穿透

一般缓存设计,首先从缓存查询数据,如果没有值,再从DB数据库获取数据,获取数据后写入缓存。缓存穿透是指查询一个不存在的数据,在缓存层DB数据库都不能查询到数据,最后返回空数据。如果该类情况的访问并发很高,或者受到恶意的攻击,可能会造成DB数据库宕机。解决方式一般两种方案,如下:

  • 缓存空对象

在DB数据库查询不到数据时,设置一个空对象,下次再次查询该对象时,则直接返回缓存对象。示例代码如下:

public Goods getById(Long id) {
 
        if (Objects.isNull(id)) {
            return null;
        }
        String key = GOODS + id;
        String goodsJson = redisTemplate.opsForValue().get(key);
        if (StringUtils.isNotBlank(goodsJson)) {
            return JSON.parseObject(goodsJson, Goods.class);
        }
        Goods goods = goodsMapper.selectByPrimaryKey(id);
        if (Objects.nonNull(goods)) {
            redisTemplate.opsForValue().set(key, JSON.toJSONString(goods), RedisConst.DEFAULT_TIMEOUT_SECONDS, TimeUnit.SECONDS);
            return goods;
        } else {
            
            // 穿透优化
            redisTemplate.opsForValue().set(key, JSON.toJSONString(new Goods()), RedisConst.DEFAULT_TIMEOUT_SECONDS, TimeUnit.SECONDS);
            return null;
        }
    }

但是,缓存空对象存在一些问题。存储空对象后,需要更多的存储空间(如果是收到攻击,问题更严重)。可以设置一个失效时间,让其自动删除。缓存了空对象后,与DB数据库的数据会有一段数据不一致。例如,设置缓存的失效时间是2分钟,在这段时间内,数据库又添加了该id的数据,就出现了缓存和数据库数据不一致。此时,可以利用消息系统或者其他方式清理缓存。 

  • 使用布隆过滤器

对于数据实时性比较小的数据,可以使用bloomfilter过滤器存储数据库存在的数据,在缓存查询数据前,根据bloomfilter过滤器做一次校验。如果存在,在进行查询操作,反之,则直接返回空值。这在一定程度上减少了缓存和DB数据库的负载,起到了保护作用。由于bloomfilter没有删除操作,所以,该解决方案只适用于数据实时性不大,数据量大的情况。

缓存雪崩

在高并发请求的情况下,由于缓存层服务器崩溃,导致所有的请求直接在db层执行,导致db崩溃,进而导致整个服务的崩溃不可用。解决方案如下:

  1. 要保证缓存层的高可用,以redis为例,其高可用方案包括Redis sentinel(哨兵模式),Redis Cluster(集群模式)。
  2. 对高并发的服务做限流和降级处理,当请求超过了服务的负载,则进行对应的限流和降级处理。常见的解决方案包括spring cloud 的hystrix,以及alibaba sentinel。

缓存击穿

一般缓存设计都是使用缓存+失效时间的模式,这种模式存在一种问题。对于一些热点key的数据,当该key的缓存失效,需要重新从DB数据库获取。这时,涌入大量请求,所有请求并发地访问DB数据库,可能会压垮数据库,这种情况叫做缓存击穿。解决方案如下:

  • 使用互斥分布式锁

在请求并发访问数据库时,对查询数据库操作进行加锁,同一时间只有一个请求操作数据库。示例代码如下:

public Goods getById(Long id) {
 
        if (Objects.isNull(id)) {
            return null;
        }
        String key = GOODS + id;
        String goodsJson = redisTemplate.opsForValue().get(key);
        if (StringUtils.isNotBlank(goodsJson)) {
            return JSON.parseObject(goodsJson, Goods.class);
        }
        Goods goods = goodsMapper.selectByPrimaryKey(id);
        if (Objects.nonNull(goods)) {
            redisTemplate.opsForValue().set(key, JSON.toJSONString(goods), RedisConst.DEFAULT_TIMEOUT_SECONDS, TimeUnit.SECONDS);
            return goods;
        } else {
            
            // 穿透优化
            redisTemplate.opsForValue().set(key, JSON.toJSONString(new Goods()), RedisConst.DEFAULT_TIMEOUT_SECONDS, TimeUnit.SECONDS);
            return null;
        }
    }
  • 设置热点key数据永不过期

就是保证数据一直在缓存不过期,在缓存即将过期时,进行缓存的更新,已经过期时间的延长。

 

 

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值