【第22章】spring-mvc之缓存


前言

自3.1版本以来,Spring Framework提供了对向现有Spring应用程序透明添加缓存的支持。与事务支持类似,缓存抽象允许在对代码影响最小的情况下一致使用各种缓存解决方案。

在SpringFramework4.1中,缓存抽象得到了显著扩展,支持JSR-107注释和更多自定义选项。


一、缓存抽象

缓存抽象的核心是将缓存应用于Java方法,从而减少了基于缓存中可用信息的执行次数。也就是说,每次调用目标方法时,抽象都会应用缓存行为来检查是否已经为给定的参数调用了该方法。如果已经调用了它,则返回缓存的结果,而不必调用实际的方法。如果尚未调用该方法,则会调用它,并缓存结果并返回给用户,以便下次调用该方法时,返回缓存的结果。这样,对于给定的一组参数,昂贵的方法(无论是CPU绑定还是IO绑定)只能调用一次,并且结果可以重用,而不必再次实际调用该方法。缓存逻辑的应用是透明的,不会对调用程序造成任何干扰。

要使用缓存抽象,您需要注意两个方面:

  • 缓存声明:确定需要缓存的方法及其策略。
  • 缓存配置:存储数据和从中读取数据的后备缓存。

二、启用注解

1. Java Config

@Configuration
@EnableCaching
public class AppConfig {

	@Bean
	CacheManager cacheManager() {
		CaffeineCacheManager cacheManager = new CaffeineCacheManager();
		cacheManager.setCacheSpecification(...);
		return cacheManager;
	}
}

2. xml配置

<beans xmlns="http://www.springframework.org/schema/beans"
	xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xmlns:cache="http://www.springframework.org/schema/cache"
	xsi:schemaLocation="
		http://www.springframework.org/schema/beans https://www.springframework.org/schema/beans/spring-beans.xsd
		http://www.springframework.org/schema/cache https://www.springframework.org/schema/cache/spring-cache.xsd">

	<cache:annotation-driven/>

	<bean id="cacheManager" class="org.springframework.cache.caffeine.CaffeineCacheManager">
		<property name="cacheSpecification" value="..."/>
	</bean>
</beans>

三、缓存注解

Spring提供了几个注解来简化缓存的使用:

  • @Cacheable:表示方法的结果是可缓存的。如果缓存中存在结果,则直接返回缓存的结果,而不执行方法。
  • @CachePut:无论方法是否被调用过,都会将其结果存入缓存。常用于更新缓存。
  • @CacheEvict:用于从缓存中移除一个或多个条目。
  • @Caching:允许你在一个方法上使用多个缓存注解。
  • @CacheConfig:允许你在类级别共享一些公共的缓存配置。

1. @Cacheable

顾名思义,您可以使用@Cacheable来划分可缓存的方法 — 也就是说,将结果存储在缓存中的方法,以便在后续调用(使用相同的参数)时,返回缓存中的值,而不必实际调用该方法。在最简单的形式中,注释声明需要与注释方法关联的缓存的名称,如下例所示:

1.1 单个缓存

@Cacheable("books")
public Book findBook(ISBN isbn) {...}

1.2 多个缓存

@Cacheable({"books", "isbns"})
public Book findBook(ISBN isbn) {...}

2.key生成器

2.1 默认key生成器

由于缓存本质上是键值存储,因此缓存方法的每次调用都需要转换为用于缓存访问的合适的键。缓存抽象使用基于以下算法的简单KeyGenerator:

  • 如果未给定任何参数,则返回SimpleKey。空的。

  • 如果只给定一个参数,则返回该实例。

  • 如果给定了多个参数,则返回一个包含所有参数的SimpleKey。

这种方法适用于大多数用例,只要参数具有自然键并实现有效的hashCode()和equals()方法。如果不是这样,你需要改变策略。
要提供不同的默认密钥生成器,您需要实现org.springframework.cache.interceptor。KeyGenerator接口。

2.2 自定义key生成器

key

你可以使用SpEL(Spring Expression Language)来动态生成缓存键。在@Cacheable@CachePut@CacheEvict注解的key属性中,你可以引用方法参数、方法返回值或其他对象属性。

@Cacheable(cacheNames="books", key="#isbn")
public Book findBook(ISBN isbn, boolean checkWarehouse, boolean includeUsed)

@Cacheable(cacheNames="books", key="#isbn.rawNumber")
public Book findBook(ISBN isbn, boolean checkWarehouse, boolean includeUsed)

@Cacheable(cacheNames="books", key="T(someType).hash(#isbn)")
public Book findBook(ISBN isbn, boolean checkWarehouse, boolean includeUsed)

keyGenerator

如果负责生成密钥的算法过于特定,或者需要共享密钥,则可以在操作上定义自定义密钥生成器。为此,请指定要使用的KeyGenerator bean实现的名称,如下例所示:

@Cacheable(cacheNames="books", keyGenerator="myKeyGenerator")
public Book findBook(ISBN isbn, boolean checkWarehouse, boolean includeUsed)

key和keyGenerator参数是互斥的,同时指定这两个参数的操作会导致异常。

3. 缓存解析器

3. 1 默认缓存解析器

缓存抽象使用一个简单的CacheResolver,通过使用配置的CacheManager检索在操作级别定义的缓存。

要提供不同的默认缓存解析程序,您需要实现org.springframework.cache.interceptor.CacheResolver接口。

3. 2 自定义缓存解析器

默认缓存分辨率非常适合使用单个CacheManager并且没有复杂缓存分辨率要求的应用程序。
对于使用多个缓存管理器的应用程序,可以将cacheManager设置为用于每个操作,如下例所示:

@Cacheable(cacheNames=“books”,cacheManager=“anotherCacheManager”)
public Book findBook(ISBN ISBN){…}

指定另一个CacheManager。

您也可以以类似于替换密钥生成的方式完全替换CacheResolver。解析是为每个缓存操作请求的,使实现能够根据运行时参数实际解析要使用的缓存。以下示例显示了如何指定CacheResolver:

@Cacheable(cacheResolver=“runtimeCacheResolver”)
public Book findBook(ISBN ISBN){…}

与key和keyGenerator类似,cacheManager和cacheResolver参数是互斥的,指定这两个参数的操作会导致异常,因为自定义cacheManager会被cacheResolver实现忽略。这可能不是你所期望的。

4. 同步缓存

同步缓存
在多线程环境中,某些操作可能会同时调用同一个参数(通常在启动时)。默认情况下,缓存抽象不会锁定任何内容,并且可能会多次计算相同的值,从而破坏缓存的目的。
对于这些特定情况,可以使用sync属性指示基础缓存提供程序在计算值时锁定缓存项。因此,只有一个线程忙于计算值,而其他线程则被阻塞,直到缓存中的条目被更新。以下示例显示了如何使用sync属性:

@Cacheable(cacheNames="foos", sync=true) 
public Foo executeExpensiveOperation(String id) {...}

5. 异步返回缓存

从6.1开始,缓存注释将CompletableFuture和反应式返回类型考虑在内,从而相应地自动调整缓存交互。

@Cacheable("books")
public CompletableFuture<Book> findBook(ISBN isbn) {...}

更多类型请查看官方文档

6. 条件缓存

有时,一个方法可能不适合一直缓存(例如,它可能取决于给定的参数)。缓存注释通过条件参数支持此类用例,条件参数采用一个计算为true或false的SpEL表达式。如果为true,则缓存该方法。如果没有,它的行为就好像没有缓存该方法一样(也就是说,无论缓存中有什么值或使用了什么参数,每次都会调用该方法)。例如,只有当参数名称的长度小于32时,才会缓存以下方法:

@Cacheable(cacheNames="book", condition="#name.length() < 32") 
public Book findBook(String name)

你可以使用conditionunless属性来根据某些条件来决定是否缓存结果或更新缓存。这些属性接受SpEL表达式。

7. 可用缓存SpEL

字段名位置描述示例
methodName根对象被调用的方法的名称#root.methodName
method根对象被调用的方法本身(可能是一个Java的Method对象)#root.method.name
target根对象被调用的目标对象#root.target
targetClass根对象被调用的目标的类#root.targetClass
args根对象用于调用目标的参数(作为数组)#root.args[0]
caches根对象当前方法运行所依赖的缓存集合#root.caches[0].name
Argument name上下文方法参数的名称(或索引)#iban#a0
result上下文方法调用的结果(要缓存的值)#result

8. @CachePut

当需要在不干扰方法执行的情况下更新缓存时,可以使用@CachePut注释。也就是说,方法总是被调用,其结果被放入缓存中

@CachePut(cacheNames="book", key="#isbn")
public Book updateBook(ISBN isbn, BookDescriptor descriptor)

9. @CacheEvict

缓存逐出,此过程对于从缓存中删除过时或未使用的数据非常有用。具有一个额外的参数(allEntries),该参数指示是否需要执行缓存范围的逐出,而不仅仅是(基于密钥的)条目逐出。以下示例从图书缓存中收回所有条目:

@CacheEvict(cacheNames="books", allEntries=true) 
public void loadBooks(InputStream batch)

10. @Caching

有时,需要指定多个相同类型的注释(如@CacheEvict或@CachePut) — 例如,因为不同缓存之间的条件或密钥表达式不同@缓存允许在同一方法上使用多个嵌套的@Cacheable、@CachePut和@CacheEvict注释。以下示例使用了两个@CacheEvict注释:

@Caching(evict = { @CacheEvict("primary"), @CacheEvict(cacheNames="secondary", key="#p0") })
public Book importBooks(String deposit, Date date)

11. @CacheConfig

@CacheConfig是一个类级注释,允许共享缓存名称、自定义KeyGenerator、自定义CacheManager和自定义CacheResolver。将此注释放置在类上不会启用任何缓存操作。

@CacheConfig("books") 
public class BookRepositoryImpl implements BookRepository {

	@Cacheable
	public Book findBook(ISBN isbn) {...}
}

设置CacheManager bean的属性,也是全局的。
操作级自定义项总是覆盖@CacheConfig上的自定义项集。因此,这为每个缓存操作提供了三个级别的自定义:

  • 全局配置,例如通过CachingConfigurer。
  • 在类级别,使用@CacheConfig。
  • 在操作层面。

四、案例

接下来,让我们通过一个简单的案例来学习spring缓存的使用。

1. 启用缓存

@Configuration
@EnableCaching
public class WebConfig implements WebMvcConfigurer {
}

2. 定义缓存管理器

@Bean
CacheManager cacheManager() {
    return new ConcurrentMapCacheManager();
}

3. User

package org.example.springmvc.params.entity;

import com.alibaba.fastjson2.annotation.JSONField;
import com.alibaba.fastjson2.annotation.JSONType;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;
import org.springframework.format.annotation.DateTimeFormat;
import java.util.Date;
import java.util.List;
import java.util.Map;

/**
 * Create by zjg on 2024/4/27
 */
@Getter
@Setter
@ToString
@JSONType(ignores = "relatives")
@EqualsAndHashCode
public class User {
    private int id;
    private String name;
    @DateTimeFormat(pattern = "yyyy-MM-dd")
    @JSONField(format = "yyyy-MM-dd")
    private Date birth;
    private String[]alias;//外号
    private List<String>hobbies;//爱好
    private Map<String,String>relatives;//家人们
    private Role role;//角色
    private List<User> friends;//朋友们
}

重点是要重写@EqualsAndHashCode

4. 控制器

package org.example.springmvc.cache.controller;

import org.example.springmvc.cache.service.CacheService;
import org.example.springmvc.params.entity.User;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

/**
 * Create by zjg on 2024/5/13
 */
@RestController
@RequestMapping("/cache/")
public class CacheController {
    @Autowired
    CacheService cacheService;
    @RequestMapping("001")
    public String cache001(){
        return cacheService.service001();
    }
    @RequestMapping("002")
    public String cache002(int id){
        return cacheService.service002(id);
    }
    @RequestMapping("003")
    public String cache003(int id,String name){
        return cacheService.service003(id,name);
    }
    @RequestMapping("004")
    public String cache004(int id,String name,int age){
        return cacheService.service004(id,name,age);
    }
    @RequestMapping("005")
    public String cache005(User user){
        return cacheService.service005(user);
    }
    @RequestMapping("006")
    public String cache006(){
        return cacheService.service006();
    }
}

5. 缓存服务

package org.example.springmvc.cache.service;

import org.example.springmvc.params.entity.User;

/**
 * Create by zjg on 2024/5/13
 */
public interface CacheService {
    String service001();
    String service002(int id);
    String service003(int id,String name);
    String service004(int id,String name,int age);
    String service005(User user);
    String service006();
}

package org.example.springmvc.cache.service.impl;

import org.example.springmvc.cache.service.CacheService;
import org.example.springmvc.params.entity.User;
import org.springframework.cache.annotation.CacheConfig;
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.stereotype.Service;

/**
 * Create by zjg on 2024/5/13
 */
@Service
@CacheConfig(cacheNames= CacheServiceImpl.CACHE_NAMES)
public class CacheServiceImpl implements CacheService {
    public static final String CACHE_NAMES="cacheNames";
    @Override
    @Cacheable
    public String service001() {//无参数
        System.out.println("service001执行了!");
        return "success";
    }

    @Override
    @Cacheable
    public String service002(int id) {//单个参数
        System.out.println("service002执行了!");
        return "success";
    }

    @Override
    @Cacheable
    public String service003(int id,String name) {//多个参数
        System.out.println("service003执行了!");
        return "success";
    }

    @Override
    @Cacheable(key = "#id+#name")//"#a0+#a1"
    public String service004(int id, String name,int age) {//多个参数(取前两个参数作为key,第三个值不参与缓存匹配)
        System.out.println("service004执行了!");
        return "success";
    }

    @Override
    @Cacheable
    public String service005(User user) {//复杂对象
        System.out.println("service005执行了!");
        return "success";
    }

    @Override
    @CacheEvict(allEntries=true)
    public String service006() {//这里清空缓存
        System.out.println("service006执行了!");
        return "success";
    }
}


总结

回到顶部
请注意,虽然ConcurrentMapCacheManager提供了一种简单的内存缓存解决方案,但在处理大量数据或需要持久化缓存的场景中,可能需要考虑使用更复杂的缓存解决方案,如基于Redis或EhCache的缓存管理器。

使用reids也可以轻松实现方法级别的缓存,且对于集群部署支持更多友好。

postman测试数据已上传附件。

  • 4
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值