chapter10:SpringBoot与缓存

尚硅谷SpringBoot整合教程

1. JSR107

缓存开发规范,Java Caching 定义了5个核心接口, 分别是CachingProvider,CacheManager,Cache,Entry和Expiry。

  • CachingProvider:定义了创建,配置,获取,管理和控制多个CacheManager。一个应用可以在运行期访问多个CachingProvider.

  • CacheManager:定义了创建,配置,获取,管理和控制多个唯一命名的Cache,这些Cache存在于CacheManger的上下文中, 一个CacheManager仅被一个CachingProvider所拥有。

  • Cache:是一个类似Map的数据结构并临时存储以key为索引的值,一个Cache仅被一个CacheManger所拥有。

  • Entry: 是一个存储在Cache中的key-value对。

  • Expiry:每一个存储在Cache中的条目有一个定义的有效期。一旦超过这个时间,条目为过期的状态。一旦过期,条目将不可访问,更新和删除。缓存有效期可以通过ExpiryPolicy设置。

chapter10:SpringBoot与缓存_第1张图片

chapter10:SpringBoot与缓存_第2张图片

2. Spring缓存抽象

Spring从3.1开始定义了org.springframework.cache.Cache和org.springframework.cache.CacheManager接口来统一不同的缓存技术;并支持使用JCache(JSR-107)注解简化我们的开发。

  • Cache接口为缓存的组件规范定义,包含缓存的各种操作集合;
  • Cache接口下Spring提供了各种xxxCache的实现;如RedisCache,ConcurrentMapCache等;
  • 每次调用需要缓存的方法时,Spring会检查指定参数的指定目标方法是否已经被调用过,如果有就直接从缓存中获取方法调用后的结果,如果没有就调用方法并缓存结果返回给用户,下次调用直接从缓存中获取;
  • 使用Spring缓存抽象时我们需要关注以下两点;
    • 确定方法需要被缓存以及他们的缓存策略;
    • 从缓存中读取之前缓存存储的数据;

几个重要概念及缓存注解:

关键词或注解 解释
Cache 缓存接口,定义缓存操作;实现有:RedisCache,EncacheCache,ConcurrentMapCache等;
CacheManager 缓存管理器,管理各种缓存(Cache)组件;
@Cacheable 主要针对方法配置,能够根据方法的请求参数对其结果进行缓存
@CacheEvict 清空缓存
@CachePut 更新缓存
@EnableCaching 开启基于注解的缓存
keyGenerator 缓存数据时key生成策略
serialize 缓存数据时value序列化策略

Cache SpEL Available metadata
chapter10:SpringBoot与缓存_第3张图片

3. 使用缓存

3.1 搭建基本环境

导入数据库文件spring_cache.sql,创建department和employee表

CREATE TABLE `department` (
  `id` int NOT NULL AUTO_INCREMENT COMMENT '部门ID',
  `department_name` varchar(300) COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '部门名称',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;

cREATE TABLE `employee` (
  `id` int NOT NULL AUTO_INCREMENT COMMENT '员工ID',
  `d_id` int DEFAULT NULL COMMENT '部门ID',
  `lastName` varchar(50) DEFAULT NULL COMMENT '姓名',
  `gender` int DEFAULT NULL COMMENT '性别',
  `email` varchar(20) DEFAULT NULL COMMENT '邮箱',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;

创建SpringBoot项目 springboot01-cache ,导入相关依赖。


<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0modelVersion>
    <parent>
        <groupId>org.springframework.bootgroupId>
        <artifactId>spring-boot-starter-parentartifactId>
        <version>1.5.10.RELEASEversion>
        <relativePath/> 
    parent>
    <groupId>cn.cryswgroupId>
    <artifactId>springboot01-cacheartifactId>
    <version>0.0.1-SNAPSHOTversion>
    <name>springboot01-cachename>
    <description>springboot01-cachedescription>
    <properties>
        <java.version>1.8java.version>
    properties>
    <dependencies>
        <dependency>
            <groupId>org.springframework.bootgroupId>
            <artifactId>spring-boot-starter-webartifactId>
        dependency>
        <dependency>
            <groupId>org.mybatis.spring.bootgroupId>
            <artifactId>mybatis-spring-boot-starterartifactId>
            <version>1.3.5version>
        dependency>


        <dependency>
            <groupId>javax.cachegroupId>
            <artifactId>cache-apiartifactId>
            <version>1.1.1version>
        dependency>

        

        <dependency>
            <groupId>mysqlgroupId>
            <artifactId>mysql-connector-javaartifactId>
            <scope>runtimescope>
            <version>8.0.12version>
        dependency>

        <dependency>
            <groupId>org.projectlombokgroupId>
            <artifactId>lombokartifactId>
            <optional>trueoptional>
        dependency>
        <dependency>
            <groupId>org.springframework.bootgroupId>
            <artifactId>spring-boot-starter-testartifactId>
            <scope>testscope>
        dependency>

    dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.bootgroupId>
                <artifactId>spring-boot-maven-pluginartifactId>
                <configuration>
                    <excludes>
                        <exclude>
                            <groupId>org.projectlombokgroupId>
                            <artifactId>lombokartifactId>
                        exclude>
                    excludes>
                configuration>
            plugin>
        plugins>
    build>

project>

3.2 完善用例

创建javaBean封装数据, 因为缓存中的对象数据会序列化和反序列化,所以JavaBean需要实现序列化接口。

@Data
@AllArgsConstructor
@NoArgsConstructor
@Builder
public class Department implements Serializable {
    private Integer id;
    private String departmentName;
}


@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class Employee implements Serializable {
    private Integer id;
    private String lastName;
    private String email;
    /**
     * 性别 1-男,0-女
     */
    private Integer gender;
    /**
     * 部门ID
     */
    private Integer dId;
}

整合myBatis操作数据库, 在全局配置文件添加相关配置。

# 配置数据源
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.url=jdbc:mysql://localhost:3306/spring_cache?useSSL=false
spring.datasource.username=root
spring.datasource.password=123456
# 开启驼峰映射
mybatis.configuration.map-underscore-to-camel-case=true
# debug
debug=true
logging.level.cn.crysw.cache.mapper=debug

# redis
#spring.redis.host=192.168.111.129

编写mapper接口

@Mapper
public interface DepartmentMapper {
    @Select("select id, department_name as \"departmentName\" from department where id = #{id}")
    Department getDeptById(Integer id);
}

@Mapper
public interface EmployeeMapper {

    @Select(value = "select * from employee where id=#{id}")
    Employee getEmpById(Integer id);

    @Update(value = "update employee set lastName=#{lastName}, email=#{email}, gender=#{gender}, d_id=#{dId} where id=#{id}")
    void updateEmp(Employee employee);

    @Delete(value = "delete from employee where id=#{id}")
    void deleteEmpById(Integer id);

    @Insert(value = "insert into employee(lastName,email,gender, d_id) values(#{lastName},#{email},#{gender},#{dId})")
    void insertEmp(Employee employee);

    @Select(value = "select * from employee where lastName=#{name}")
    Employee getEmpByLastName(String name);
}

3.3 缓存步骤

主启动类上开启基于注解的缓存配置 @EnableCaching

@SpringBootApplication
@MapperScan(value = "cn.crysw.cache.mapper")
@EnableCaching
public class Springboot01CacheApplication {
    public static void main(String[] args) {
        SpringApplication.run(Springboot01CacheApplication.class, args);
    }
}

标注缓存注解 @Cacheable @CachePut @CacheEvict @Caching @CacheConfig;

3.3.1 注解@Cacheable

作用:将方法的运行结果进行缓存。

注解@Cacheable的几个属性介绍:

  • cacheNames/value:指定缓存组件的名字
  • key:缓存数据使用的key,默认是使用方法参数的值;编写SpEL: #id 参数id的值
  • keyGenerator:key的生成器,可以自己指定key的生成器; key和keyGenerator二选一使用
  • cacheManager:指定缓存管理器
  • cacheResolver:指定缓存解析器,cacheResolver和cacheManager二选一使用;
  • condition:指定符合条件的情况下才缓存; condition = “#id>0” 表示参数id大于0时才缓存数据;
  • unless: 否定缓存,当unless指定的条件为true,方法的返回值就不会缓存,可以获取到结果进行判断操作;unless = “#resultnull" 返回结果是null不缓存,unless="#a02” 表示第一个参数的值是2,结果就不缓存
  • sync: 使用异步模式, 使用比较少;
@Service
public class EmployeeService {

    @Autowired
    private EmployeeMapper employeeMapper;
    // 没指定key或keyGenerator,系统默认按照参数为key缓存数据,后面分析原理的时候看源码
    @Cacheable(cacheNames = {"emp"}/*,*/
            /*key = "#root.methodName+'['+#id+']'",*/
            /* keyGenerator = "myKeyGenerator",*/
            /* condition = "#id>1",*/
            /* unless = "#result==null", cacheManager = "employeeRedisCacheManager"*/)
    public Employee getEmp(Integer id) {
        System.out.println("查询" + id + "号员工");
        Employee employee = employeeMapper.getEmpById(id);
        return employee;
    }
}    

通过controller调用当前service类的接口,第一调用是查询的db,第二次是从缓存中读取。

查询2号员工
2023-07-10 23:04:24.273 DEBUG 22104 --- [nio-8080-exec-4] // 查db
c.c.c.mapper.EmployeeMapper.getEmpById   : ==>  Preparing: select * from employee where id=? 
2023-07-10 23:04:24.288 DEBUG 22104 --- [nio-8080-exec-4] c.c.c.mapper.EmployeeMapper.getEmpById   : ==> Parameters: 2(Integer)
2023-07-10 23:04:24.301 DEBUG 22104 --- [nio-8080-exec-4] c.c.c.mapper.EmployeeMapper.getEmpById   : <==      Total: 1
2023-07-10 23:04:24.316 DEBUG 22104 --- [nio-8080-exec-4]  
// 保存数据到缓存
o.s.b.w.f.OrderedRequestContextFilter    : Cleared thread-bound request context: org.apache.catalina.connector.RequestFacade@45847bd3
// 查缓存
2023-07-10 23:04:32.058 DEBUG 22104 --- [nio-8080-exec-1] o.s.b.w.f.OrderedRequestContextFilter    : Bound request context to thread: org.apache.catalina.connector.RequestFacade@45847bd3
2023-07-10 23:04:32.062 DEBUG 22104 --- [nio-8080-exec-1] o.s.b.w.f.OrderedRequestContextFilter    : Cleared thread-bound request context: org.apache.catalina.connector.RequestFacade@45847bd3

3.3.2 注解@CachePut

作用:更新数据时更新缓存

更新员工信息的同时更新缓存, 如果没有指定key,会以返回的employee对象作为key进行缓存,会导致再次查询还是更新之前的缓存数据,因为更新的key和查询时缓存到cache中的key不一致; @CachePut 应该使用查询缓存时的key,查询使用的是参数id作为key,更新可以使用#employee.id或返回结果#result.id.

@Service
public class EmployeeService {

    @Autowired
    private EmployeeMapper employeeMapper;
    
    @CachePut(cacheNames = {"emp"}, key = "#result.id", condition = "#employee.id>1")
    public Employee updateEmp(Employee employee) {
        System.out.println("updateEmp:" + employee);
        employeeMapper.updateEmp(employee);
        return employee;
    }
}    

3.3.3 注解@CacheEvict

作用:清除指定的缓存数据。

  • key:指定要清除的数据
  • allEntries=true : 删除这个缓存的所有数据
  • beforeInvocation=false: 默认是在方法执行之后执行, 如果方法执行异常,缓存就不会清除;
  • beforeInvocation=true,缓存的清除是否在方法执行之前执行,无论方法执行是否异常,缓存都会清除;
@Service
public class EmployeeService {

    @Autowired
    private EmployeeMapper employeeMapper;
    
    @CacheEvict(cacheNames = {"emp"}, key = "#id", allEntries = true)
    public void deleteEmp(Integer id) {
        System.out.println("deleteEmp: " + id);
        employeeMapper.deleteEmpById(id);
    }
}    

执行效果就是,先查询触发数据保存到缓存中; deleteEmp删除数据后再次调用查询接口,数据是从db读取而不是缓存了。

2023-07-10 23:21:34.108  INFO 14292 --- [nio-8080-exec-1] o.a.c.c.C.[Tomcat].[localhost].[/]       : Initializing Spring FrameworkServlet 'dispatcherServlet' // 派发请求
2023-07-10 23:21:34.108  INFO 14292 --- [nio-8080-exec-1] o.s.web.servlet.DispatcherServlet        : FrameworkServlet 'dispatcherServlet': initialization started
2023-07-10 23:21:34.118  INFO 14292 --- [nio-8080-exec-1] o.s.web.servlet.DispatcherServlet        : FrameworkServlet 'dispatcherServlet': initialization completed in 10 ms
2023-07-10 23:21:34.124 DEBUG 14292 --- [nio-8080-exec-1] o.s.b.w.f.OrderedRequestContextFilter    : Bound request context to thread: org.apache.catalina.connector.RequestFacade@488c71d6
查询2号员工 // 第一次请求getEmp查询db,然后将数据存入缓存中
2023-07-10 23:21:40.722 DEBUG 14292 --- [nio-8080-exec-1]  c.c.c.mapper.EmployeeMapper.getEmpById   : ==>  Preparing: select * from employee where id=? 
2023-07-10 23:21:40.736 DEBUG 14292 --- [nio-8080-exec-1] c.c.c.mapper.EmployeeMapper.getEmpById   : ==> Parameters: 2(Integer)
2023-07-10 23:21:40.748 DEBUG 14292 --- [nio-8080-exec-1] c.c.c.mapper.EmployeeMapper.getEmpById   : <==      Total: 1
2023-07-10 23:21:40.784 DEBUG 14292 --- [nio-8080-exec-1] 
// 数据存入缓存中
o.s.b.w.f.OrderedRequestContextFilter    : Cleared thread-bound request context: org.apache.catalina.connector.RequestFacade@488c71d6
// 第二次请求getEmp, 读取的缓存中的数据
2023-07-10 23:21:48.810 DEBUG 14292 --- [nio-8080-exec-4] o.s.b.w.f.OrderedRequestContextFilter    : Bound request context to thread: org.apache.catalina.connector.RequestFacade@488c71d6
2023-07-10 23:21:48.814 DEBUG 14292 --- [nio-8080-exec-4] o.s.b.w.f.OrderedRequestContextFilter    : Cleared thread-bound request context: org.apache.catalina.connector.RequestFacade@488c71d6
// 请求delEmp,先删除缓存数据
2023-07-10 23:22:05.588 DEBUG 14292 --- [nio-8080-exec-5] o.s.b.w.f.OrderedRequestContextFilter    : Bound request context to thread: org.apache.catalina.connector.RequestFacade@488c71d6
deleteEmp: 2 // 再删除db数据
2023-07-10 23:22:05.590 DEBUG 14292 --- [nio-8080-exec-5] c.c.c.m.EmployeeMapper.deleteEmpById     : ==>  Preparing: delete from employee where id=? 
2023-07-10 23:22:05.590 DEBUG 14292 --- [nio-8080-exec-5] c.c.c.m.EmployeeMapper.deleteEmpById     : ==> Parameters: 2(Integer)
2023-07-10 23:22:05.597 DEBUG 14292 --- [nio-8080-exec-5] c.c.c.m.EmployeeMapper.deleteEmpById     : <==    Updates: 1
2023-07-10 23:22:05.599 DEBUG 14292 --- [nio-8080-exec-5] o.s.b.w.f.OrderedRequestContextFilter    : Cleared thread-bound request context: org.apache.catalina.connector.RequestFacade@488c71d6
// 再次请求getEmp,先去缓存读取,没有数据
2023-07-10 23:22:14.877 DEBUG 14292 --- [nio-8080-exec-7] o.s.b.w.f.OrderedRequestContextFilter    : Bound request context to thread: org.apache.catalina.connector.RequestFacade@488c71d6
查询2号员工 // 上面被删除了,缓存中没有读取到数据,重新读取db数据并缓存数据
2023-07-10 23:22:14.879 DEBUG 14292 --- [nio-8080-exec-7] c.c.c.mapper.EmployeeMapper.getEmpById   : ==>  Preparing: select * from employee where id=? 
2023-07-10 23:22:14.879 DEBUG 14292 --- [nio-8080-exec-7] c.c.c.mapper.EmployeeMapper.getEmpById   : ==> Parameters: 2(Integer)
2023-07-10 23:22:14.880 DEBUG 14292 --- [nio-8080-exec-7] c.c.c.mapper.EmployeeMapper.getEmpById   : <==      Total: 0
2023-07-10 23:22:14.881 DEBUG 14292 --- [nio-8080-exec-7] 
// 读取db数据后,进行数据缓存
o.s.b.w.f.OrderedRequestContextFilter    : Cleared thread-bound request context: org.apache.catalina.connector.RequestFacade@488c71d6

3.3.4 注解@Caching

定义复杂的缓存规则, 可以同时指定缓存数据,更新缓存,删除缓存的配置。

@Service
public class EmployeeService {

    @Autowired
    private EmployeeMapper employeeMapper;
    
    @Caching(
            cacheable = {@Cacheable(value = "emp", key = "#lastName")},
            put = {@CachePut(value = "emp", key = "#result.id"),
                    @CachePut(value = "emp", key = "#result.email")
            },
            evict = {@CacheEvict(key = "#result.id")}
    )
    public Employee getEmpByLastName(String lastName) {
        Employee emp = employeeMapper.getEmpByLastName(lastName);
        System.out.println("getEmpByLastName: " + emp);
        return emp;
    }
}   

3.3.5 注解@CacheConfig

作用:为当前类统一指定缓存特性;

  • cacheNames : 指定缓存组件名称;
  • keyGenerator:指定缓存组件的key生成器;
  • cacheManager:指定缓存组件的缓存管理器;
  • cacheResolver:指定缓存组件的缓存解析器;
@CacheConfig(cacheNames = {"emp"})
@Service
public class EmployeeService {

    @Autowired
    private EmployeeMapper employeeMapper;
    
    @Cacheable
    public Employee getEmp(Integer id) {
        System.out.println("查询" + id + "号员工");
        Employee employee = employeeMapper.getEmpById(id);
        return employee;
    }
}    

4. 缓存的配置原理

4.1 CacheAutoConfiguration

SpringBoot提供了对SpringCache的自动配置支持, 相关源码在自动配置类CacheAutoConfiguration中,里面导入了下面这些缓存配置类。

org.springframework.boot.autoconfigure.cache.GenericCacheConfiguration

org.springframework.boot.autoconfigure.cache.GenericCacheConfiguration

org.springframework.boot.autoconfigure.cache.EhCacheCacheConfiguration

org.springframework.boot.autoconfigure.cache.HazelcastCacheConfiguration

org.springframework.boot.autoconfigure.cache.InfinispanCacheConfiguration

org.springframework.boot.autoconfigure.cache.CouchbaseCacheConfiguration

org.springframework.boot.autoconfigure.cache.RedisCacheConfiguration

org.springframework.boot.autoconfigure.cache.CaffeineCacheConfiguration

org.springframework.boot.autoconfigure.cache.GuavaCacheConfiguration

org.springframework.boot.autoconfigure.cache.SimpleCacheConfiguration

org.springframework.boot.autoconfigure.cache.NoOpCacheConfiguration

动配置类CacheAutoConfiguration的源码:

// 1
@Configuration
@ConditionalOnClass(CacheManager.class)
@ConditionalOnBean(CacheAspectSupport.class)
@ConditionalOnMissingBean(value = CacheManager.class, name = "cacheResolver")
@EnableConfigurationProperties(CacheProperties.class)
@AutoConfigureBefore(HibernateJpaAutoConfiguration.class)
@AutoConfigureAfter({ CouchbaseAutoConfiguration.class, HazelcastAutoConfiguration.class,
		RedisAutoConfiguration.class })
@Import(CacheConfigurationImportSelector.class)
public class CacheAutoConfiguration {
    // 。。。省略部分代码
	// 导入配置
	static class CacheConfigurationImportSelector implements ImportSelector {

		@Override
		public String[] selectImports(AnnotationMetadata importingClassMetadata) {
            // 获取所有支持的缓存配置类型
			CacheType[] types = CacheType.values();
			String[] imports = new String[types.length];
			for (int i = 0; i < types.length; i++) {
                // 根据支持的缓存配置类型获取对应的缓存配置类
				imports[i] = CacheConfigurations.getConfigurationClass(types[i]);
			}
			return imports;
		}
	}
}    

// 2
final class CacheConfigurations {

	private static final Map<CacheType, Class<?>> MAPPINGS;

	static {
		Map<CacheType, Class<?>> mappings = new HashMap<CacheType, Class<?>>();
		mappings.put(CacheType.GENERIC, GenericCacheConfiguration.class);
		mappings.put(CacheType.EHCACHE, EhCacheCacheConfiguration.class);
		mappings.put(CacheType.HAZELCAST, HazelcastCacheConfiguration.class);
		mappings.put(CacheType.INFINISPAN, InfinispanCacheConfiguration.class);
		mappings.put(CacheType.JCACHE, JCacheCacheConfiguration.class);
		mappings.put(CacheType.COUCHBASE, CouchbaseCacheConfiguration.class);
		mappings.put(CacheType.REDIS, RedisCacheConfiguration.class);
		mappings.put(CacheType.CAFFEINE, CaffeineCacheConfiguration.class);
		addGuavaMapping(mappings);
		mappings.put(CacheType.SIMPLE, SimpleCacheConfiguration.class);
		mappings.put(CacheType.NONE, NoOpCacheConfiguration.class);
        // 容器存入所有支持的缓存配置类
		MAPPINGS = Collections.unmodifiableMap(mappings);
	}

	@Deprecated
	private static void addGuavaMapping(Map<CacheType, Class<?>> mappings) {
		mappings.put(CacheType.GUAVA, GuavaCacheConfiguration.class);
	}

	private CacheConfigurations() {
	}

    //  根据缓存配置类型获取对应的缓存配置类
	public static String getConfigurationClass(CacheType cacheType) {
		Class<?> configurationClass = MAPPINGS.get(cacheType);
		Assert.state(configurationClass != null, "Unknown cache type " + cacheType);
		return configurationClass.getName();
	}

    // 根据缓存配置类的名称获取缓存配置类的类型
	public static CacheType getType(String configurationClassName) {
		for (Map.Entry<CacheType, Class<?>> entry : MAPPINGS.entrySet()) {
			if (entry.getValue().getName().equals(configurationClassName)) {
				return entry.getKey();
			}
		}
		throw new IllegalStateException(
				"Unknown configuration class " + configurationClassName);
	}
}

// 3
public enum CacheType {

	/**
	 * Generic caching using 'Cache' beans from the context.
	 */
	GENERIC,

	/**
	 * JCache (JSR-107) backed caching.
	 */
	JCACHE,

	/**
	 * EhCache backed caching.
	 */
	EHCACHE,

	/**
	 * Hazelcast backed caching.
	 */
	HAZELCAST,

	/**
	 * Infinispan backed caching.
	 */
	INFINISPAN,

	/**
	 * Couchbase backed caching.
	 */
	COUCHBASE,

	/**
	 * Redis backed caching.
	 */
	REDIS,

	/**
	 * Caffeine backed caching.
	 */
	CAFFEINE,

	/**
	 * Guava backed caching.
	 */
	@Deprecated GUAVA,

	/**
	 * Simple in-memory caching.
	 */
	SIMPLE,

	/**
	 * No caching.
	 */
	NONE;
}

4.2 SimpleCacheConfiguration

系统如果没有引入其他缓存组件,默认加载的是SimpleCacheConfiguration配置类, 开启debug的启动日志也可以看到匹配的配置类。

 SimpleCacheConfiguration matched:
      - Cache org.springframework.boot.autoconfigure.cache.SimpleCacheConfiguration automatic cache type (CacheCondition)

SimpleCacheConfiguration源码

@Configuration
@ConditionalOnMissingBean(CacheManager.class)
@Conditional(CacheCondition.class)
class SimpleCacheConfiguration {
	// 提供了spring.cache.xxx的配置项来修改缓存相关的配置属性值
	private final CacheProperties cacheProperties;
	// 缓存组件定制器
	private final CacheManagerCustomizers customizerInvoker;

	SimpleCacheConfiguration(CacheProperties cacheProperties,
			CacheManagerCustomizers customizerInvoker) {
		this.cacheProperties = cacheProperties;
		this.customizerInvoker = customizerInvoker;
	}

    // 配置了缓存管理器 ConcurrentMapCacheManager
	@Bean
	public ConcurrentMapCacheManager cacheManager() {
		ConcurrentMapCacheManager cacheManager = new ConcurrentMapCacheManager();
		List<String> cacheNames = this.cacheProperties.getCacheNames();
		if (!cacheNames.isEmpty()) {
            // 如果指定了缓存组件名称, 就创建缓存组件并设置缓存组件名称
            // ConcurrentMapCacheManager#setCacheNames
			cacheManager.setCacheNames(cacheNames);
		}
        // 调用缓存组件定制器的customize方法进行定制化处理,可以自定义CacheManagerCustomizer
		return this.customizerInvoker.customize(cacheManager);
	}
}

4.3 CacheManager

默认使用ConcurrentMapCacheManager缓存管理器将封装在ConcurrentMapCache的数据保存在ConcurrentMap cacheMap中;开发中使用缓存中间件 redis, memcached,ehcache。

public class ConcurrentMapCacheManager implements CacheManager, BeanClassLoaderAware {
	// 缓存容器
	private final ConcurrentMap<String, Cache> cacheMap = new ConcurrentHashMap<String, Cache>(16);
    private boolean dynamic = true;
    // 就设置缓存组件名称
    public void setCacheNames(Collection<String> cacheNames) {
		if (cacheNames != null) {
			for (String name : cacheNames) {
                // 创建缓存组件并存入cacheMap中
				this.cacheMap.put(name, createConcurrentMapCache(name));
			}
			this.dynamic = false;
		}
		else {
			this.dynamic = true;
		}
	}
    
    // 如果没有设置spring.cache.cacheNames, 在第一次请求getEmp接口时也会先从读取缓存
    @Override
	public Cache getCache(String name) {
		Cache cache = this.cacheMap.get(name);
        // 因为初始化时没有设置spring.cache.cacheNames, 此处获取的cache是null
		if (cache == null && this.dynamic) {
			synchronized (this.cacheMap) {
				cache = this.cacheMap.get(name);
				if (cache == null) {
                    // 系统会创建鉿缓存组件 ConcurrentMapCache
					cache = createConcurrentMapCache(name);
                    // 将缓存组件存入cacheMap中
					this.cacheMap.put(name, cache);
				}
			}
		}
		return cache;
	}
    
    // 创建缓存组件
	protected Cache createConcurrentMapCache(String name) {
		SerializationDelegate actualSerialization = (isStoreByValue() ? this.serialization : null);
        // new 缓存组件ConcurrentMapCache
		return new ConcurrentMapCache(name, new ConcurrentHashMap<Object, Object>(256),
				isAllowNullValues(), actualSerialization);

	}    
    
}    

4.4 ConcurrentMapCache

缓存组件ConcurrentMapCache

public class ConcurrentMapCache extends AbstractValueAdaptingCache {
    private final String name;
    // 缓存数据的ConcurrentMap容器
	private final ConcurrentMap<Object, Object> store;
    private final SerializationDelegate serialization;
    // 创建缓存组件
    protected ConcurrentMapCache(String name, ConcurrentMap<Object, Object> store,
			boolean allowNullValues, SerializationDelegate serialization) {

		super(allowNullValues);
		Assert.notNull(name, "Name must not be null");
		Assert.notNull(store, "Store must not be null");
		this.name = name;
		this.store = store;
		this.serialization = serialization;
	}
    
    // 先读取缓存
    @Override
	protected Object lookup(Object key) {
        // 从缓存容器中获取指定key的值
		return this.store.get(key);
	}

	@SuppressWarnings("unchecked")
	@Override
	public <T> T get(Object key, Callable<T> valueLoader) {
		if (this.store.containsKey(key)) {
			return (T) get(key).get();
		}
		else {
			synchronized (this.store) {
				if (this.store.containsKey(key)) {
					return (T) get(key).get();
				}
				T value;
				try {
					value = valueLoader.call();
				}
				catch (Throwable ex) {
					throw new ValueRetrievalException(key, valueLoader, ex);
				}
				put(key, value);
				return value;
			}
		}
	}

    // 缓存数据
	@Override
	public void put(Object key, Object value) {
        // 将数据缓存到store容器中
		this.store.put(key, toStoreValue(value));
	}
    
    // 删除指定的缓存
    @Override
	public void evict(Object key) {
		this.store.remove(key);
	}
	// 清除所有缓存
	@Override
	public void clear() {
		this.store.clear();
	}
}

AbstractValueAdaptingCache

public abstract class AbstractValueAdaptingCache implements Cache {
    // 先读取缓存
    @Override
	public ValueWrapper get(Object key) {
        // 走子类重写 ConcurrentMapCache#lookup
		Object value = lookup(key);
		return toValueWrapper(value);
	}
    
    protected abstract Object lookup(Object key);
    // 默认将数据封装到SimpleValueWrapper中
    protected Cache.ValueWrapper toValueWrapper(Object storeValue) {
		return (storeValue != null ? new SimpleValueWrapper(fromStoreValue(storeValue)) : null);
	}
}

public interface Cache {
    String getName();
    ValueWrapper get(Object key);
    // 缓存数据
    void put(Object key, Object value);
    // 更新缓存
    ValueWrapper putIfAbsent(Object key, Object value);
    // 删除指定key的缓存
    void evict(Object key);
    // 清除所有缓存
    void clear();
    
    interface ValueWrapper {

		/**
		 * Return the actual value in the cache.
		 */
		Object get();
	}
}

public class SimpleValueWrapper implements ValueWrapper {

	private final Object value;


	/**
	 * Create a new SimpleValueWrapper instance for exposing the given value.
	 * @param value the value to expose (may be {@code null})
	 */
	public SimpleValueWrapper(Object value) {
		this.value = value;
	}


	/**
	 * Simply returns the value as given at construction time.
	 */
	@Override
	public Object get() {
		return this.value;
	}

}

4.5 debug测试

启动应用,请求getEmp接口(http://localhost:8080/emp/1)

第一次请求接口,先按照配置的缓存名称emp查找缓存组件,第一次缓存里面肯定没有,读取结果cache是null,就会去创建缓存组件ConcurrentMapCache。
chapter10:SpringBoot与缓存_第4张图片
创建完缓存组件后,再次去缓存组件中根据请求参数id为key读取缓存数据, 查询结果也是null。
chapter10:SpringBoot与缓存_第5张图片

读取缓存结果为null后,再去执行db查询操作。
chapter10:SpringBoot与缓存_第6张图片

将执行db查询的结果存入缓存容器中,按照key=1进行缓存,也就是请求参数id=1的值进行的缓存,后面再介绍key为什么是请求参数id的值?
chapter10:SpringBoot与缓存_第7张图片
数据成功存入缓存后,再次请求 getEmp接口(http://localhost:8080/emp/1), 首先读取缓存就有数据了。
chapter10:SpringBoot与缓存_第8张图片

5. KeyGenerator

查看缓存数据的key生成。

CacheAspectSupport#findCachedItem

private Cache.ValueWrapper findCachedItem(Collection<CacheOperationContext> contexts) {
		Object result = CacheOperationExpressionEvaluator.NO_RESULT;
		for (CacheOperationContext context : contexts) {
			if (isConditionPassing(context, result)) {
                // 生成key
				Object key = generateKey(context, result);
				Cache.ValueWrapper cached = findInCaches(context, key);
				if (cached != null) {
					return cached;
				}
				else {
					if (logger.isTraceEnabled()) {
						logger.trace("No cache entry for key '" + key + "' in cache(s) " + context.getCacheNames());
					}
				}
			}
		}
		return null;
	}
 // 生成key
private Object generateKey(CacheOperationContext context, Object result) {
		Object key = context.generateKey(result);
		if (key == null) {
			throw new IllegalArgumentException("Null key returned for cache operation (maybe you are " +
					"using named params on classes without debug info?) " + context.metadata.operation);
		}
		if (logger.isTraceEnabled()) {
			logger.trace("Computed cache key '" + key + "' for operation " + context.metadata.operation);
		}
		return key;
	}
// 生成key
protected Object generateKey(Object result) {
			if (StringUtils.hasText(this.metadata.operation.getKey())) {
				EvaluationContext evaluationContext = createEvaluationContext(result);
				return evaluator.key(this.metadata.operation.getKey(), this.methodCacheKey, evaluationContext);
			}
			return this.metadata.keyGenerator.generate(this.target, this.metadata.method, this.args);
		}

生成的key是getEmp(String id)的参数id值,那这个key=2是如何生成的呢?下面SimpleKeyGenerator
chapter10:SpringBoot与缓存_第9张图片

然后根据key查找缓存

CacheAspectSupport#findInCaches

private Cache.ValueWrapper findInCaches(CacheOperationContext context, Object key) {
		for (Cache cache : context.getCaches()) {
            // 根据key获取缓存
			Cache.ValueWrapper wrapper = doGet(cache, key);
			if (wrapper != null) {
				if (logger.isTraceEnabled()) {
					logger.trace("Cache entry for key '" + key + "' found in cache '" + cache.getName() + "'");
				}
				return wrapper;
			}
		}
		return null;
	}

AbstractCacheInvoker#doGet

protected Cache.ValueWrapper doGet(Cache cache, Object key) {
		try {
     // 根据key获取缓存, 此处的cache.get(key)就是使用容器中默认的ConcurrentMapCache去查找缓存。
			return cache.get(key);
		}
		catch (RuntimeException ex) {
			getErrorHandler().handleCacheGetError(ex, cache, key);
			return null;  // If the exception is handled, return a cache miss
		}
	}

AbstractValueAdaptingCache#get 》》 ConcurrentMapCache#lookup, 从ConcurrentMap store容器中查找缓存数据。
chapter10:SpringBoot与缓存_第10张图片

系统默认使用SimpleKeyGenerator来生成key,查看源码。

public class SimpleKeyGenerator implements KeyGenerator {

	@Override
	public Object generate(Object target, Method method, Object... params) {
		return generateKey(params);
	}

	/**
	 * Generate a key based on the specified parameters.
	 */
	public static Object generateKey(Object... params) {
		if (params.length == 0) {
			return SimpleKey.EMPTY;
		}
		if (params.length == 1) {
            // 如果只有1个参数,就用当前参数作为key返回。所以上面的测试中key=id=2
			Object param = params[0];
			if (param != null && !param.getClass().isArray()) {
				return param;
			}
		}
        // 如果是多个参数,就以封装参数列表的SimpleKey对象为key返回
		return new SimpleKey(params);
	}
}

6. 自定义KeyGenerator

配置自定义KeyGenerator, 只需要实现KeyGenerator接口,重写generate方法,返回的就是自定义的key;

public interface KeyGenerator {

	/**
	 * Generate a key for the given method and its parameters.
	 * @param target the target instance
	 * @param method the method being called
	 * @param params the method parameters (with any var-args expanded)
	 * @return a generated key
	 */
	Object generate(Object target, Method method, Object... params);

}

lambda表达式风格简化了匿名内部类的实现。

/**
 * 自定义KeyGenerator
 *
 * @Author crysw
 * @Version 1.0
 * @Date 2023/6/24 22:10
 */
@Configuration
public class MyCacheConfig {

    @Bean(name = "myKeyGenerator")
    public KeyGenerator keyGenerator() {
        // 自定义key策略: 方法名 + [方法参数]
        return (Object target, Method method, Object... params) -> method.getName() + "[" + Arrays.asList(params) + "]";
    }
}

在缓存时,指定自定义的myKeyGenerator策略生成key进行数据缓存。

@Service
public class EmployeeService {

    @Autowired
    private EmployeeMapper employeeMapper;
    
    @Cacheable(cacheNames = {"emp"},
            /*key= "#root.methodName+'['+#id+']'",*/
           /*  key = "#id",*/ 
             keyGenerator = "myKeyGenerator",
            /* condition = "#id>1"*/
            unless = "#result==null")
    public Employee getEmp(Integer id) {
        System.out.println("查询" + id + "号员工");
        Employee employee = employeeMapper.getEmpById(id);
        return employee;
    }
}    

7. 整合Redis

引入Redis的starter依赖

<dependency>
    <groupId>org.springframework.bootgroupId>
    <artifactId>spring-boot-starter-data-redisartifactId>
    <version>1.5.10.RELEASEversion>
dependency>

导入Redis的自动配置依赖后,启动应用后匹配的就是RedisCacheConfiguration缓存配置注解。

RedisCacheConfiguration matched:
      - Cache org.springframework.boot.autoconfigure.cache.RedisCacheConfiguration automatic cache type (CacheCondition)

匹配上RedisCacheConfiguration配置,使用RedisCacheManager创建了RedisCache来作为缓存组件,RedisCache通过Redis操作缓存数据;可以类比SimpleCacheConfiguration#ConcurrentMapCacheManager

@Configuration
@AutoConfigureAfter(RedisAutoConfiguration.class)
@ConditionalOnBean(RedisTemplate.class)
@ConditionalOnMissingBean(CacheManager.class)
@Conditional(CacheCondition.class)
class RedisCacheConfiguration {

	private final CacheProperties cacheProperties;

	private final CacheManagerCustomizers customizerInvoker;

	RedisCacheConfiguration(CacheProperties cacheProperties,
			CacheManagerCustomizers customizerInvoker) {
		this.cacheProperties = cacheProperties;
		this.customizerInvoker = customizerInvoker;
	}

    // RedisCacheManager缓存管理器
	@Bean
	public RedisCacheManager cacheManager(RedisTemplate<Object, Object> redisTemplate) {
		RedisCacheManager cacheManager = new RedisCacheManager(redisTemplate);
		cacheManager.setUsePrefix(true);
		List<String> cacheNames = this.cacheProperties.getCacheNames();
		if (!cacheNames.isEmpty()) {
            // 将配置的spring.cache.cacheNames设置缓存名称 
			cacheManager.setCacheNames(cacheNames);
		}
		return this.customizerInvoker.customize(cacheManager);
	}
}

启动应用后,访问http://localhost:8080/emp/2,请求的是getEmp(id=2), 发现redis Desktop Manager客户端存入的数据是乱码, 是因为Redis默认使用的是JdkSerializationRedisSerializer序列化。
chapter10:SpringBoot与缓存_第11张图片

RedisTemplate

public class RedisTemplate<K, V> extends RedisAccessor implements RedisOperations<K, V>, BeanClassLoaderAware {
    
    private RedisSerializer<?> defaultSerializer;
    private boolean initialized = false;
    
    public void afterPropertiesSet() {
		super.afterPropertiesSet();
		boolean defaultUsed = false;
		if (defaultSerializer == null) {
            // 如果没有设置默认的序列化器,默认使用JDK序列化器
			defaultSerializer = new JdkSerializationRedisSerializer(
					classLoader != null ? classLoader : this.getClass().getClassLoader());
		}
		initialized = true;
	}
}

如果要缓存数据是我们能看懂的, 只需要自己配置其他序列化器,比如Jackson2JsonRedisSerializer

@Configuration
public class MyRedisConfig {

    /**
     * 自定义redisTemplate,使用Jackson2JsonRedisSerializer序列化器
     *
     * @param redisConnectionFactory
     * @return
     */

    @Bean
    public RedisTemplate<Object, Employee> employeeRedisTemplate(RedisConnectionFactory redisConnectionFactory) {
        // 序列化Employee对象
        RedisTemplate<Object, Employee> template = new RedisTemplate<>();
        template.setConnectionFactory(redisConnectionFactory);
        template.setDefaultSerializer(new Jackson2JsonRedisSerializer(Employee.class));
        return template;
    }

    @Bean
    public RedisTemplate<Object, Department> departmentRedisTemplate(RedisConnectionFactory redisConnectionFactory) {
        // 序列化Department对象
        RedisTemplate<Object, Department> template = new RedisTemplate<>();
        template.setConnectionFactory(redisConnectionFactory);
        template.setDefaultSerializer(new Jackson2JsonRedisSerializer(Department.class));
        return template;
    }

    @Bean
    @Primary
    public RedisCacheManager employeeRedisCacheManager(RedisTemplate<Object, Employee> employeeRedisTemplate) {
        // 要使用自定义的缓存管理器,因为使用了自定义的employeeRedisTemplate去操作缓存,json格式进行序列化数据
        RedisCacheManager cacheManager = new RedisCacheManager(employeeRedisTemplate);
        cacheManager.setUsePrefix(true);
        return cacheManager;
    }

    @Bean
    public RedisCacheManager departmentRedisCacheManager(RedisTemplate<Object, Department> departmentRedisTemplate) {
        // 缓存管理器
        RedisCacheManager cacheManager = new RedisCacheManager(departmentRedisTemplate);
        cacheManager.setUsePrefix(true);
        return cacheManager;
    }

}

并且在缓存方法上指定缓存管理器cacheManager为自定义的employeeRedisCacheManager。

@Service
public class EmployeeService {

    @Autowired
    private EmployeeMapper employeeMapper;
 	   
    @Cacheable(cacheNames = {"emp"},
            /*key= "#root.methodName+'['+#id+']'",*/
            key = "#id",
            /* keyGenerator = "myKeyGenerator",*/
            /* condition = "#id>1"*/
            unless = "#result==null" ,cacheManager = "employeeRedisCacheManager")
    public Employee getEmp(Integer id) {
        System.out.println("查询" + id + "号员工");
        Employee employee = employeeMapper.getEmpById(id);
        return employee;
    }
}

再次查看Redis客户端,以json格式缓存了emp,key=2的员工信息。
chapter10:SpringBoot与缓存_第12张图片

你可能感兴趣的:(SpringBoot,spring,boot,缓存,java)