为什么使用缓存
- 提升读速度,数据缓存,大量联表查询。例如,根据用户名称查询用户,获取相关的角色,操作权限等信息。
- 降低数据库层的负载,当并发比较高时,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崩溃,进而导致整个服务的崩溃不可用。解决方案如下:
- 要保证缓存层的高可用,以redis为例,其高可用方案包括Redis sentinel(哨兵模式),Redis Cluster(集群模式)。
-
对高并发的服务做限流和降级处理,当请求超过了服务的负载,则进行对应的限流和降级处理。常见的解决方案包括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数据永不过期
就是保证数据一直在缓存不过期,在缓存即将过期时,进行缓存的更新,已经过期时间的延长。
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。
文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/13643.html