简直骚操作,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 源码 却不曾在千万级生产环境上使用实操"的用户非常有 ...
随机推荐
- Spring AOP底层实现分析
Spring AOP代理对象的生成 Spring提供了两种方式来生成代理对象: JdkProxy和Cglib,具体使用哪种方式生成由AopProxyFactory根据AdvisedSupport对象的 ...
- [Qt2D绘图]-02坐标系统&&抗锯齿渲染
本节的内容可以在帮助中通过Coordinate System关键字查看. 或者入门可以看<Qt Creator 快速入门>这本书.强烈推荐入门使用.下面的内容为本书的阅读笔记,喜欢的可以买 ...
- 前端框架-jQuery自学笔记
What's jQuery jq就是一个封装了很多方法的js库. Why use jQuery 原生js的缺点 不能添加多个入口函数(window.onload),如果添加多个,后面会把前面的覆盖 a ...
- GPO - Windows Server Update Services
Windows Server Update Services Configuration Wizard: Approve procedure of these updates is very tiri ...
- Python Ethical Hacking - TROJANS Analysis(3)
BYPASSING ANTI-VIRUS PROGRAMS AV programs detect viruses based on: 1. Code - compare files to huge d ...
- ajax原生js封装
不带注释的 function ajax(json) { json.type = json.type ? json.type : 'get'; json.async = json.async == fa ...
- STL Stack(栈)学习笔记 + 洛谷 P1449 后缀表达式
稍微看了看刘汝佳的白皮书,“实用主义”的STL实在是香到我了,而且在实验室大佬的推荐下我开始了stl的学习. 每篇附带一个题目方便理解,那行,直接开始. 毕竟是实用主义,所以就按照给的题目的例子来理解 ...
- 2.UDP协议
UDP只在IP数据报服务之上增加了很少功能,即复用分用和差错检测功能. 应用层给UDP多长的报文,UDP就照样发送,即一次发送一个完整报文 一.UDP首部格式 这里的长度是指(首部+数据) UDP校验 ...
- MacOS系统隐藏文件操作
显示或关闭隐藏文件 显示:defaults write com.apple.finder AppleShowAllFiles -bool true ; killall Finder隐藏:default ...
- 【Java面试】- 并发容器篇
JDK 提供的并发容器 ConcurrentHashMap: 线程安全的 HashMap CopyOnWriteArrayList: 线程安全的 List,在读多写少的场合性能非常好,远远好于 Vec ...