SpringBoot 缓存
文章目录
1. Spring缓存抽象
Spring从3.1开始定义了org.springframework.cache.Cache和org.springframework.cache.CacheManager接口来统一不同的缓存技术;
并支持使用JCache(JSR-107)注解简化我们开发;
• Cache接口为缓存的组件规范定义,包含缓存的各种操作集合;
• Cache接口下Spring提供了各种xxxCache的实现;如RedisCache,EhCacheCache ,
ConcurrentMapCache等;
• 每次调用需要缓存功能的方法时,Spring会检查检查指定参数的指定的目标方法是否
已经被调用过;如果有就直接从缓存中获取方法调用后的结果,如果没有就调用方法
并缓存结果后返回给用户。下次调用直接从缓存中获取。
• 使用Spring缓存抽象时我们需要关注以下两点;
1、确定方法需要被缓存以及他们的缓存策略
2、从缓存中读取之前缓存存储的数据
2. 几个重要概念&缓存注解
Cache | **缓存接口,定义缓存操作。实现有:RedisCache、EhCacheCache、ConcurrentMapCache等缓存管理器,管理各种缓存(Cache)组件 |
---|---|
@Cacheable | 主要针对方法配置,能够根据方法的请求参数对其结果进行缓存 |
@CacheEvict | 清空缓存 |
@CachePut | 保证方法被调用,又希望结果被缓存。 |
@EnableCaching | 开启基于注解的缓存 |
keyGenerator | 缓存数据时key生成策略 |
serialize | 缓存数据时 value序列化策略 |
3. @Cacheable/@CachePut/@CacheEvict 主要的参数
value | 缓存的名称,在 spring 配置文件中定义,必须指定至少一个 | 例如:@Cacheable(value=”mycache”) 或者@Cacheable(value={”cache1”,”cache2”} |
---|---|---|
key | 缓存的 key,可以为空,如果指定要按照 SpEL 表达式编写,如果不指定,则缺省按照方法的所有参数进行组合 | 例如:@Cacheable(value=”testcache”,key=”#userName”) |
condition | 缓存的条件,可以为空,使用 SpEL 编写,返回 true 或者 false,只有为 true 才进行缓存/清除缓存 | 例如:@Cacheable(value=”testcache”,condition=”#userName.length()>2”) |
allEntries(@CacheEvict ) | 是否清空所有缓存内容,缺省为 false,如果指定为 true,则方法调用后将立即清空所有缓存 | 例如:@CachEvict(value=”testcache”,allEntries=true) |
beforeInvocation**(@CacheEvict)** | 是否在方法执行前就清空,缺省为 false,如果指定为 true,则在方法还没有执行的时候就清空缓存,缺省情况下,如果方法执行抛出异常,则不会清空缓存 | 例如:@CachEvict(value=”testcache”,beforeInvocation=true) |
4.搭建环境
pom.xml:
<!-- cache -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-cache</artifactId>
</dependency>
<!-- web -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- mybatis -->
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.1.1</version>
</dependency>
<!-- mysql驱动 -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.47</version>
</dependency>
sql文件(参考):
CREATE DATABASE db_user;
SET FOREIGN_KEY_CHECKS=0;
-- ----------------------------
-- Table structure for tb_user
-- ----------------------------
DROP TABLE IF EXISTS `tb_user`;
CREATE TABLE `tb_user` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`true_name` varchar(20) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT NULL,
`nick_name` varchar(20) DEFAULT NULL,
`age` int(11) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8;
-- ----------------------------
-- Records of tb_user
-- ----------------------------
INSERT INTO `tb_user` VALUES ('1', '二钊', '二二', '16');
INSERT INTO `tb_user` VALUES ('2', 'admin', '最高权限', '18');
INSERT INTO `tb_user` VALUES ('3', '炮', 'bilibili', '20');
application.properties:
#数据库配置
spring.datasource.driver-class-name=com.mysql.jdbc.Driver
spring.datasource.url=jdbc:mysql://localhost:3306/db_user?useSSL=false
spring.datasource.username=root
spring.datasource.password=333666999520
#开启mybatis驼峰命名
mybatis.configuration.map-underscore-to-camel-case=true
#日志级别设置为debug(方便查看sql语句的调用)
logging.level.com.example.springcache.mapper.UserMapper=debug
server.port=9090
debug=true
User.java
public class User implements Serializable {
private Integer id;
private String trueName;
private String nickName;
private Integer age;
public User(){
}
public User(Integer id, String trueName, String nickName, Integer age) {
this.id = id;
this.trueName = trueName;
this.nickName = nickName;
this.age = age;
}
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public String getTrueName() {
return trueName;
}
public void setTrueName(String trueName) {
this.trueName = trueName;
}
public String getNickName() {
return nickName;
}
public void setNickName(String nickName) {
this.nickName = nickName;
}
public Integer getAge() {
return age;
}
public void setAge(Integer age) {
this.age = age;
}
@Override
public String toString() {
return "User{" +
"id=" + id +
", trueName='" + trueName + '\'' +
", nickName='" + nickName + '\'' +
", age=" + age +
'}';
}
}
UserMapper.java
@Mapper
public interface UserMapper {
@Delete("DELETE FROM tb_user WHERE id=#{id}")
public int deleteUser(Integer id);
@Update("UPDATE tb_user SET true_name = #{trueName},nick_name=#{nickName},age=#{age} WHERE id=#{id}")
public int updateUser(User user);
@Select("SELECT * FROM tb_user WHERE id=#{id}")
public User selectUserByID(Integer id);
}
UserService.java
@Service
public class UserService {
@Resource
private UserMapper userMapper;
@CacheEvict(cacheNames = "user",key="#id")
public int deleteUser(Integer id){
return userMapper.deleteUser(id);
}
@CachePut(cacheNames = "user",key = "#user.id")
public User updateUser(User user){
userMapper.updateUser(user);
return user;
}
@Cacheable(cacheNames = "user",key="#id")
public User selectUserByID(Integer id){
return userMapper.selectUserByID(id);
}
}
UserController.java
@RestController
public class UserController {
@Autowired
private UserService userService;
@GetMapping("/user/{id}")
public User user(@PathVariable("id")Integer id){
return userService.selectUserByID(id);
}
@RequestMapping("/user/update")
public User update(User user){
return userService.updateUser(user);
}
@RequestMapping("/user/delete/{id}")
public String del(@PathVariable("id")Integer id){
int rs = userService.deleteUser(id);
return rs==1?"成功":"失败";
}
}
最后在启动类上使用@EnableCaching注解,开启缓存
4.1 测试@Cacheable
测试方法:调用user/{id},若注解生效则第一次调用时会调用数据库的数据,控制台上会看见sql的日志,第二次调用则不会:
4.2 测试@CachePut
@CachePut:既调用方法,又更新缓存数据;修改了数据库的某个数据,同时更新缓存;
运行时机:
1、先调用目标方法
2、将目标方法的结果缓存起来
测试步骤:
- 查询2号用户;查到的结果会放在缓存中;
http://localhost:9090/user/2
{“id”:2,“trueName”:“admin”,“nickName”:“最高权限”,“age”:18} - 以后查询还是之前的结果
- 更新2号用户;
http://localhost:9090/user/update?id=2&trueName=admin&nickName=管理员&age=17
将方法的返回值也放进缓存了;
key: 传入的employee对象值: 返回的employee对象; - 查询2号用户?
http://localhost:9090/user/2
应该是更新后的员工:{“id”:2,“trueName”:“admin”,“nickName”:“管理员”,“age”:17}
3.测试@CacheEvict
注:三步测试是连在一起的
@CacheEvict:缓存清除
key:指定要清除的数据
allEntries = true:
指定清除这个缓存中所有的数据
beforeInvocation = false: 缓存的清除是否在方法之前执行
默认代表缓存清除操作是在方法执行之后执行;如果出现异常缓存就不会清除
beforeInvocation = true:
代表清除缓存操作是在方法运行之前执行,无论方法是否出现异常,缓存都清除|
测试步骤:
- 经过第二步操作后,缓存内存有2号用户的数据,删除2号用户
http://localhost:9090/user/delete/2
的同时,缓存会被删除,再次访问http://localhost:9090/user/2
时应该是看不到任何数据的
5. @Cacheable的属性
@Cacheable:
将方法的运行结果进行缓存;以后再要相同的数据,直接从缓存中获取,不用调用方法;
CacheManager管理多个Cache组件的,对缓存的真正CRUD操作在Cache组件中,每一个缓存组件有自己唯一一个名字;
几个属性:
-
cacheNames/value:指定缓存组件的名字;,
-
key:缓存数据使用的key;可以用它来指定。默认是使用方法参数的值 1 -方法的返回值
编写SpEL; #id;参数id的值 #a0 #pθ #root. args[0] -
keyGenerator: key的生成器; 可以自己指定key的生成器的组件id
- key/keyGenerator:二选一使用
-
cacheManager:指定缓存管理器;或者cacheResolver指定获取解析器
-
condition:指定符合条件的情况下才缓存;
-
unless:否定缓存;当unless指定的条件为 true,方法的返回值就不会被缓存;可以获取到结果进行判断
unless = “#result == null”
6. 缓存的原理探索(@Cacheable)
1.自动配置类 CacheAutoConfiguration 加载的配置(SimpleConfiguration)
搜索CacheAutoConfiguration,进入该类
在这个类中有如下方法,打上断点,用debug模式启动
可以看到它加载了10个配置类
0 = "org.springframework.boot.autoconfigure.cache.GenericCacheConfiguration"
1 = "org.springframework.boot.autoconfigure.cache.JCacheCacheConfiguration"
2 = "org.springframework.boot.autoconfigure.cache.EhCacheCacheConfiguration"
3 = "org.springframework.boot.autoconfigure.cache.HazelcastCacheConfiguration"
4 = "org.springframework.boot.autoconfigure.cache.InfinispanCacheConfiguration"
5 = "org.springframework.boot.autoconfigure.cache.CouchbaseCacheConfiguration"
6 = "org.springframework.boot.autoconfigure.cache.RedisCacheConfiguration"
7 = "org.springframework.boot.autoconfigure.cache.CaffeineCacheConfiguration"
8 = "org.springframework.boot.autoconfigure.cache.SimpleCacheConfiguration"
9 = "org.springframework.boot.autoconfigure.cache.NoOpCacheConfiguration"
那么哪个配置类生效了呢,我们在全局配置文件上配上debug=true
,重新启动,点击Console一栏,Ctrl+F查找CacheConfiguration
2. SimpleConfiguration为我们做了什么(注册CacheManager)
SimpleConfiguration生效,那么这个类为我们做了什么呢?Ctrl+N,进入这个类
可以看到,它为我们注册了一个ConcurrentMapCacheManager,它实现了CacheManager接口,接口中有一个根据name取Cache的方法
在ConcurrentMapCacheManager找到getCahce()方法,该方法尝试根据name获取Cache,若Cache为空则锁一下,再次获取,若仍为空,则根据name属性创建一个Cache,并将Cache保存在本类中的cacheMap
点开createConcurrentMapCache,点击ConcurrentMapCache,进入ConcurrentMapCache
3. Cache
可以看见ConcurrentMapCache里面是使用一个map保存缓存数据的,里面有以下两个函数,分别执行get,put操作
4. KeyGenerator
那么上面的key是从哪里来的呢?在以上两个方法打断点,debug模式运行
沿着红色箭头方向一个一个找,可以发现它是由一个KeyGenerator生成的
以上的步骤最好自己亲手动手试验一下,否则可能会很晕。以上部分打的断点有:
-
CacheConfigurationImportSelector的selectImports方法
-
ConcurrentMapCacheManager的getCache方法
-
ConcurrentMapCache的lookup和put方法
7. 原理总结
测试如下方法:
@Cacheable(cacheNames = "user",key="#id")
public User selectUserByID(Integer id){
return userMapper.selectUserByID(id);
}
运行流程:
@Cacheable:
1、方法运行之前,先去查询Cache (缓存组件),按照cacheNames指定的名字获取:
(CacheManager先获取相应的缓存),第-次获取缓存如果没有Cache组件会自动创建。
2、去Cache中查找缓存的内容,使用一个key,默认就是方法的参数:
key是按照某种策略生成的;默认是使用keyGenerator生成的,默认使用SimpleKeyGenerator生成key;
SimpleKeyGenerator生成key的默认策略:
- 如果没有参数: key=new SimpleKey();
- 如果有一个参数: key=参数的值
- 如果有多个参数: key=new SimpleKey(params);
3、没有查到缓存就调用目标方法;
4、将目标方法返回的结果,放进缓存中
@Cacheable标注的方法执行之前先来检查缓存中有没有这个数据,默认按照参数的值作为hey去查询缓存,_
如果没有就运行方法并将结果放入缓存:以后再来调用就可以直接使用缓存中的数据:
核心:
1)、使用CacheManager [ConcurrentMapCacheManager]按照名字得到Cache [ConcurrentMapCache]组件:
2)、key使用keyGenerator生成的,默认是SimpleKeyGenerator
8.整合Redis
SpringBoot1.x版本和SpringBoot2.x版本的redis配置不同,注意区分,这里使用的是SpringBoot2.x版本。
(1)Springboot中使用redis操作的两种方式:lettuce和jedis,两者在进行操作时都需要序列化器来实现序列化(推荐使用jackson2JsonRedisSerializer,相比于JDK提供的序列化器和String序列化器长度更短),lettuce和redis都是 redis的客户端。
(2)Springboot 1.x整合Spring-data-redis底层用的是jedis,Springboot 2.x整合spring-data-redis用的是lettuce,jedis在多线程环境下是非线程安全的,使用了jedis pool连接池,为每个Jedis实例增加物理连接。Lettuce的连接是基于Netty的,连接实例(StatefulRedisConnection)可以在多个线程间并发访问。
pom文件:
<!-- cache -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-cache</artifactId>
</dependency>
<!-- web -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- mybatis -->
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.1.1</version>
</dependency>
<!-- mysql驱动 -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.47</version>
</dependency>
<!-- Redis -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!--spring2.0集成redis所需common-pool2-->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-pool2</artifactId>
<version>2.4.2</version>
</dependency>
application.properties:
spring.redis.host=localhost
spring.redis.port=6379
spring.redis.password=
spring.cache.redis.time-to-live=-1ms
# 数据库连接超时时间,2.0 中该参数的类型为Duration,这里在配置的时候需要指明单位
spring.redis.timeout=1000s
# 最大活跃连接数,负数为不限制
spring.redis.lettuce.pool.max-active=-1
# 最大空闲连接数
spring.redis.lettuce.pool.max-idle=8
#最小空闲连接数
spring.redis.lettuce.pool.min-idle=8
# 等待可用连接的最大时间,负数为不限制
spring.redis.lettuce.pool.max-wait=-1ms
配置类:
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.cache.interceptor.KeyGenerator;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.cache.RedisCacheConfiguration;
import org.springframework.data.redis.cache.RedisCacheManager;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.*;
import javax.annotation.Resource;
import java.time.Duration;
import java.util.HashMap;
import java.util.Map;
@Configuration
public class RedisConfig {
//lettuce客户端连接工厂
@Resource
private LettuceConnectionFactory lettuceConnectionFactory;
//日志
private Logger logger=LoggerFactory.getLogger(RedisConfig.class);
//json序列化器
private Jackson2JsonRedisSerializer<Object> jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);
//缓存生存时间
private Duration timeToLive = Duration.ofDays(1);
@Bean
public RedisCacheManager cacheManager(RedisConnectionFactory connectionFactory) {
//redis缓存配置
RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig()
.entryTtl(this.timeToLive)
.serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(keySerializer()))
.serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(valueSerializer()))
.disableCachingNullValues();
//缓存配置map
Map<String,RedisCacheConfiguration> cacheConfigurationMap=new HashMap<>();
//自定义缓存名,后面使用的@Cacheable的CacheName
//cacheConfigurationMap.put("users",config);
cacheConfigurationMap.put("default",config);
//根据redis缓存配置和reid连接工厂生成redis缓存管理器
RedisCacheManager redisCacheManager = RedisCacheManager.builder(connectionFactory)
.cacheDefaults(config)
.transactionAware()
.withInitialCacheConfigurations(cacheConfigurationMap)
.build();
logger.debug("自定义RedisCacheManager加载完成");
return redisCacheManager;
}
//redisTemplate模板提供给其他类对redis数据库进行操作
@Bean(name = "redisTemplate")
public RedisTemplate<String,Object> redisTemplate(RedisConnectionFactory redisConnectionFactory){
RedisTemplate<String,Object> redisTemplate = new RedisTemplate<>();
redisTemplate.setConnectionFactory(redisConnectionFactory);
redisTemplate.setKeySerializer(keySerializer());
redisTemplate.setHashKeySerializer(keySerializer());
redisTemplate.setValueSerializer(valueSerializer());
redisTemplate.setHashValueSerializer(valueSerializer());
logger.debug("自定义RedisTemplate加载完成");
return redisTemplate;
}
//redis键序列化使用StrngRedisSerializer
private RedisSerializer<String> keySerializer() {
return new StringRedisSerializer();
}
//redis值序列化使用json序列化器
private RedisSerializer<Object> valueSerializer() {
return new GenericJackson2JsonRedisSerializer();
}
//缓存键自动生成器
@Bean
public KeyGenerator myKeyGenerator() {
return (target, method, params) -> {
StringBuilder sb = new StringBuilder();
sb.append(target.getClass().getName());
sb.append(method.getName());
for (Object obj : params) {
sb.append(obj.toString());
}
return sb.toString();
};
}
}
关于Redis整合SpringBoot的个人不是非常清楚,这里只是为了做个记录,可以参考一下其他人的博客:
https://www.cnblogs.com/coder-lichao/p/10889457.html
https://blog.csdn.net/zhulier1124/article/details/82154937
https://www.jianshu.com/p/0d4aea41a70c