最近发现线上监控有个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