ThreadLocal can also be used as a cache

Background description

A friend asked me a question about interface optimization. His optimization point is very clear. Because many internal services are called in the interface to form a completed business function. The logic in each service is independent, which leads to many repeated queries. See the figure below.

The upper level query is passed down

For this scenario, the best way is to query the required data at the upper level and then transfer it to the lower level for consumption. So you don't have to query repeatedly.

If you do this when you start writing code, it's OK, but many times, when you write code before, it's independent, or reuse the old logic, which has independent queries.

If you want to optimize, you can only overload one of the old methods and pass the required information directly to the past.

public void xxx(int goodsId) {
    Goods goods = goodsService.get(goodsId);
    .....
}
public void xxx(Goods goods) {
    .....
}

Add cache

If your business scenario allows a certain delay in data, you can solve the problem of repeated calls directly by adding cache. The advantage of this is that the database will not be queried repeatedly, but the data will be fetched directly from the cache.

The greater advantage is that it has the least impact on the optimization class. The original code logic does not need to be changed. You only need to annotate the query method for caching.

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

If your business scenario does not allow caching, the above method will not work. So is it necessary to change the code and transfer the required information layer by layer?

Customize caching within threads

Let's summarize the current problems:

  1. In the same request, multiple times of the same query to obtain calls such as RPC.
  2. The real-time requirement of data is high, which is not suitable for adding cache. It is mainly that adding cache is not good to set the expiration time, unless the method of actively updating cache by data change is adopted.
  3. You only need to cache in this request without affecting other places.
  4. Don't want to change the existing code.

After summarizing, it is found that this scenario is suitable for using ThreadLocal to transfer data. The amount of changes to the existing code is the smallest, and it is only effective for the current thread and will not affect other threads.

public void xxx(int goodsId) {
    Goods goods = ThreadLocal.get();
    if (goods == null) {
        goods = goodsService.get(goodsId);
    }
    .....
}

The above code uses ThreadLocal to obtain data. If any, it can be used directly without re querying. If not, it can be queried without affecting the old logic.

Although it can achieve the effect, it is not very good and elegant. It's not universal enough. What if you want to cache multiple types of data in one request? ThreadLocal cannot store fixed types. In addition, the old logic still has to be changed and a judgment has to be added.

Here is an elegant way:

  1. Custom cache annotations are added to the query method.
  2. The defined section is cut to the method with cache annotation, and the return value is obtained for the first time and stored in ThreadLocal. The second time, the value is returned directly from ThreadLocal.
  3. The Map is stored in ThreadLocal, and the Key is an ID of a method, which can cache various types of results.
  4. remove ThreadLocal in the Filter because threads are reused and need to be emptied after use.

Note: ThreadLocal cannot cross thread. If there is a cross thread requirement, please use Alibaba's ttl to decorate it.

Annotation definition

@Target({ ElementType.METHOD })
@Retention(RetentionPolicy.RUNTIME)
public @interface ThreadLocalCache {
    /**
     * Cache key, support SPEL expression
     * @return
     */
    String key() default "";
}

Storage definition

/**
 * In thread cache management
 *
 * @Author Yin Jihuan
 * @Time: 10:47, July 12, 2020
 */
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);
        }
    }
}

Section definition

/**
 * In thread cache
 *
 * @Author Yin Jihuan
 * @Time: 10:48, July 12, 2020
 */
@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) {
            // The SPEL cannot be resolved. The default is class name + method name + parameter
            return defaultKey;
        }
    }
}

Filter definition

/**
 * Thread cache filter
 *
 * @Author Yin Jihuan
 * @Personal wechat jihuan900
 * @Wechat official account ape world
 * @GitHub https://github.com/yinjihuan
 * @Author introduction http://cxytiandi.com/about
 * @Time: 19:07-2020
 */
@Slf4j
public class ThreadLocalCacheFilter implements Filter {
    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
        filterChain.doFilter(servletRequest, servletResponse);
        // Clear cache after execution
        ThreadLocalCacheManager.removeCache();
    }
}

Auto configuration class

@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();
    }
}

Use case

@Service
public class TestService {
    /**
     * ThreadLocalCache Will cache, valid only for the current thread
     * @return
     */
    @ThreadLocalCache
    public String getName() {
        System.out.println("Start the query");
        return "yinjihaun";
    }
    /**
     * SPEL expressions are supported
     * @param id
     * @return
     */
    @ThreadLocalCache(key = "#id")
    public String getName(String id) {
        System.out.println("Start the query");
        return "yinjihaun" + id;
    }
}

Function code: https://github.com/yinjihuan/kitty

Case code: https://github.com/yinjihuan/kitty-samples

About the author: yinjihuan, a simple technology enthusiast, author of "Spring Cloud microservice - full stack technology and case analysis", author of "Spring Cloud microservice introduction practice and advanced", and initiator of official account ape world. Personal wechat jihuan900, welcome to hook up.

Tags: Java

Posted by AJReading on Mon, 23 May 2022 16:17:23 +0300