spring cache注解@Cacheable缓存穿透详解
作者:chengbinbbs 发布时间:2023-12-23 13:41:25
最近发现线上监控有个SQL调用量很大,但是方法的调用量不是很大,查看接口实现,发现接口是做了缓存操作的,使用Spring cache缓存注解结合tair实现缓存操作。
但是为啥SQL调用量这么大,难道缓存没有生效。测试发现缓存是正常的,分析了代码发现,代码存在缓存穿透的风险。
具体注解是这样的
@Cacheable(value = "storeDeliveryCoverage", key = "#sellerId + '|' + #cityCode", unless = "#result == null")
unless = "#result == null"表明接口返回值不为空的时候才缓存,如果线上有大量不合法的请求参数过来,由于为空的不会缓存起来,每次请求都打到DB上,导致DB的sql调用量巨大,给了黑客可乘之机,风险还是很大的。
找到原因之后就修改,查询结果为空的时候兜底一个null,把这句unless = "#result == null"条件去掉测试了一下,发现为空的话还是不会缓存。于是debug分析了一波源码,终于发现原来是tair的问题。
由于tair自身的特性,无法缓存null。既然无法缓存null,那我们就兜底一个空对象进去,取出来的时候把空对象转化为null。
基于这个思路我把Cache的实现改造了一下
@Override
public void put(Object key, Object value) {
if (value == null) {
// 为空的话,兜底一个空对象,防止缓存穿透(由于tair自身特性不允许缓存null对象的原因,这里缓存一个空对象)
value = new Nil();
}
if (value instanceof Serializable) {
final String tairKey = String.format("%s:%s", this.name, key);
final ResultCode resultCode = this.tairManager.put(
this.namespace,
tairKey,
(Serializable) value,
0,
this.timeout
);
if (resultCode != ResultCode.SUCCESS) {
TairSpringCache.log.error(
String.format(
"[CachePut]: unable to put %s => %s into tair due to: %s",
key,
value,
resultCode.getMessage()
)
);
}
} else {
throw new RuntimeException(
String.format(
"[CachePut]: value %s is not Serializable",
value
)
);
}
}
Nil类默认是一个空对象,这里给了个内部类:
static class Nil implements Serializable {
private static final long serialVersionUID = -9138993336039047508L;
}
取缓存的get方法实现
@Override
public ValueWrapper get(Object key) {
final String tairKey = String.format("%s:%s", this.name, key);
final Result<DataEntry> result = this.tairManager.get(this.namespace, tairKey);
if (result.isSuccess() && (result.getRc() == ResultCode.SUCCESS)) {
final Object obj = result.getValue().getValue();
// 缓存为空兜底的是Nil对象,这里返回的时候需要转为null
if (obj instanceof Nil) {
return null;
}
return () -> obj;
}
return null;
}
改好了之后,测试一下,结果发现还是没有生效,缓存没有兜底,请求都打到DB上了。
debug走一遍,看了下Cache的源码,终于发现关键问题所在(具体实现流程参考上一篇:Spring Cache- 缓存 * ( CacheInterceptor)):
private Object execute(final CacheOperationInvoker invoker, Method method, CacheOperationContexts contexts) {
// Special handling of synchronized invocation
if (contexts.isSynchronized()) {
CacheOperationContext context = contexts.get(CacheableOperation.class).iterator().next();
if (isConditionPassing(context, CacheOperationExpressionEvaluator.NO_RESULT)) {
Object key = generateKey(context, CacheOperationExpressionEvaluator.NO_RESULT);
Cache cache = context.getCaches().iterator().next();
try {
return wrapCacheValue(method, cache.get(key, new Callable<Object>() {
@Override
public Object call() throws Exception {
return unwrapReturnValue(invokeOperation(invoker));
}
}));
}
catch (Cache.ValueRetrievalException ex) {
// The invoker wraps any Throwable in a ThrowableWrapper instance so we
// can just make sure that one bubbles up the stack.
throw (CacheOperationInvoker.ThrowableWrapper) ex.getCause();
}
}
else {
// No caching required, only call the underlying method
return invokeOperation(invoker);
}
}
// 处理beforeIntercepte=true的缓存删除操作
processCacheEvicts(contexts.get(CacheEvictOperation.class), true,
CacheOperationExpressionEvaluator.NO_RESULT);
// 从缓存中查找,是否有匹配@Cacheable的缓存数据
Cache.ValueWrapper cacheHit = findCachedItem(contexts.get(CacheableOperation.class));
// 如果@Cacheable没有被缓存,那么就需要将数据缓存起来,这里将@Cacheable操作收集成CachePutRequest集合,以便后续做@CachePut缓存数据存放。
List<CachePutRequest> cachePutRequests = new LinkedList<CachePutRequest>();
if (cacheHit == null) {
collectPutRequests(contexts.get(CacheableOperation.class),
CacheOperationExpressionEvaluator.NO_RESULT, cachePutRequests);
}
Object cacheValue;
Object returnValue;
//如果没有@CachePut操作,就使用@Cacheable获取的结果(可能也没有@Cableable,所以result可能为空)。
if (cacheHit != null && cachePutRequests.isEmpty() && !hasCachePut(contexts)) {
//如果没有@CachePut操作,并且cacheHit不为空,说明命中缓存了,直接返回缓存结果
cacheValue = cacheHit.get();
returnValue = wrapCacheValue(method, cacheValue);
}
else {
// 否则执行具体方法内容,返回缓存的结果
returnValue = invokeOperation(invoker);
cacheValue = unwrapReturnValue(returnValue);
}
// Collect any explicit @CachePuts
collectPutRequests(contexts.get(CachePutOperation.class), cacheValue, cachePutRequests);
// Process any collected put requests, either from @CachePut or a @Cacheable miss
for (CachePutRequest cachePutRequest : cachePutRequests) {
cachePutRequest.apply(cacheValue);
}
// Process any late evictions
processCacheEvicts(contexts.get(CacheEvictOperation.class), false, cacheValue);
return returnValue;
}
根据key从缓存中查找,返回的结果是ValueWrapper,它是返回结果的包装器:
private Cache.ValueWrapper findCachedItem(Collection<CacheOperationContext> contexts) {
Object result = CacheOperationExpressionEvaluator.NO_RESULT;
for (CacheOperationContext context : contexts) {
if (isConditionPassing(context, result)) {
Object key = generateKey(context, result);
Cache.ValueWrapper cached = findInCaches(context, key);
if (cached != null) {
return cached;
}
else {
if (logger.isTraceEnabled()) {
logger.trace("No cache entry for key '" + key + "' in cache(s) " + context.getCacheNames());
}
}
}
}
return null;
}
private Cache.ValueWrapper findInCaches(CacheOperationContext context, Object key) {
for (Cache cache : context.getCaches()) {
Cache.ValueWrapper wrapper = doGet(cache, key);
if (wrapper != null) {
if (logger.isTraceEnabled()) {
logger.trace("Cache entry for key '" + key + "' found in cache '" + cache.getName() + "'");
}
return wrapper;
}
}
return null;
}
这里判断缓存是否命中的逻辑是根据cacheHit是否为空,而cacheHit是ValueWrapper类型,查看ValueWrapper是一个接口,它的实现类是SimpleValueWrapper,这是一个包装器,将缓存的结果包装起来了。
而我们前面的get方法取缓存的时候如果为Nil对象,返回的是null,这样缓存判断出来是没有命中,即cacheHit==null,就会去执行具体方法朔源。
所以到这里已经很清晰了,关键问题是get取缓存的结果如果是兜底的Nil对象,应该返回new SimpleValueWrapper(null)。
应该返回包装器,包装的是缓存的对象为null。
测试了一下,发现ok了
具体源码如下:
/**
* 基于tair的缓存,适配spring缓存框架
*/
public class TairSpringCache implements Cache {
private static final Logger log = LoggerFactory.getLogger(TairSpringCache.class);
private TairManager tairManager;
private final String name;
private int namespace;
private int timeout;
public TairSpringCache(String name, TairManager tairManager, int namespace) {
this(name, tairManager, namespace, 0);
}
public TairSpringCache(String name, TairManager tairManager, int namespace, int timeout) {
this.name = name;
this.tairManager = tairManager;
this.namespace = namespace;
this.timeout = timeout;
}
@Override
public String getName() {
return this.name;
}
@Override
public Object getNativeCache() {
return this.tairManager;
}
@Override
public ValueWrapper get(Object key) {
final String tairKey = String.format("%s:%s", this.name, key);
final Result<DataEntry> result = this.tairManager.get(this.namespace, tairKey);
if (result.isSuccess() && (result.getRc() == ResultCode.SUCCESS)) {
final Object obj = result.getValue().getValue();
// 缓存为空兜底的是Nil对象,这里返回的时候需要转为null
if (obj instanceof Nil) {
return () -> null;
}
return () -> obj;
}
return null;
}
@Override
public <T> T get(Object key, Class<T> type) {
return (T) this.get(key).get();
}
public <T> T get(Object o, Callable<T> callable) {
return null;
}
@Override
public void put(Object key, Object value) {
if (value == null) {
// 为空的话,兜底一个空对象,防止缓存穿透(由于tair自身特性不允许缓存null对象的原因,这里缓存一个空对象)
value = new Nil();
}
if (value instanceof Serializable) {
final String tairKey = String.format("%s:%s", this.name, key);
final ResultCode resultCode = this.tairManager.put(
this.namespace,
tairKey,
(Serializable) value,
0,
this.timeout
);
if (resultCode != ResultCode.SUCCESS) {
TairSpringCache.log.error(
String.format(
"[CachePut]: unable to put %s => %s into tair due to: %s",
key,
value,
resultCode.getMessage()
)
);
}
} else {
throw new RuntimeException(
String.format(
"[CachePut]: value %s is not Serializable",
value
)
);
}
}
public ValueWrapper putIfAbsent(Object key, Object value) {
final ValueWrapper vw = this.get(key);
if (vw.get() == null) {
this.put(key, value);
}
return vw;
}
@Override
public void evict(Object key) {
final String tairKey = String.format("%s:%s", this.name, key);
final ResultCode resultCode = this.tairManager.delete(this.namespace, tairKey);
if ((resultCode == ResultCode.SUCCESS)
|| (resultCode == ResultCode.DATANOTEXSITS)
|| (resultCode == ResultCode.DATAEXPIRED)) {
return;
}
else {
final String errMsg = String.format(
"[CacheDelete]: unable to evict key %s, resultCode: %s",
key,
resultCode
);
TairSpringCache.log.error(errMsg);
throw new RuntimeException(errMsg);
}
}
@Override
public void clear() {
//TODO fgz: implement here later
}
public void setTairManager(TairManager tairManager) {
this.tairManager = tairManager;
}
public void setNamespace(int namespace) {
this.namespace = namespace;
}
public void setTimeout(int timeout) {
this.timeout = timeout;
}
static class Nil implements Serializable {
private static final long serialVersionUID = -9138993336039047508L;
}
}
测试用例就不贴了。
来源:https://blog.csdn.net/chengbinbbs/article/details/88393102


猜你喜欢
- 首先,将json串转为一个JObject对象:JObject jo = (JObject)JsonConvert.DeserializeOb
- 一、Autowired注解的用法1、概述使用spring开发时,进行配置主要有两种方式,一是xml的方式,二是java config的方式。
- 先给大家展示下效果图:1、验证码生成类:import java.util.Random;import java.awt.imag
- 一.RabbitMQ消息丢失的三种情况第一种:生产者弄丢了数据。生产者将数据发送到 RabbitMQ 的时候,可能数据就在半路给搞丢了,因为
- HashMap和HashSet的区别是Java面试中最常被问到的问题。如果没有涉及到Collection框架以及多线程的面试,可以说是不完整
- 一、首先看图二、lock()跟踪源码这里对公平锁和非公平锁做了不同实现,由构造方法参数决定是否公平。public ReentrantLock
- 一、着色游戏概述近期群里偶然看到一哥们在群里聊不规则图像填充什么四联通、八联通什么的,就本身好学务实的态度去查阅了相关资料。对于这类着色的资
- 异常捕获机制 C#1.示意图2.异常捕获机制,代码:3.异常捕获机制,结果:4.求几周,剩余几天?代码:5.结果:6.求几月几周零几天 设一
- 本文实例讲述了Java数组的定义、初始化、及二维数组用法。分享给大家供大家参考,具体如下:数组的定义1.数组是有序数据的集合,数组中的每个元
- 本文实例讲述了Jaxb2实现JavaBean与xml互转的方法。分享给大家供大家参考,具体如下:一、简介JAXB(Java Architec
- 一、WebRequestMethods.Ftp类:表示可与 FTP 请求一起使用的 FTP 协议方法的类型。AppendFile:表示要用于
- Mybatis-Plus是一个优秀的Mybatis增强工具,目前更新到3.1.1。Mybatis-Plus原生提供了很多单表操作的方法,极大
- 小背景:我们公司项目中的小脚本是一些工具类,比如常用的是MapUtil工具类的一些方法写公司的MapUtil工具类的方法要注意,方法名的命名
- 1.代码调试的重要性代码调试在程序开发阶段占有举足轻重的地位,可见代码调试的重要性。但是有一点必须强调:程序是设计出来的,而不是调试出来的。
- 实体对象之间相互传值,如:VO对象的值赋给Entity对象,是代码中常用功能,如果通过get、set相互赋值,则很麻烦,借助工具类BeanU
- springBoot使用事物比较简单,在Application启动类s上添加@EnableTransactionManagement注解,然
- 本文描述了TCP协议,首先简单介绍了TCP完成了一些什么功能;介绍了TCP报文格式,以及典型报文的数据格式;接着从链路控制和数据传输两个方面
- 微信红包的使用已经很广泛,本篇文章介绍了微信发红包的实例,需要有认证的公众号,且开通了微信支付,商户平台且开通了现金红包的权限即可。http
- 简单看一下描述,例子最重要。1、getPath():返回定义时的路径,(就是你写什么路径,他就返回什么路径)2、getAbsolutePat
- 插入排序插入排序的代码实现虽然没有冒泡排序和选择排序那么简单粗暴,但它的原理应该是最容易理解的了,因为只要打过扑克牌的人都应该能够秒懂。插入