鱼C论坛

 找回密码
 立即注册
查看: 378|回复: 1

[技术交流] SpringBoot如何缓存方法返回值?

[复制链接]
发表于 2023-11-29 09:49:01 | 显示全部楼层 |阅读模式

马上注册,结交更多好友,享用更多功能^_^

您需要 登录 才可以下载或查看,没有账号?立即注册

x
本帖最后由 陈浩楠 于 2023-11-29 09:54 编辑

[toc]


Why?

为什么要对方法的返回值进行缓存呢?


简单来说是为了提升后端程序的性能和提高前端程序的访问速度。减小对db和后端应用程序的压力。


一般而言,缓存的内容都是不经常变化的,或者轻微变化对于前端应用程序是可以容忍的。


否则,不建议加入缓存,因为增加缓存会使程序复杂度增加,还会出现一些其他的问题,比如缓存同步,数据一致性,更甚者,可能出现经典的缓存穿透、缓存击穿、缓存雪崩问题。


HowDo

如何缓存方法的返回值?应该会有很多的办法,本文简单描述两个比较常见并且比较容易实现的办法:


•自定义注解
•SpringCache


annotation

整体思路:


第一步:定义一个自定义注解,在需要缓存的方法上面添加此注解,当调用该方法的时候,方法返回值将被缓存起来,下次再调用的时候将不会进入该方法。其中需要指定一个缓存键用来区分不同的调用,建议为:类名+方法名+参数名


第二步:编写该注解的切面,根据缓存键查询缓存池,若池中已经存在则直接返回不执行方法;若不存在,将执行方法,并在方法执行完毕写入缓冲池中。方法如果抛异常了,将不会创建缓存


第三步:缓存池,首先需要尽量保证缓存池是线程安全的,当然了没有绝对的线程安全。其次为了不发生缓存臃肿的问题,可以提供缓存释放的能力。另外,缓存池应该设计为可替代,比如可以丝滑得在使用程序内存和使用redis直接调整。


MethodCache

创建一个名为MethodCache 的自定义注解



package com.ramble.methodcache.annotation;
import java.lang.annotation.*;

@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD})
public @interface MethodCache {

}



MethodCacheAspect

编写MethodCache注解的切面实现



package com.ramble.methodcache.annotation;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.stereotype.Component;
import java.util.Arrays;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

@Slf4j
@Aspect
@Component
public class MethodCacheAspect {

    private static final Map<String, Object> CACHE_MAP = new ConcurrentHashMap<>();
   
    @Around(value = "@annotation(methodCache)")
    public Object around(ProceedingJoinPoint jp, MethodCache methodCache) throws Throwable {
        String className = jp.getSignature().getDeclaringType().getSimpleName();
        String methodName = jp.getSignature().getName();
        String args = String.join(",", Arrays.toString(jp.getArgs()));
        String key = className + ":" + methodName + ":" + args;
        // key 示例:DemoController:findUser:[FindUserParam(id=1, name=c7)]
        log.debug("缓存的key={}", key);
        Object cache = getCache(key);
        if (null != cache) {
            log.debug("走缓存");
            return cache;
        } else {
            log.debug("不走缓存");
            Object value = jp.proceed();
            setCache(key, value);
            return value;
        }
    }
   
    private Object getCache(String key) {
        return CACHE_MAP.get(key);
    }
   
    private void setCache(String key, Object value) {
        CACHE_MAP.put(key, value);
    }
}



&#8226;Around:对被MethodCache注解修饰的方法启用环绕通知
&#8226;ProceedingJoinPoint:通过此对象获取方法所在类、方法名和参数,用来组装缓存key
&#8226;CACHE_MAP:缓存池,生产环境建议使用redis等可以分布式存储的容器,直接放程序内存不利于后期业务扩张后多实例部署


controller


package com.ramble.methodcache.controller;
import com.ramble.methodcache.annotation.MethodCache;
import com.ramble.methodcache.controller.param.CreateUserParam;
import com.ramble.methodcache.controller.param.FindUserParam;
import com.ramble.methodcache.service.DemoService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.*;

@Tag(name = "demo - api")
@Slf4j
@RequiredArgsConstructor
@RestController
@RequestMapping("/demo")
public class DemoController {

    private final DemoService demoService;
   
    @MethodCache
    @GetMapping("/{id}")
    public String getUser(@PathVariable("id") String id) {
        return demoService.getUser(id);
    }
   
    @Operation(summary = "查询用户")
    @MethodCache
    @PostMapping("/list")
    public String findUser(@RequestBody FindUserParam param) {
        return demoService.findUser(param);
    }
}



通过反复调用被@MethodCache注解修饰的方法,会发现若缓存池有数据,将不会进入方法体。


SpringCache

其实SpringCache的实现思路和上述方法基本一致,SpringCache提供了更优雅的编程方式,更丝滑的缓存池切换和管理,更强大的功能和统一规范。


EnableCaching

使用 @EnableCaching 开启SpringCache功能,无需引入额外的pom。


默认情况下,缓存池将由 ConcurrentMapCacheManager 这个对象管理,也就是默认是程序内存中缓存。其中用于存放缓存数据的是一个 ConcurrentHashMap,源码如下:



public class ConcurrentMapCacheManager implements CacheManager, BeanClassLoaderAware {

    private final ConcurrentMap<String, Cache> cacheMap = new ConcurrentHashMap(16);
   
   
    ......
   
}



此外可选的缓存池管理对象还有:


&#8226;EhCacheCacheManager
&#8226;JCacheCacheManager
&#8226;RedisCacheManager
&#8226;......


Cacheable


package com.ramble.methodcache.controller;
import com.ramble.methodcache.controller.param.FindUserParam;
import com.ramble.methodcache.service.DemoService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.web.bind.annotation.*;

@Tag(name = "user - api")
@Slf4j
@RequiredArgsConstructor
@RestController
@RequestMapping("/user")
public class UserController {

    private final DemoService demoService;
   
    @Cacheable(value = "userCache")
    @GetMapping("/{id}")
    public String getUser(@PathVariable("id") String id) {
        return demoService.getUser(id);
    }
   
    @Operation(summary = "查询用户")
    @Cacheable(value = "userCache")
    @PostMapping("/list")
    public String findUser(@RequestBody FindUserParam param) {
        return demoService.findUser(param);
    }
}



&#8226;使用@Cacheable注解修饰需要缓存返回值的方法
&#8226;value必填,不然运行时报异常。类似一个分组,将不同的数据或者方法(当然也可以其他维度,主要看业务需要)放到一堆,便于管理
&#8226;可以修饰接口方法,但是不建议,IDEA会报一个提示Spring doesn't recommend to annotate interface methods with @Cache* annotation


常用属性:


&#8226;value:缓存名称
&#8226;cacheNames:缓存名称。value 和cacheNames都被AliasFor注解修饰,他们互为别名
&#8226;key:缓存数据时候的key,默认使用方法参数的值,可以使用SpEL生产key
&#8226;keyGenerator:key生产器。和key二选一
&#8226;cacheManager:缓存管理器
&#8226;cacheResolver:和caheManager二选一,互为别名
&#8226;condition:创建缓存的条件,可用SpEL表达式(如#id>0,表示当入参id大于0时候才缓存方法返回值)
&#8226;unless:不创建缓存的条件,如#result==null,表示方法返回值为null的时候不缓存


CachePut

用来更新缓存。被CachePut注解修饰的方法,在被调用的时候不会校验缓存池中是否已经存在缓存,会直接发起调用,然后将返回值放入缓存池中。


CacheEvict

用来删除缓存,会根据key来删除缓存中的数据。并且不会将本方法返回值缓存起来。


常用属性:


&#8226;value/cacheeName:缓存名称,或者说缓存分组
&#8226;key:缓存数据的键
&#8226;allEntries:是否根据缓存名称清空所有缓存,默认为false。当此值为true的时候,将根据cacheName清空缓存池中的数据,然后将新的返回值放入缓存
&#8226;beforeInvocation:是否在方法执行之前就清空缓存,默认为false


Caching

此注解用于在一个方法或者类上面,同时指定多个SpringCache相关注解。这个也是SpringCache的强大之处,可以自定义各种缓存创建、更新、删除的逻辑,应对复杂的业务场景。


属性:


&#8226;cacheable:指定@Cacheable注解
&#8226;put:指定@CachePut注解
&#8226;evict:指定@CacheEvict注解


源码:



@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
public @interface Caching {
    Cacheable[] cacheable() default {};

    CachePut[] put() default {};

    CacheEvict[] evict() default {};
}


相当于就是注解里面套注解,用来完成复杂和多变的场景,这个设计相当的哇塞。


CacheConfig

放在类上面,那么类中所有方法都会被缓存


SpringCacheEnv

SpringCache内置了一些环境变量,可用于各个注解的属性中。


&#8226;methodName:被修饰方法的方法名
&#8226;method:被修饰方法的Method对象
&#8226;target:被修饰方法所属的类对象的实例
&#8226;targetClass:被修饰方法所属类对象
&#8226;args:方法入参,是一个 object[] 数组
&#8226;caches:这个对象其实就是ConcurrentMapCacheManager中的cacheMap,这个cacheMap呢就是一开头提到的ConcurrentHashMap,即缓存池。caches的使用场景尚不明了。
&#8226;argumentName:方法的入参
&#8226;result:方法执行的返回值


使用示例:



@Cacheable(value = "userCache", condition = "#result!=null",unless = "#result==null")
public String showEnv() {
    return "打印各个环境变量";
}


表示仅当方法返回值不为null的时候才缓存结果,这里通过result env 获取返回值。


另外,condition 和 unless 为互补关系,上述condition = "#result!=null"和unless = "#result==null"其实是一个意思。



@Cacheable(value = "userCache", key = "#name")
public String showEnv(String id, String name) {
    return "打印各个环境变量";
}


表示使用方法入参作为该条缓存数据的key,若传入的name为gg,则实际缓存的数据为:gg->打印各个环境变量


另外,如果name为空会报异常,因为缓存key不允许为null



@Cacheable(value = "userCache",key = "#root.args")
public String showEnv(String id, String name) {
    return "打印各个环境变量";
}


表示使用方法的入参作为缓存的key,若传递的参数为id=100,name=gg,则实际缓存的数据为:Object[]->打印各个环境变量,Object[]数组中包含两个值。


既然是数组,可以通过下标进行访问,root.args[1] 表示获取第二个参数,本例中即 取 name 的值 gg,则实际缓存的数据为:gg->打印各个环境变量。



@Cacheable(value = "userCache",key = "#root.targetClass")
public String showEnv(String id, String name) {
    return "打印各个环境变量";
}


表示使用被修饰的方法所属的类作为缓存key,实际缓存的数据为:Class->打印各个环境变量,key为class对象,不是全限定名,全限定名是一个字符串,这里是class对象。


可是,不是很懂这样设计的应用场景是什么......



@Cacheable(value = "userCache",key = "#root.target")
public String showEnv(String id, String name) {
    return "打印各个环境变量";
}


表示使用被修饰方法所属类的实例作为key,实际缓存的数据为:UserController->打印各个环境变量。


被修饰的方法就是在UserController中,调试的时候甚至可以获取到此实例注入的其它容器对象,如userService等。


可是,不是很懂这样设计的应用场景是什么......



@Cacheable(value = "userCache",key = "#root.method")
public String showEnv(String id, String name) {
    return "打印各个环境变量";
}


表示使用Method对象作为缓存的key,是Method对象,不是字符串。


可是,不是很懂这样设计的应用场景是什么......



@Cacheable(value = "userCache",key = "#root.methodName")
public String showEnv(String id, String name) {
    return "打印各个环境变量";
}


表示使用方法名作为缓存的key,就是一个字符串。


如何获取缓存的数据?


ConcurrentMapCacheManager的cacheMap是一个私有变量,所以没有办法可以打印缓存池中的数据,不过可以通过调试的方式进入对象内部查看。如下:



@Tag(name = "user - api")
@Slf4j
@RequiredArgsConstructor
@RestController
@RequestMapping("/user")
public class UserController {

    private final ConcurrentMapCacheManager cacheManager;
   
    /**
     * 只有调试才课可以查看缓存池中的数据
     */
    @GetMapping("/cache")
    public void showCacheData() {
        //需要debug进入
        Collection<String> cacheNames = cacheManager.getCacheNames();
    }
   
}



总结:


虽然提供了很多的环境变量,但是大多都无法找到对应的使用场景,其实在实际开发中,最常见的就是key的生产,一般而言使用类名+方法名+参数值足矣。


SqEL

参考:
想知道小甲鱼最近在做啥?请访问 -> ilovefishc.com
回复

使用道具 举报

发表于 2023-11-29 09:49:30 | 显示全部楼层
Spring Boot提供了多种缓存方法返回值的方式,其中比较常见且容易实现的有自定义注解和使用Spring Cache。

1. 使用自定义注解:
   - 首先,需要定义一个自定义注解。可以命名为`@MethodCache`。
   - 将此注解标记在需要缓存返回值的方法上,当调用该方法时,方法的返回值将被缓存起来。
   - 编写一个切面类,通过切面拦截带有`@MethodCache`注解的方法,并进行缓存处理。
   - 在切面中,根据方法的参数生成一个唯一的缓存键,然后查询缓存池。
   - 如果缓存池中已存在对应的缓存值,则直接返回缓存值,否则执行方法并将返回值放入缓存池中。

示例代码如下:
// 自定义注解@Documented@Retention(RetentionPolicy.RUNTIME)@Target({ElementType.METHOD})public @interface MethodCache {}// 切面类@Aspect@Componentpublic class MethodCacheAspect {    private static final Map<String, Object> CACHE_MAP = new ConcurrentHashMap<>();    @Around("@annotation(methodCache)")    public Object around(ProceedingJoinPoint pjp, MethodCache methodCache) throws Throwable {        String key = generateCacheKey(pjp);        Object cache = getFromCache(key);        if (cache != null) {            return cache;        } else {            Object result = pjp.proceed();            putIntoCache(key, result);            return result;        }    }    private String generateCacheKey(ProceedingJoinPoint pjp) {        String className = pjp.getSignature().getDeclaringType().getSimpleName();        String methodName = pjp.getSignature().getName();        String params = Arrays.toString(pjp.getArgs());        return className + ":" + methodName + ":" + params;    }    private Object getFromCache(String key) {        return CACHE_MAP.get(key);    }    private void putIntoCache(String key, Object value) {        CACHE_MAP.put(key, value);    }}// 使用注解标记需要缓存的方法@RestControllerpublic class MyController {    @MethodCache    @GetMapping("/cached")    public String getCachedData() {        // 从数据库或其他数据源中获取数据        return "Cached Data";    }}
2. 使用Spring Cache:
   - 首先,在启动类上添加`@EnableCaching`注解开启Spring Cache功能。
   - 在需要缓存返回值的方法上使用`@Cacheable`注解,指定缓存的名称和key。
   - Spring Cache会自动在方法调用时,根据缓存名称和key来查找对应的缓存值,如果存在则直接返回,否则执行方法并将返回值放入缓存中。

示例代码如下:
@SpringBootApplication@EnableCachingpublic class MyApplication {    public static void main(String[] args) {        SpringApplication.run(MyApplication.class, args);    }}@RestControllerpublic class MyController {    @Cacheable(value = "myCache", key = "'cached_data'")    @GetMapping("/cached")    public String getCachedData() {        // 从数据库或其他数据源中获取数据        return "Cached Data";    }}
在上述示例中,`@Cacheable`注解指定了缓存的名称为`myCache`,key为`cached_data`。当调用`/cached`接口时,如果缓存中存在`cached_data`的缓存值,则直接返回该值;否则,执行方法并将返回值放入缓存中。

需要注意的是,使用Spring Cache时,需要配置一个缓存管理器(`CacheManager`),用来管理缓存池和缓存策略。常用的缓存管理器有`ConcurrentMapCacheManager`、`EhCacheCacheManager`、`RedisCacheManager`等,具体选择哪种管理器根据项目需求而定。

这两种方法都可以实现对方法返回值的缓存,选择哪种方式取决于具体场景和需求,以及个人偏好。

以上回复来自 -- ChatGPT(FishC官方接口),如未能正确解答您的问题,请继续追问。
想知道小甲鱼最近在做啥?请访问 -> ilovefishc.com
回复 支持 反对

使用道具 举报

您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

小黑屋|手机版|Archiver|鱼C工作室 ( 粤ICP备18085999号-1 | 粤公网安备 44051102000585号)

GMT+8, 2024-11-21 21:31

Powered by Discuz! X3.4

© 2001-2023 Discuz! Team.

快速回复 返回顶部 返回列表