iis服务器助手广告广告
返回顶部
首页 > 资讯 > 后端开发 > Python >spring cache注解@Cacheable缓存穿透详解
  • 485
分享到

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

2024-04-02 19:04:59 485人浏览 安东尼

Python 官方文档:入门教程 => 点击学习

摘要

目录具体注解是这样的基于这个思路我把Cache的实现改造了一下取缓存的get方法实现测试了一下,发现ok了最近发现线上监控有个sql调用量很大,但是方法的调用量不是很大,查看接口实现

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

具体源码如下:



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;
    }
}

测试用例就不贴了。

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

--结束END--

本文标题: spring cache注解@Cacheable缓存穿透详解

本文链接: https://www.lsjlt.com/news/161024.html(转载时请注明来源链接)

有问题或投稿请发送至: 邮箱/279061341@qq.com    QQ/279061341

本篇文章演示代码以及资料文档资料下载

下载Word文档到电脑,方便收藏和打印~

下载Word文档
猜你喜欢
  • spring cache注解@Cacheable缓存穿透详解
    目录具体注解是这样的基于这个思路我把Cache的实现改造了一下取缓存的get方法实现测试了一下,发现ok了最近发现线上监控有个SQL调用量很大,但是方法的调用量不是很大,查看接口实现...
    99+
    2024-04-02
  • Spring中的@Cacheable缓存注解详解
    目录1 什么是缓存2 本地缓存和集中式缓存3 本地缓存的优点4 Spring对于缓存的支持4.1 spring支持的CacheManager4.2 GuavaCache4.3 引入依...
    99+
    2023-05-20
    Spring @Cacheable Spring 缓存注解
  • 详解Spring缓存注解@Cacheable,@CachePut , @CacheEvict使用
    注释介绍@Cacheable@Cacheable 的作用 主要针对方法配置,能够根据方法的请求参数对其结果进行缓存@Cacheable 作用和配置方法 参数 解释 example ...
    99+
    2023-05-31
    spring cache 注解
  • spring缓存cache的使用详解
    目录spring缓存cache的使用springcache配置缓存存活时间spring缓存cache的使用 在spring配置文件中添加schema和spring对缓存注解的支持: ...
    99+
    2024-04-02
  • 详解缓存穿透击穿雪崩解决方案
    目录一:前言二:缓存穿透三:解决方案四:缓存雪崩五:解决方案六:缓存击穿七:解决方案1、使用互斥锁(mutexkey)2、"提前"使用互斥锁(mutexkey)3、"永远不过期"4、...
    99+
    2024-04-02
  • Spring缓存注解@Cacheable @CacheEvit @CachePut使用介绍
    目录I. 项目环境 1. 项目依赖 II. 缓存注解介绍 1. @Cacheable 2. @CachePut 3. @CacheEvict 4. @Caching 5. 异常时,缓...
    99+
    2024-04-02
  • Redis缓存击穿、缓存穿透、缓存雪崩如何解决
    本篇内容介绍了“Redis缓存击穿、缓存穿透、缓存雪崩如何解决”的有关知识,在实际案例的操作过程中,不少人都会遇到这样的困境,接下来就让小编带领大家学习一下如何处理这些情况吧!希望大家仔细阅读,能够学有所成!Redis缓存使用场景Redis...
    99+
    2023-07-05
  • 缓存穿透,缓存击穿,缓存雪崩解决方案分析
    阅读本文大概需要 3.7 分钟。一、前言设计一个缓存系统,不得不要考虑的问题就是:缓存穿透、缓存击穿与失效时的雪崩效应。二、缓存穿透缓存穿透是指查询一个一定不存在的数据,由于缓存是不命中时被动写的,并且出于容错考虑,如果从存储层查不到数据则...
    99+
    2023-06-05
  • redis缓存穿透解决方法
    缓存技术可以用来减轻数据库的压力,提升访问效率。目前在企业项目中对缓存也是越来越重视。但是缓存不是说随随便便加入项目就可以了。将缓存整合到项目中,这才是第一步。而缓存带来的穿透问题,进而导致的雪蹦问题都是我...
    99+
    2024-04-02
  • 如何解决redis缓存穿透
    redis 缓存穿透是指不存在于缓存中的键每次都会直接查询数据库,可采取以下措施解决:1. 使用布隆过滤器快速判断键是否存在;2. 使用空值缓存缓存不存在的值;3. 应用缓存穿透保护算法...
    99+
    2024-04-20
    redis
  • 怎么使用spring-cache代码解决缓存击穿问题
    本篇内容介绍了“怎么使用spring-cache代码解决缓存击穿问题”的有关知识,在实际案例的操作过程中,不少人都会遇到这样的困境,接下来就让小编带领大家学习一下如何处理这些情况吧!希望大家仔细阅读,能够学有所成!正文目前缺陷首先,为什么说...
    99+
    2023-06-30
  • 使用spring-cache一行代码解决缓存击穿问题
    目录引言正文目前缺陷真正方案缓存穿透缓存击穿缓存雪崩文末引言 今天,重新回顾一下缓存击穿这个问题! 之所以写这个文章呢,因为目前网上流传的文章落地性太差(什么布隆过滤器啊,布谷过滤器...
    99+
    2024-04-02
  • redis如何解决缓存穿透
    redis通过以下方法解决缓存穿透:1. 设置默认值;2. 使用布隆过滤器;3. 使用空值缓存;4. 使用限流。这样,当查询不存在的键时,redis可以返回默认值、检查存在性或限制请求速...
    99+
    2024-04-19
    redis
  • redis缓存穿透如何解决
    缓存穿透是指恶意用户不断查询数据库中不存在的数据,导致性能下降。解决方案包括:设置默认值使用布隆过滤器采用验证码机制限流慢查询日志分析加强数据校验 Redis 缓存穿透解决方案 什么是...
    99+
    2024-04-20
    redis
  • 如何解决redis缓存穿透、缓存击穿、缓存雪崩的问题
    这篇文章主要介绍了如何解决redis缓存穿透、缓存击穿、缓存雪崩的问题,具有一定借鉴价值,感兴趣的朋友可以参考下,希望大家阅读完这篇文章之后大有收获,下面让小编带着大家一起了解一下。缓存穿透:key中对应的...
    99+
    2024-04-02
  • Spring@Cacheable注解中key的使用详解
    目录Spring@Cacheable注解中key使用下面是几个使用参数作为key的示例condition属性指定发生的条件@CachePut@CacheEvictallEntries...
    99+
    2024-04-02
  • 使用Spring如何实现注解Cache与Redis缓存
    使用Spring如何实现注解Cache与Redis缓存?相信很多没有经验的人对此束手无策,为此本文总结了问题出现的原因和解决方法,通过这篇文章希望你能解决这个问题。1. redis 和 ehcache的区别:简单了解了下,个人觉得 从部署上...
    99+
    2023-05-31
    spring redis 缓存
  • SpringBoot使用@Cacheable注解实现缓存功能流程详解
    目录一、Spring从3.1开始支持Cache二、@Cacheable常用属性1、value/cacheNames2、key3、condition4、unless5、keyGener...
    99+
    2023-01-12
    SpringBoot @Cacheable注解 SpringBoot @Cacheable实现缓存
  • 详解Redis缓存穿透/击穿/雪崩原理及其解决方案
    目录1. 简介2. 缓存穿透2.1描述2.2 解决方案3. 缓存击穿3.1 描述3.2 解决方案4. 缓存雪崩4.1 描述4.1 解决方案5. 布隆过滤器5.1 描述5.2 数据结构...
    99+
    2024-04-02
  • redis缓存穿透的解决方法
    这篇文章主要介绍redis缓存穿透的解决方法,文中介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们一定要看完!缓存技术可以用来减轻数据库的压力,提升访问效率。目前在企业项目中对缓存也是越来越重视。但是缓...
    99+
    2024-04-02
软考高级职称资格查询
编程网,编程工程师的家园,是目前国内优秀的开源技术社区之一,形成了由开源软件库、代码分享、资讯、协作翻译、讨论区和博客等几大频道内容,为IT开发者提供了一个发现、使用、并交流开源技术的平台。
  • 官方手机版

  • 微信公众号

  • 商务合作