简直骚操作,ThreadLocal还能当缓存用
背景说明
有朋友问我一个关于接口优化的问题,他的优化点很清晰,由于接口中调用了内部很多的 service 去组成了一个完成的业务功能。每个 service 中的逻辑都是独立的,这样就导致了很多查询是重复的,看下图你就明白了。
上层查询传递下去
对于这种场景最好的就是在上层将需要的数据查询出来,然后传递到下层去消费。这样就不用重复查询了。
如果开始写代码的时候是这样做的没问题,但很多时候,之前写的时候都是独立的,或者复用的老逻辑,里面就是有独立的查询。
如果要做优化就只能将老的方法重载一个,将需要的信息直接传递过去。
public void xxx(int goodsId) {
Goods goods = goodsService.get(goodsId);
.....
}
public void xxx(Goods goods) {
.....
}
加缓存
如果你的业务场景允许数据有一定延迟,那么重复调用你可以直接通过加缓存来解决。这样的好处在于不会重复查询数据库,而是直接从缓存中取数据。
更大的好处在于对于优化类的影响最小,原有的代码逻辑都不用改变,只需要在查询的方法上加注解进行缓存即可。
public void xxx(int goodsId) {
Goods goods = goodsService.get(goodsId);
.....
}
public void xxx(Goods goods) {
Goods goods = goodsService.get(goodsId);
.....
}
class GoodsService {
@Cached(expire = 10, timeUnit = TimeUnit.SECONDS)
public Goods get(int goodsId) {
return dao.findById(goodsId);
}
}
如果你的业务场景不允许有缓存的话,上面这个方法就不能用了。那么是不是还得改代码,将需要的信息一层层往下传递呢?
自定义线程内的缓存
我们总结下目前的问题:
- 同一次请求内,多次相同的查询获取 RPC 等的调用。
- 数据实时性要求高,不适合加缓存,主要是加缓存也不好设置过期时间,除非采用数据变更主动更新缓存的方式。
- 只需要在这一次请求里缓存即可,不影响其他地方。
- 不想改动已有代码。
总结后发现这个场景适合用 ThreadLocal 来传递数据,对已有代码改动量最小,而且也只对当前线程生效,不会影响其他线程。
public void xxx(int goodsId) {
Goods goods = ThreadLocal.get();
if (goods == null) {
goods = goodsService.get(goodsId);
}
.....
}
上面代码就是使用了 ThreadLocal 来获取数据,如果有的话就直接使用,不用去重新查询,没有的话就去查询,不影响老逻辑。
虽然能实现效果,但是不太好,不够优雅。也不够通用,如果一次请求内要缓存多种类型的数据怎么处理? ThreadLocal 就不能存储固定的类型。还有就是老的逻辑还是得改,加了个判断。
下面介绍一种比较优雅的方式:
- 自定义缓存注解,加在查询的方法上。
- 定义切面切到加了缓存注解的方法上,第一次获取返回值存入 ThreadLocal。第二次直接从 ThreadLocal 中取值返回。
- ThreadLocal 中存储 Map,Key 为某方法的某一标识,这样可以缓存多种类型的结果。
- 在 Filter 中将 ThreadLocal 进行 remove 操作,因为线程是复用的,使用完需要清空。
注意:ThreadLocal 不能跨线程,如果有跨线程需求,请使用阿里的 ttl 来装饰。
注解定义
@Target({ ElementType.METHOD })
@Retention(RetentionPolicy.RUNTIME)
public @interface ThreadLocalCache {
/**
* 缓存key,支持SPEL表达式
* @return
*/
String key() default "";
}
存储定义
/**
* 线程内缓存管理
*
* @作者 尹吉欢
* @时间 2020-07-12 10:47
*/
public class ThreadLocalCacheManager {
private static ThreadLocal<Map> threadLocalCache = new ThreadLocal<>();
public static void setCache(Map value) {
threadLocalCache.set(value);
}
public static Map getCache() {
return threadLocalCache.get();
}
public static void removeCache() {
threadLocalCache.remove();
}
public static void removeCache(String key) {
Map cache = threadLocalCache.get();
if (cache != null) {
cache.remove(key);
}
}
}
切面定义
/**
* 线程内缓存
*
* @作者 尹吉欢
* @时间 2020-07-12 10:48
*/
@Aspect
public class ThreadLocalCacheAspect {
@Around(value = "@annotation(localCache)")
public Object aroundAdvice(ProceedingJoinPoint joinpoint, ThreadLocalCache localCache) throws Throwable {
Object[] args = joinpoint.getArgs();
Method method = ((MethodSignature) joinpoint.getSignature()).getMethod();
String className = joinpoint.getTarget().getClass().getName();
String methodName = method.getName();
String key = parseKey(localCache.key(), method, args, getDefaultKey(className, methodName, args));
Map cache = ThreadLocalCacheManager.getCache();
if (cache == null) {
cache = new HashMap();
}
Map finalCache = cache;
Map<String, Object> data = new HashMap<>();
data.put("methodName", className + "." + methodName);
Object cacheResult = CatTransactionManager.newTransaction(() -> {
if (finalCache.containsKey(key)) {
return finalCache.get(key);
}
return null;
}, "ThreadLocalCache", "CacheGet", data);
if (cacheResult != null) {
return cacheResult;
}
return CatTransactionManager.newTransaction(() -> {
Object result = null;
try {
result = joinpoint.proceed();
} catch (Throwable throwable) {
throw new RuntimeException(throwable);
}
finalCache.put(key, result);
ThreadLocalCacheManager.setCache(finalCache);
return result;
}, "ThreadLocalCache", "CachePut", data);
}
private String getDefaultKey(String className, String methodName, Object[] args) {
String defaultKey = className + "." + methodName;
if (args != null) {
defaultKey = defaultKey + "." + JsonUtils.toJson(args);
}
return defaultKey;
}
private String parseKey(String key, Method method, Object[] args, String defaultKey){
if (!StringUtils.hasText(key)) {
return defaultKey;
}
LocalVariableTableParameterNameDiscoverer nameDiscoverer = new LocalVariableTableParameterNameDiscoverer();
String[] paraNameArr = nameDiscoverer.getParameterNames(method);
ExpressionParser parser = new SpelExpressionParser();
StandardEvaluationContext context = new StandardEvaluationContext();
for(int i = 0;i < paraNameArr.length; i++){
context.setVariable(paraNameArr[i], args[i]);
}
try {
return parser.parseExpression(key).getValue(context, String.class);
} catch (SpelEvaluationException e) {
// 解析不出SPEL默认为类名+方法名+参数
return defaultKey;
}
}
}
过滤器定义
/**
* 线程缓存过滤器
*
* @作者 尹吉欢
* @个人微信 jihuan900
* @微信公众号 猿天地
* @GitHub https://github.com/yinjihuan
* @作者介绍 http://cxytiandi.com/about
* @时间 2020-07-12 19:46
*/
@Slf4j
public class ThreadLocalCacheFilter implements Filter {
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
filterChain.doFilter(servletRequest, servletResponse);
// 执行完后清除缓存
ThreadLocalCacheManager.removeCache();
}
}
自动配置类
@Configuration
public class ThreadLocalCacheAutoConfiguration {
@Bean
public FilterRegistrationBean idempotentParamtFilter() {
FilterRegistrationBean registration = new FilterRegistrationBean();
ThreadLocalCacheFilter filter = new ThreadLocalCacheFilter();
registration.setFilter(filter);
registration.addUrlPatterns("/*");
registration.setName("thread-local-cache-filter");
registration.setOrder(1);
return registration;
}
@Bean
public ThreadLocalCacheAspect threadLocalCacheAspect() {
return new ThreadLocalCacheAspect();
}
}
使用案例
@Service
public class TestService {
/**
* ThreadLocalCache 会缓存,只对当前线程有效
* @return
*/
@ThreadLocalCache
public String getName() {
System.out.println("开始查询了");
return "yinjihaun";
}
/**
* 支持SPEL表达式
* @param id
* @return
*/
@ThreadLocalCache(key = "#id")
public String getName(String id) {
System.out.println("开始查询了");
return "yinjihaun" + id;
}
}
功能代码: https://github.com/yinjihuan/kitty
案例代码: https://github.com/yinjihuan/kitty-samples
关于作者 :尹吉欢,简单的技术爱好者,《Spring Cloud 微服务-全栈技术与案例解析》, 《Spring Cloud 微服务 入门 实战与进阶》作者, 公众号 猿天地 发起人。个人微信 jihuan900 ,欢迎勾搭。
简直骚操作,ThreadLocal还能当缓存用的更多相关文章
- 用Markdown写Html和.md也就图一乐,真骚操作还得用来做PPT
前言 和这篇文章一样,我就是用Markdown写的.相信各位平时也就用Markdown写写文档,做做笔记,转成XHtml.Html等,今天教大伙一招骚操作:用Markdown写PPT. 绝大多数朋友做 ...
- Java 12 骚操作, String居然还能这样玩!
Java 13 都快要来了,12必须跟栈长学起! Java 13 即将发布,新特性必须抢先看! 栈长之前在Java技术栈微信公众号分享过<Java 11 已发布,String 还能这样玩!> ...
- Java 12 骚操作, 文件比对居然还能这样玩!
Java 13 都快要来了,12必须跟栈长学起! Java 13 即将发布,新特性必须抢先看! 之前分享了一些 Java 12 的骚操作,今天继续,今天要分享的是 Java 12 中的文件比对骚操作. ...
- 聊聊redis实际运用及骚操作
前言 聊起 redis 咱们大部分后端猿应该都不陌生,或多或少都用过.甚至大部分前端猿都知道. 数据结构: string. hash. list. set (无序集合). setsorted(有序集合 ...
- 你没玩过的全新版本!Win10这些骚操作你知多少
你没玩过的全新版本!Win10这些骚操作你知多少 [PConline技巧]不知不觉,Win10与我们相伴已经整整四个年头了,从最开始的组团抗拒到现在的默默接受,个中滋味相信谁心里都有个数.近日微软开始 ...
- Git科普文,Git基本原理&各种骚操作
Git简单介绍 Git是一个分布式版本控制软件,最初由Linus Torvalds创作,于2005年以GPL发布.最初目的是为更好地管理Linux内核开发而设计. Git工作流程以及各个区域 Work ...
- Guava中这些Map的骚操作,让我的代码量减少了50%
原创:微信公众号 码农参上,欢迎分享,转载请保留出处. Guava是google公司开发的一款Java类库扩展工具包,内含了丰富的API,涵盖了集合.缓存.并发.I/O等多个方面.使用这些API一方面 ...
- 教你一招用 IDE 编程提升效率的骚操作!
阅读本文大概需要 3 分钟. IDEA 有个很牛逼的功能,那就是后缀补全(不是自动补全),很多人竟然不知道这个操作,还在手动敲代码. 这个功能可以使用代码补全来模板式地补全语句,如遍历循环语句(for ...
- 闪电侠 Netty 小册里的骚操作
前言 即使这是一本小册,但基于"不提笔不读书"的理念,仍然有必要总结一下.此小册对于那些"硬杠 Netty 源码 却不曾在千万级生产环境上使用实操"的用户非常有 ...
随机推荐
- C#中String与byte[]的相互转换
从文件中读取字符串 string filePath = @"C:\Temp.xml"; string xmlString= File.ReadAllText(filePath); ...
- Go Pentester - HTTP Servers(1)
HTTP Server Basics Use net/http package and useful third-party packages by building simple servers. ...
- Ethical Hacking - NETWORK PENETRATION TESTING(4)
Targeted packet sniffing airodump-ng --channel[channel] --bssid[bssid] --write[file-name][interface] ...
- python-study-文件操作
# 一.文件操作的作用 :读取内容.写入内容.备份内容.... # 文件的基本操作,文件操作包含:打开.关闭.读.写.复制.... # 打开 读写 关闭 # 文件备份 # 文件和文件夹的操作 # 总结 ...
- 工程能力UP | LightGBM的调参干货教程与并行优化
这是个人在竞赛中对LGB模型进行调参的详细过程记录,主要包含下面六个步骤: 大学习率,确定估计器参数n_estimators/num_iterations/num_round/num_boost_ro ...
- php必须掌握的常用函数
数学函数 数组函数 字符串函数
- Python3网络爬虫开发实战PDF高清完整版免费下载|百度云盘
百度云盘:Python3网络爬虫开发实战高清完整版免费下载 提取码:d03u 内容简介 本书介绍了如何利用Python 3开发网络爬虫,书中首先介绍了环境配置和基础知识,然后讨论了urllib.req ...
- nginx 的return配置
该指令一般用于对请求的客户端直接返回响应状态码.在该作用域内return后面的所有nginx配置都是无效的. 可以使用在server.location以及if配置中. 除了支持跟状态码,还可以跟字符串 ...
- Python 为什么要有 pass 语句?
本文出自"Python为什么"系列,请查看全部文章 关于 Python 中的pass语句,它似乎很简单(只有 4 个字母),即使是没有任何编程经验的初学者也能很快地掌握它的用法. ...
- Webpack 原理浅析
作者: 凹凸曼 - 风魔小次郎 背景 Webpack 迭代到4.x版本后,其源码已经十分庞大,对各种开发场景进行了高度抽象,阅读成本也愈发昂贵.但是为了了解其内部的工作原理,让我们尝试从一个最简单的 ...