缓存,两个字经常出现在我们的耳旁,它被用作数据的高性能读写,防止每次都需要进行数据库的读写,减少性能消耗。Spring从3.1版本开始就提供了cache支持,SpringBoot更是提供了spring-boot-starter-cache用于我们快速进行缓存开发,支持多种缓存组件整合:Redis、EhCache、Hazelcast等。接下来我们就以当前最火的Redis来为大家介绍cache的使用。
简介
Spring虽然提供了cache支持,但是它只提供org.springframework.cache.Cache和 org.springframework.cache.CacheManager两个接口做缓存逻辑抽象,不做具体的数据存储,所以我们需要选择一种数据库去存储,这里选择Redis。
cache的核心思想:Spring会去检查缓存方法,若该方法已经被执行过并且已经缓存数据,则下次调用该方法会直接从缓存中获取数据,而不是每次调用都执行处理逻辑。使用缓存,在多次调用结果返回值都一样的情况下,性能特别好,缓解数据库压力,提高响应速度。即使数据库数据发生变化,大家也不用担心,缓存数据也会进行相应更改。至于是如何做到的,先不用深究,我们先来一个示例,展示一下cache如何使用,再来思考为什么。
项目搭建
在这里,我们使用Idea+Maven的方式创建项目,大家应该清楚,就不展示具体步骤了。
项目版本:SpringBoot (2.1.4)
首先,引入redis和cache依赖:
pom.xml:
<!--cache公共依赖-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-cache</artifactId>
</dependency>
<!--redis依赖-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!--redis pool 依赖-->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-pool2</artifactId>
<version>2.6.1</version>
</dependency>
SpringBoot会根据依赖自动配置redis缓存,我们无需做过多的配置,只需添加如下参数。
application.properties:
#开启redis缓存
spring.cache.type=redis
#redis地址
spring.redis.host=192.168.159.130
#redis cache默认配置 非必须
spring.cache.redis.cache-null-values=true
spring.cache.redis.key-prefix=REDIS_CACHE_
spring.cache.redis.time-to-live=600000
spring.cache.redis.use-key-prefix=true
User:
@Data
@AllArgsConstructor
@NoArgsConstructor
public class User implements Serializable {
private Integer id;
private String name;
private Date birth;
}
UserService:
@Service
public class UserService {
/**
* 第一次,会执行业务逻辑,获取用户数据并缓存到user中
* 第二次,会直接从user缓存中获取数据
* ...
* @param id id
* @return user
*/
@Cacheable(value = "user", key = "#id")
public User findUserById(Integer id) {
System.out.println("查找" + id + "用户并缓存");
return new User(id, "张三", new Date());
}
/**
* 删除user::id的缓存数据
* @param id id
*/
@CacheEvict(value = "user", key = "#id")
public void deleteUserById(Integer id) {
System.out.println("删除" + id + "用户及缓存");
}
/**
* 在修改{id}用户数据之后,更新user::id缓存数据
* @param user 用户
* @return user
*/
@CachePut(value = "user", key = "#user.id")
public User updateUserById(User user) {
System.out.println("更新" + user + "用户并更新缓存");
return user;
}
}
UserController:
@RestController
@RequestMapping("/user")
public class UserController {
@Resource
private UserService userService;
@GetMapping("/{id}")
public User findUserById(@PathVariable Integer id) {
return userService.findUserById(id);
}
@DeleteMapping("/{id}")
public void deleteUserById(@PathVariable Integer id) {
userService.deleteUserById(id);
}
@PutMapping("/{id}/update")
public void updateUserById(@PathVariable Integer id) {
User user = new User(id, "李四", new Date());
userService.updateUserById(user);
}
}
大家可以运行一下上述的代码,若大家发现缓存失败,请在启动类上加上@EnableCaching,用于开启缓存。现在一个简单的用户缓存系统就做好了,大家是不是觉得特别简单,我们都没有做过多的配置,就已经实现了缓存的效果。下面,我们就来逐步分析。
基于注解的缓存
在上述UserService中,我们使用了几个注解来支持不同情况下的缓存处理,它们是Spring提供的基于JSR-107 的缓存注解。总结如下:
- @Cacheable:用于缓存结果。在每次执行的时候,都会判断当前逻辑是否已经执行,若执行过则从缓存中获取数据,否则执行具体逻辑并缓存结果。支持多个缓存名,例如:@Cacheable({"user", "person"})。该注解提供多个参数,主要参数如下:
参数 | 含义 | 示例 |
cacheNames / value | 指定缓存名 | @Cacheable(value = "user") @Cacheable(cacheNames="user") |
key | 指定缓存key,采用SpEL表达式编写,若不指定则默认采用所有参数的组合进行缓存 | @Cacheable(value = "user", key = "#id") |
keyGenerator | 用于指定缓存key的生成方式,若需要自定义的时候使用 | @Cacheable(cacheNames="user", keyGenerator="myKeyGenerator") |
condition | 用于条件判断,只有当满足条件也就是返回为true时,才会进行缓存 | @Cacheable(value = "user",condition = "#id > 10") |
- @CacheEvict:用于删除某个或多个缓存。该注解也支持上述@Cacheable中展示的参数,新增所属@CacheEvict的参数:
参数 | 含义 | 示例 |
allEntries | 指定是否清空所有缓存内容,缺省为 false,若为true,则方法调用后将立即清空所有缓存。不允许与key同时使用 | @CacheEvict(value = "user",allEntries = true) |
beforeInvocation | 是否在方法逻辑执行之前执行,缺省false,若为true,方法逻辑执行前就会删除 | @CacheEvict(value = "user", beforeInvocation = true) |
- @CachePut:用于更新缓存而不影响方法的执行。每次执行,都会进行缓存更新。
- @Caching:包含@Cacheable、@CachePut、@CacheEvict,用于组合多个注解的使用。例如:
@Caching( put = { @CachePut(value = "user", key = "#user.id"), @CachePut(value = "user", key = "#user.name") } } public User updateUserById(User user)
- @CacheConfig:用于在类级别上做一些公用的配置。例如:@CacheConfig(cacheNames = "user")
源码分析
大家已经知道如何使用注解去实现缓存了,现在我们就来看看SpringBoot是如何实现缓存功能的。在这里,我们采用的是Redis的缓存,下面我们就来找找spring-boot-starter-cache包中是否存在相应配置,结果不负所望,在/cache包下找到了RedisCacheConfiguration类,部分源码如下:
/**
* 该类初始化的前提:Redis自动配置
* 该类只会上下文不存在CacheManager Bean的情况下初始化
*/
@Configuration
@ConditionalOnClass(RedisConnectionFactory.class)
@AutoConfigureAfter(RedisAutoConfiguration.class)
@ConditionalOnBean(RedisConnectionFactory.class)
@ConditionalOnMissingBean(CacheManager.class)
@Conditional(CacheCondition.class)
class RedisCacheConfiguration {
/**
* redis缓存的配置Bean
*/
@Bean
public RedisCacheManager cacheManager(RedisConnectionFactory redisConnectionFactory,
ResourceLoader resourceLoader) {
RedisCacheManagerBuilder builder = RedisCacheManager
//设置RedisConnectionFactory
.builder(redisConnectionFactory)
//设置缓存的默认配置
.cacheDefaults(determineConfiguration(resourceLoader.getClassLoader()));
//获取application.properties -> spring.cache.cache-names配置的默认缓存名
List<String> cacheNames = this.cacheProperties.getCacheNames();
if (!cacheNames.isEmpty()) {
//初始化到 initialCaches map中
builder.initialCacheNames(new LinkedHashSet<>(cacheNames));
}
return this.customizerInvoker.customize(builder.build());
}
//redis缓存的默认配置
private org.springframework.data.redis.cache.RedisCacheConfiguration determineConfiguration(
ClassLoader classLoader) {
if (this.redisCacheConfiguration != null) {
return this.redisCacheConfiguration;
}
//获取application.properties -> spring.cache.redis.* 的配置信息
Redis redisProperties = this.cacheProperties.getRedis();
//用于获取Redis包下的默认配置
org.springframework.data.redis.cache.RedisCacheConfiguration config = org.springframework.data.redis.cache.RedisCacheConfiguration
.defaultCacheConfig();
//默认采用JdkSerializationRedisSerializer序列化value
config = config.serializeValuesWith(SerializationPair
.fromSerializer(new JdkSerializationRedisSerializer(classLoader)));
//设置key的过期时间
if (redisProperties.getTimeToLive() != null) {
config = config.entryTtl(redisProperties.getTimeToLive());
}
//设置缓存key的前缀
if (redisProperties.getKeyPrefix() != null) {
config = config.prefixKeysWith(redisProperties.getKeyPrefix());
}
//是否允许value为null
if (!redisProperties.isCacheNullValues()) {
config = config.disableCachingNullValues();
}
//是否开启key前缀配置
if (!redisProperties.isUseKeyPrefix()) {
config = config.disableKeyPrefix();
}
return config;
}
}
org.springframework.data.redis.cache.RedisCacheConfiguration (Redis包下的缓存配置):
public static RedisCacheConfiguration defaultCacheConfig(@Nullable ClassLoader classLoader) {
DefaultFormattingConversionService conversionService = new DefaultFormattingConversionService();
//用于注册默认转换配置 String类型获取byte[]采用UTF-8编码 SimpleKey类型转换为String类型
registerDefaultConverters(conversionService);
//默认参数配置
return new RedisCacheConfiguration(Duration.ZERO, true, true, CacheKeyPrefix.simple(),
//key 采用string->byte[] 形式序列化
SerializationPair.fromSerializer(RedisSerializer.string()),
//value 采用JdkSerializationRedisSerializer 序列化
SerializationPair.fromSerializer(RedisSerializer.java(classLoader)), conversionService);
}
//构造函数 ttl 键的过期时间 、 cacheNullValues value是否允许null、 usePrefix 是否开启key前缀、 keyPrefix key前缀配置
//keySerializationPair key的序列化方式、valueSerializationPair value的序列化方式、ConversionService 内容转换器
RedisCacheConfiguration(Duration ttl, Boolean cacheNullValues, Boolean usePrefix, CacheKeyPrefix keyPrefix,
SerializationPair<String> keySerializationPair, SerializationPair<?> valueSerializationPair,
ConversionService conversionService)
由于value默认采用JdkSerializationRedisSerializer序列化的方式,所以会自动把结果转化为字节数组进行保存,通过客户端查询,我们是看不出具体保存的内容,数据不直观。这时我们就可以通过修改默认配置来进行特定的序列化,例如json,如下所示:
@Bean
public RedisCacheConfiguration redisCacheConfiguration(CacheProperties cacheProperties) {
RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig();
//设置value为json的序列化方式
config = config.serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(new GenericJackson2JsonRedisSerializer()));
//获取spring.cache.redis.*的配置
CacheProperties.Redis redisProperties = cacheProperties.getRedis();
if (redisProperties.getTimeToLive() != null) {
config = config.entryTtl(redisProperties.getTimeToLive());
}
if (redisProperties.getKeyPrefix() != null) {
config = config.prefixKeysWith(redisProperties.getKeyPrefix());
}
if (!redisProperties.isCacheNullValues()) {
config = config.disableCachingNullValues();
}
if (!redisProperties.isUseKeyPrefix()) {
config = config.disableKeyPrefix();
}
return config;
}
GenericJackson2JsonRedisSerializer是已经封装好的json序列化器,使用它就可以达到我们的效果,当然大家也可以通过FastJson去自定义自己的序列化方式。这时,我们就可以看到比较直观的显示了,例如:
{
"@class": "com.fanqie.springboot2.cache.entity.User",
"id": 29,
"name": "张三",
"birth": [
"java.util.Date",
1554732381704
]
}
同理,Key的序列化器也可以进行修改,不过默认采用StringRedisSerializer已经适合大部分业务了,具体业务可以根据自己的需要定义,不做过多描述。
总结
基于Springboot2的自动配置,在使用缓存的过程中已经不需要太多的配置,会根据我们的依赖自动生成需要的Bean。当然,若大家在某些业务上,需要自定义相关配置,也是比较简单的。具体的内容,需要大家自行深入了解,上述内容是笔者自己的一些见解,若有不对的地方,望大家指正,共同努力,进步。
源码见于:https://gitee.com/hpaw/SpringBoot2Demo/tree/master/springboot-cache-redis