spring cache注解@Cacheable缓存穿透详解

目录
  • 具体注解是这样的
  • 基于这个思路我把Cache的实现改造了一下
  • 取缓存的get方法实现
  • 测试了一下,发现ok了

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

测试用例就不贴了。

以上为个人经验,希望能给大家一个参考,也希望大家多多支持我们。

(0)

相关推荐

  • springboot中redis的缓存穿透问题实现

    什么是缓存穿透问题?? 我们使用redis是为了减少数据库的压力,让尽量多的请求去承压能力比较大的redis,而不是数据库.但是高并发条件下,可能会在redis还没有缓存的时候,大量的请求同时进入,导致一大批的请求直奔数据库,而不会经过redis.使用代码模拟缓存穿透问题如下: 首先是service里面的代码: @Service public class NewsService { @Autowired private NewsDAO newsDAO; //springboot自动初始化,不需要

  • 详解Spring缓存注解@Cacheable,@CachePut , @CacheEvict使用

    注释介绍 @Cacheable @Cacheable 的作用 主要针对方法配置,能够根据方法的请求参数对其结果进行缓存 @Cacheable 作用和配置方法 参数 解释 example value 缓存的名称,在 spring 配置文件中定义,必须指定至少一个 例如: @Cacheable(value="mycache") @Cacheable(value={"cache1","cache2"} key 缓存的 key,可以为空,如果指定要按照

  • spring整合redis缓存并以注解(@Cacheable、@CachePut、@CacheEvict)形式使用

    maven项目中在pom.xml中依赖2个jar包,其他的spring的jar包省略: <dependency> <groupId>redis.clients</groupId> <artifactId>jedis</artifactId> <version>2.8.1</version> </dependency> <dependency> <groupId>org.springfra

  • springboot增加注解缓存@Cacheable的实现

    目录 springboot增加注解缓存@Cacheable 业务层使用 配置 @Cacheable注解的属性使用 cacheNames和value key keyGenerator keyGenerator condition unless(除非) sync springboot增加注解缓存@Cacheable 业务层使用 @Cacheable(value = "dictionary#1800", key = "#root.targetClass.simpleName +':

  • Spring缓存注解@Cacheable @CacheEvit @CachePut使用介绍

    目录 I. 项目环境 1. 项目依赖 II. 缓存注解介绍 1. @Cacheable 2. @CachePut 3. @CacheEvict 4. @Caching 5. 异常时,缓存会怎样? 6. 测试用例 7. 小结 III. 不能错过的源码和相关知识点 0. 项目 Spring在3.1版本,就提供了一条基于注解的缓存策略,实际使用起来还是很丝滑的,本文将针对几个常用的注解进行简单的介绍说明,有需要的小伙伴可以尝试一下 本文主要知识点: @Cacheable: 缓存存在,则使用缓存:不存在

  • spring cache注解@Cacheable缓存穿透详解

    目录 具体注解是这样的 基于这个思路我把Cache的实现改造了一下 取缓存的get方法实现 测试了一下,发现ok了 最近发现线上监控有个SQL调用量很大,但是方法的调用量不是很大,查看接口实现,发现接口是做了缓存操作的,使用Spring cache缓存注解结合tair实现缓存操作. 但是为啥SQL调用量这么大,难道缓存没有生效.测试发现缓存是正常的,分析了代码发现,代码存在缓存穿透的风险. 具体注解是这样的 @Cacheable(value = "storeDeliveryCoverage&qu

  • Spring Cache整合Redis实现方法详解

    导入依赖 <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-cache</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>

  • SpringBoot日志注解与缓存优化详解

    目录 日志注解: 缓存的优化: 总结 日志注解: 关于SpringBoot中的日志处理,在之前的文章中页写过: 点击进入 这次通过注解+Aop的方式来实现日志的输出: 首先需要定义一个注解类:  @Target(ElementType.METHOD)  @Retention(RetentionPolicy.RUNTIME)  @Documented  public @interface LogAnnotation {      String module() default "";  

  • Spring AOP注解案例及基本原理详解

    切面:Aspect 切面=切入点+通知.在老的spring版本中通常用xml配置,现在通常是一个类带上@Aspect注解.切面负责将 横切逻辑(通知) 编织 到指定的连接点中. 目标对象:Target 将要被增强的对象. 连接点:JoinPoint 可以被拦截到的程序执行点,在spring中就是类中的方法. 切入点:PointCut 需要执行拦截的方法,也就是具体实施了横切逻辑的方法.切入点的规则在spring中通过AspectJ pointcut expression language来描述.

  • Spring Boot 注解方式自定义Endpoint详解

    目录 概述 准备 编写自定义Endpoint 配置 启动&测试 注意 Spring Boot 常用endpoint的使用 Actuator 一些常用 Endpoint 如何访问 Actuator Endpoint 概述 在使用Spring Boot的时候我们经常使用actuator,健康检查,bus中使用/refresh等.这里记录如何使用注解的方式自定义Endpoint.可用于满足一些服务状态监控,或者优雅停机等. 准备 Spring Boot项目,pom中加入: <dependency&

  • Spring MVC注解式开发使用详解

    MVC注解式开发即处理器基于注解的类开发, 对于每一个定义的处理器, 无需在xml中注册. 只需在代码中通过对类与方法的注解, 即可完成注册. 定义处理器 @Controller: 当前类为处理器 @RequestMapping: 当前方法为处理器方法, 方法名随意, 对于请求进行处理与响应. @Controller public class MyController { @RequestMapping(value = "/hello.do") public ModelAndView

  • Spring @Conditional注解讲解及示例详解

    前言: @Conditional是Spring4新提供的注解,它的作用是按照一定的条件进行判断,满足条件给容器注册bean. @Conditional的定义: //此注解可以标注在类和方法上 @Target({ElementType.TYPE, ElementType.METHOD}) @Retention(RetentionPolicy.RUNTIME) @Documented public @interface Conditional { Class<? extends Condition>

  • spring缓存代码详解

    本文研究的主要是spring缓存的相关内容,具体介绍如下. 这篇文章是根据谷歌翻译大致修改出来的,由于原文不知道是什么语,所以可能导致翻译的有错误和不准确的地方,但是大致的方向感觉还是蛮不错的,所以在这里整理了一下,希望能够有所帮助. 高速缓存一直是一个非常需要这两个提高应用程序性能并降低其工作量.此外,它的用处今天是特别明显,可以作出处理成千上万的游客concurrents.D'un架构上的Web应用,高速缓存管理正交于应用程序的业务逻辑和出于这个原因,应该对应用程序本身的发展产生的影响最小.

  • Spring 整合 Hibernate 时启用二级缓存实例详解

    Spring 整合 Hibernate 时启用二级缓存实例详解 写在前面: 1. 本例使用 Hibernate3 + Spring3: 2. 本例的查询使用了 HibernateTemplate: 1. 导入 ehcache-x.x.x.jar 包: 2. 在 applicationContext.xml 文件中找到 sessionFactory 相应的配置信息并在设置 hibernateProperties 中添加如下代码: <!-- 配置使用查询缓存 --> <prop key=&q

  • Spring学习笔记1之IOC详解尽量使用注解以及java代码

    在实战中学习Spring,本系列的最终目的是完成一个实现用户注册登录功能的项目. 预想的基本流程如下: 1.用户网站注册,填写用户名.密码.email.手机号信息,后台存入数据库后返回ok.(学习IOC,mybatis,SpringMVC的基础知识,表单数据验证,文件上传等) 2.服务器异步发送邮件给注册用户.(学习消息队列) 3.用户登录.(学习缓存.Spring Security) 4.其他. 边学习边总结,不定时更新.项目环境为Intellij + Spring4. 一.准备工作. 1.m

随机推荐