使用缓存,需要注意的是缓存穿透、缓存击穿、缓存雪崩等问题。而我们使用SpringCache注解式缓存,更应该统一的处理这些情况。
1. 缓存穿透
此语义带有攻击性,一般是黑客攻击服务器时的手段,所以叫做缓存穿透。
若数据在数据库中不存在,那么也不可能在缓存中存在。对于这样的数据,在缓存中查询不到就会去DB中查询。当请求数量变多时,给DB带来压力。
为避免这两种请求,可以采取以下两种方案:
- 约定:若数据在DB中不存在,依旧进行缓存。
- 过滤:使用bitMap或者布隆过滤器制定过滤规则去过滤一些不存在的问题。
SpringCache中的RedisCache默认运行将Null值存入到缓存中。
可以通过disableCachingNullValues()
方法关闭此设置。
public RedisCacheConfiguration customProtoStuffRedisCacheConfiguration(Duration ttl) {
//设置序列化格式
ProtoStuffRedisSerializer protoStuffRedisSerializer = new ProtoStuffRedisSerializer();
return RedisCacheConfiguration.
defaultCacheConfig().serializeValuesWith(
RedisSerializationContext.SerializationPair.fromSerializer(protoStuffRedisSerializer)).
computePrefixWith(cacheName -> "lianyin:" + cacheName + ":"). //设置Cache的前缀,默认::
// disableCachingNullValues(). //若返回值为null,则不允许存储到Cache中
entryTtl(ttl); //设置缓存的超时时间
}
2. 缓存击穿
缓存失效,导致了缓存被击穿。
某些热点数据失效后,可能会造成大量请求未命中缓存转而去DB请求。
- 加锁,只有一个线程去维护缓存,其他线程阻塞。
- 异步加载:缓存击穿是热点数据才会出现的问题,可以对这部分数据采用到期自动刷新的策略,而不是到期自动淘汰。
SpringCache采用sync
属性,只有一个线程去维护缓存,其他线程会被阻塞,直到缓存中更新该条目为止。注意此次不是分布式锁,是进程锁。好处是即使维护线程的请求由于特殊原因被阻塞,其他进程也可以维护线程。防止死锁
。
@Cacheable(cacheNames="foos", sync=true)
public Foo executeExpensiveOperation(String id) {...}
3. 缓存雪崩
缓存雪崩是值缓存不可用或者大量缓存由于超时时间相同在同一时间段失效。大量请求访问数据库,数据库压力过大导致雪崩。
- 增加缓存监控,关注缓存的健康程度,根据业务适当的扩容缓存;
- 采用多级缓存,不同级别的缓存设置不同的超时时间;
- 缓存的key失效时间设置为随机数。
缓存失效,可以重写RedisCache,在执行增改删查时,先查进程内缓存。