文章目录
1. 引入
在实际的应用场景中,对于数据库来说,并不是所有的表都会对其使用同等频率的操作,而是对于某些热点信息所在的表操作更加的频繁。如果每次获取表中的信息都需要连接数据库、执行SQL语句和关闭连接等一系列的操作,那么性能的开销将很大。因此,为了减少开销,同时保证查询的速度,常常使用缓存来保存热点数据。当再次查询相同的数据时,不必从数据库中查询,而是直接从缓存中获取,大大的提高了效率。
Spring Boot中缓存都要遵守JSR107规范,它定义了缓存所需的5个核心接口,分别是:
- CachingProvider:定义了创建、配置、获取、管理和控制多个CacheManager。一个应用可以在运行期访问多个CachingProvider
- CacheManager:定义了创建、配置、获取、管理和控制多个唯一命名的Cache,这些Cache存在CacheManager的上下文中,一个CacheManager仅被一个CachingProvider所拥有
- Cache:一个类似Map的数据结构并临时存储以Key为索引的值,一个Cache仅被一个CacheManager所拥有
- Entry:一个存储在Cache中的key-value对
- Expiry:每一个存储在Cache中的条目有一个定义的有效期。一旦超过这个时间,条目更改为为过期状态,此时条目将不可访问、更新和删除。缓存有效期可以通过ExpiryPolicy设置
它们之间的关系如下所示:
2. Spring缓存抽象
所有满足JSR107规范的缓存都可以被使用,Spring框架为了统一不同类型的缓存定义了了org.springframework.cache.Cache和org.springframework.cache.CacheManager接口,并提供了同样满足规范的注解简化开发。
2.1 Cache接口
Cache接口规范的定义了每种缓存组件的规范,包含缓存的各种操作。Spring中的Cache接口为各种常用的缓存都提供了接口实现,如RedisCache,EhCacheCache , ConcurrentMapCache等。当Spring Boot开启缓存功能,每次调用需要缓存功能的方法时,Spring首先会从缓存中根据指定的参数寻找是否有满足要求的数据,如果有则直接返回;否则就调用方法返回结果,同时将结果保存到缓存中。
因此,当在Spring中使用缓存时,需要考虑以下几点:
- 什么样的方法需要缓存功能?
- 如果需要缓存,那么什么样的缓存策略适合?
- 当需读取数据时,优先考虑从缓存中寻找
2.2 CacheManager接口
CacheManager是–个缓存通用接口抽象类库,它支持各种高速缓存提供者,例如Memcache,Redis,并且有许多先进的功能特性。它的设计目标就是简化程序员对各种复杂缓存场景的处理,通过CacheManager只需要几行的代码就可以支持多层的缓存,从进程内缓存到分布式的缓存。通过CacheManager可以很容易在项目中更改缓存策略,它还提供一些更有价值的特性,例如高速缓存同步,并发更新,事件通知,性能计数器等。
3. 核心概念
3.1 核心注解
| 概念 | 说明 |
|---|---|
| Cache | 缓存接口,定义缓存操作,实现有RedisCache、EhCacheCache、ConcurrentMapCache等 |
| CacheManager | 缓存管理器,管理各种缓存(Cache)组件 |
@Cacheable | 针对方法配置,可根据方法的请求参数对其结果进行缓存 |
@CacheEvict | 清空缓存 |
@Cacheput | 保证方法被调用,又希望结果被缓存 |
@EnableCaching | 开启基于注解的缓存 |
| keyGenerator | 缓存数据时key的生成策略 |
| serialize | 缓存数据时value序列化序列 |
3.2 主要参数
这些参数针对于@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) |
| unless(@CachePut、@Cacheable) | 用于否决缓存,该表达式只在方法执行之后判断,此时可以拿到返回值result进行判断。条件为true不会缓存,fasle才缓存 | @Cacheable(value=”testcache”,unless=”#result == null”) |
4. SpEL表达式
| 名字 | 位置 | 描述 | 例子 |
|---|---|---|---|
| methodName | root object | 当前被调用的方法名 | #root.methodName |
| method | root object | 当前被调用的方法 | #root.method.name |
| target | root object | 当前被调用的目标对象 | #root.target |
| targetClass | root object | 当前被调用的目标对象类 | #root.targetClass |
| args | root object | 当前被调用的方法的参数列表 | #root.args[0] |
| caches | root object | 当前方法调用使用的缓存列表 | #root.caches[0].name |
| argument name | evaluation context | 方法参数的名字. 可以直接#参数名,也可以使用#p0或#a0 的形式,0代表参数的索引; | #iban、#a0 、#p0 |
| result | evaluation context | 方法执行后的返回值 | #result |
5. 使用案例
5.1 环境搭建
结合Mybatis对于数据库的操作来演示如何在项目中使用缓存,假设此时的account表如下所示:
mysql> select * from account;
+----+----------+-------+
| id | name | money |
+----+----------+-------+
| 1 | Forlogen | 1000 |
| 2 | Kobe | 1000 |
| 3 | James | 1000 |
+----+----------+-------+
3 rows in set (0.00 sec)
要想要在Spring Boot中使用缓存,首先需引入spring-boot-starter-cache模块:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-cache</artifactId>
</dependency>
然后在主程序中使用@EnableCaching开启基于注解的缓存。
为了使用Mybaits进行数据持久化操作,这里还需要配置数据源,另外使Mybatis支持驼峰命名法:
spring:
datasource:
username: root
password: 123456
url: jdbc:mysql://localhost:3306/sql_store?serverTimezone=GMT
driver-class-name: com.mysql.cj.jdbc.Driver
mybatis:
configuration:
map-underscore-to-camel-case: true
编写表对应的实体类Account,注意这里要实现Serializable 接口,保证类对象可以进行序列化和反序列化:
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class Account implements Serializable {
@Getter
@Setter
private Integer id;
@Getter
@Setter
private String name;
@Getter
@Setter
private Float money;
}
5.2 无缓存策略
编写持久层接口AccountMapper,接口中包含查询所有和根据ID查询两个方法:
@Mapper
public interface AccountMapper {
@Select("select * from account")
public List<Account> findAll();
@Select("select * from account where id=#{id}")
public Account findById(@Param("id")Integer id);
}
同时在被@SpringBootApplication注解标识的启动类中使用@MapperScan("xxx")指定要扫描的被@Mapper注解标识的类所在的包。
编写业务层实现类,并实现AccountMapper对象的自动注入:
@Service
public class AccountService {
@Autowired
AccountMapper accountMapper;
public List<Account> findALl(){
System.out.println("service findAll...");
List<Account> all = accountMapper.findAll();
return all;
}
public Account findById(Integer id){
System.out.println("service findById"+id);
Account account = accountMapper.findById(id);
return account;
}
}
最后,编写表现层的Controller
@RestController
public class AccountController {
@Autowired
AccountService accountService;
@GetMapping("/account")
public List<Account> testFindAll(){
List<Account> aLl = accountService.findALl();
return aLl;
}
@GetMapping("/account/{id}")
public Account testFindById(@PathVariable("id") Integer id){
Account account = accountService.findById(id);
return account;
}
}
执行主程序,并在浏览器中通过localhaost:8080/account查询所有,此时可以得到表中的信息:
[{"id":1,"name":"Forlogen","money":1000.0},{"id":2,"name":"Kobe","money":1000.0},{"id":3,"name":"James","money":1000.0}]
并且执行多次查询,控制台每一次都会输出service findAll...,表示每一次的查询操作都要执行SQL操作来查询数据库获取结果。通过localhaost:8080/account/1根据ID查询同样会在每次查询时执行SQL语句,这样显然是不好的选择。
5.3 使用默认缓存
在环境搭建中已经导入缓存所需的依赖,但上面并没有使用缓存,所以每次查询都需要访问数据库。使用缓存首先需在主程序上使用@EnableCaching开启缓存
@SpringBootApplication
@MapperScan("dyliang.mapper")
@EnableCaching
public class DyliangApplication {
public static void main(String[] args) {
SpringApplication.run(DyliangApplication.class, args);
}
}
然后对service层中要使用缓存的方法添加注解,这里首先使用@Cacheable
@Cacheable(value = "account", key = "root.args[0]")
public List<Account> findALl(){
System.out.println("service findAll...");
List<Account> all = accountMapper.findAll();
return all;
}
执行主程序发动请求,注意观察控制台的输出。首先可以看到,Spring Boot默认匹配的是SimpleCacheConfiguration
SimpleCacheConfiguration matched:
- Cache org.springframework.boot.autoconfigure.cache.SimpleCacheConfiguration automatic cache type (CacheCondition)
- @ConditionalOnMissingBean (types: org.springframework.cache.CacheManager; SearchStrategy: all) did not find any beans (OnBeanCondition)
第一次执行查询,控制台输出:
service findAll...
2020-06-20 21:52:50.355 INFO 5460 --- [nio-8080-exec-1] com.zaxxer.hikari.HikariDataSource : HikariPool-1 - Starting...
2020-06-20 21:52:51.015 INFO 5460 --- [nio-8080-exec-1] com.zaxxer.hikari.HikariDataSource : HikariPool-1 - Start completed.
然后多次执行相同的查询继续观察控制台输出,可以发现此时service findAll...只会在首次执行查询时输出,后续相同的操作并不会输出该信息,说明缓存已经生效。
对根据ID查询的方法同样开启缓存
@Cacheable(value = "account", key = "#id", condition = "#id>1")
public Account findById(Integer id){
System.out.println("service findById"+id);
Account account = accountMapper.findById(id);
return account;
}
执行主程序发送请求,发现此时控制台在每一次查询时都会输出service findById1,难道缓存没有生效嘛?其实,是因为在@Cacheable中配置了condition属性,它表示:只有在ID大于1时才缓存结果。如果此时查询ID为2,可以看到,只会在首次查询是看到service findById1,说明缓存仍然是生效的,而且condition属性也起到了作用。
@Cacheable的源码定义如下所示:
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
public @interface Cacheable {
@AliasFor("cacheNames")
String[] value() default {};
@AliasFor("value")
String[] cacheNames() default {};
String key() default "";
String keyGenerator() default "";
String cacheManager() default "";
String cacheResolver() default "";
String condition() default "";
String unless() default "";
boolean sync() default false;
}
最后来分析一下使用到的属性,以及其他未用到的该如何使用:
-
value/cacheNames:用于指定缓存组件的名字,String类型的数组,表示可以指定多个缓存
-
key:用于指定编写缓存数据时所用的key,默认时方法的参数值,也可以通过SpEL表达式指定
-
keyGenerator:key的生成器,可以指定自定义的key的生成器的组件id,但是它和key只能使用其中一个。
其中的keyGenerator可以通过实现相应的KeyGenerator接口自定义,并将其注册到Spring Boot的ioc容器中。KeyGenerator在Spring Boot中定义为一个函数式接口,接口的实现只需要重写其中的
generate()即可。例如,下面就是一个简单的自定义keyGenerator:@Configuration public class MyCacheConfig { @Bean("myKeyGenerator") public KeyGenerator keyGenerator(){ return new KeyGenerator(){ @Override public Object generate(Object target, Method method, Object... params) { return method.getName()+"["+ Arrays.asList(params).toString()+"]"; } }; } } -
cacheManager:指定缓存管理器,或者cacheResolver指定获取解析器
-
condition:指定符合条件的情况下才缓存,例如上面的
condition = #id>1表示当输入的ID大于1时才使用缓存 -
unless:否定缓存,例如可以配置
unless = "#id==1"表示如果传入的ID为1则不会使用缓存 -
sync:是否使用异步模式
为了实验前面提到的@CachePut、@CacheEvict和@Caching,首先在AccountMapper中添加一些方法:
@Mapper
public interface AccountMapper {
@Select("select * from account")
public List<Account> findAll();
@Select("select * from account where id=#{id}")
public Account findById(Integer id);
@Select("select * from account where name=#{name}")
public Account findByName(String name);
@Insert("insert into account(name,money) values (#{name},#{money})")
public int insertAccount(Account account);
@Update("update account set name=#{name} where id=#{id}")
public int updateAccount(Account account);
@Delete("delete from account where id=#{id}")
public void deleteAccount(Integer id);
}
更新Service层的方法:
@Service
public class AccountService {
@CachePut(value = "account", key = "#result.id")
public Account updateAccount(Account account){
accountMapper.updateAccount(account);
return account;
}
@CacheEvict(value = "account", key = "#id", beforeInvocation = true)
public void deleteAccount(Integer id){
accountMapper.deleteAccount(id);
}
@Caching(
cacheable = {@Cacheable(value = "account", key = "#name")},
put = {
@CachePut(value = "account", key = "#result.id"),
@CachePut(value = "account", key = "#result.name")
}
)
public Account findByName(String name){
Account account = accountMapper.findByName(name);
return account;
}
}
更新Controller:
@RestController
public class AccountController {
@Autowired
AccountService accountService;
@ResponseBody
@GetMapping("/delAccount/{id}")
public String testDeleteAccount(@PathVariable("id") Integer id){
accountService.deleteAccount(id);
return "success";
}
@GetMapping("/account/name/{name}")
public Account testFindByName(@PathVariable("name") String name){
Account byName = accountService.findByName(name);
return byName;
}
@GetMapping("/account/update")
public Account testUpdateAccount(Account account){
Account account1 = accountService.updateAccount(account);
return account1;
}
}
最后执行执行主程序发送不同的请求,都可以在控制台和数据库中表的更新来看到不同注解的功能。
总结:
-
@CachePut:既调用方法,又更新缓存数据,而且是同步更新。它首先调用目标方法,然后将结果缓存起来,但缓存不一定成功,所以说是希望缓存结果
public @interface CachePut { @AliasFor("cacheNames") String[] value() default {}; @AliasFor("value") String[] cacheNames() default {}; String key() default ""; String keyGenerator() default ""; String cacheManager() default ""; String cacheResolver() default ""; String condition() default ""; String unless() default ""; } -
@CacheEvict:缓存清除,其中属性:
-
key:指定要清除的数据 -
allEntries = true:指定清除这个缓存中所有的数据 -
beforeInvocation:缓存的清除是否在方法之前执行false:缓存清除操作是在方法执行之后执行,如果出现异常缓存就不会清除true:缓存清除操作是在方法运行之前执行,无论方法是否出现异常,缓存都清除
public @interface CacheEvict { @AliasFor("cacheNames") String[] value() default {}; @AliasFor("value") String[] cacheNames() default {}; String key() default ""; String keyGenerator() default ""; String cacheManager() default ""; String cacheResolver() default ""; String condition() default ""; boolean allEntries() default false; boolean beforeInvocation() default false; } -
-
@Caching:定义复杂的缓存规则
public @interface Caching { Cacheable[] cacheable() default {}; CachePut[] put() default {}; CacheEvict[] evict() default {}; }
另外还可以在类上使用@CacheConfig来抽取缓存公共的配置,如value/cacheName或cacheManager等。
@CacheConfig(cacheNames="xxx" ,cacheManager = "xxx")
6. 整合Redis
6.1 概述
Redis(REmote DIctionary Server)是一个由Salvatore Sanfilippo写的key-value存储系统,它是一个开源的使用ANSI C语言编写、遵守BSD协议、支持网络、可基于内存亦可持久化的日志型、Key-Value数据库,并提供多种语言的API。它通常被称为数据结构服务器,因为值(value)可以是字符串(String), 哈希(Hash), 列表(list), 集合(sets) 和有序集合(sorted sets)等类型。
6.2 安装Redis
这里同样在云服务器上通过docker安装Redis:
- 拉取镜像:
docker pull redis - 映射端口运行Redis:
docker run -d -p 6379:6379 --name redis redis的镜像名 - 查看:
docker ps
注意:为了可以实现远程客户端连接,还需要在服务器的安全组中添加规则:
最后使用Redis客户端测试连接,如果出现如下界面,表示连接成功。
6.3 使用案例
表现层、持久层都和前面一样,为了方便演示,这里只使用查询所有和根据ID查询两个方法,如下所示:
@Mapper
public interface AccountMapper {
@Select("select * from account")
public List<Account> findAll();
@Select("select * from account where id=#{id}")
public Account findById(Integer id);
}
@RestController
public class AccountController {
@Autowired
AccountService accountService;
@GetMapping("/account")
public List<Account> testFindAll(){
List<Account> aLl = accountService.findALl();
return aLl;
}
@ResponseBody
@GetMapping("/account/{id}")
public Account testFindById(@PathVariable("id") Integer id){
Account account = accountService.findById(id);
return account;
}
}
Spring Boot要整合Redis作为缓存,首先需引入依赖:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-cache</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
Spring Boot2.x中使用的是lettuce操作Redis,Lettuce的连接是基于Netty的,连接实例(StatefulRedisConnection)可以在多个线程间并发访问。
Springboot 1.x整合Spring-data-redis底层用的是jedis,jedis在多线程环境下是非线程安全的,使用了jedis pool连接池,为每个Jedis实例增加物理连接。
<dependency>
<groupId>io.lettuce</groupId>
<artifactId>lettuce-core</artifactId>
<scope>compile</scope>
</dependency>
运行主程序,从控制台输出可以看出此时的Redis缓存已经生效。
RedisCacheConfiguration matched:
- @ConditionalOnClass found required class 'org.springframework.data.redis.connection.RedisConnectionFactory' (OnClassCondition)
- Cache org.springframework.boot.autoconfigure.cache.RedisCacheConfiguration automatic cache type (CacheCondition)
发送请求http://localhost:8080/account执行查询所有,浏览器可以得到正确数据:
[{"id":1,"name":"Forlogen","money":1000.0},{"id":2,"name":"Kobe","money":1000.0},{"id":3,"name":"James","money":1000.0}]
查看Redis,可以看到此时数据已经成功的保存在了Redis中,但是保存的是序列化后的结果,所以结果并不具有可读性。
执行根据ID查询的请求同样可以将结果缓存到Redis中。
并且再次发送同样的请求,从控制台可以看出并没有访问数据库,因此Redis缓存已经生效。但如果使得保存到Redis中的结果具有可读性,例如转换为json格式?这就需要自定义RedisCacheManager和RedisTemplate。
6.4 自定义缓存数据格式
为了使得Redis中缓存的数据具有可读性,通常将其转化为json格式在进行缓存,同样反序列化使用的也是json格式的缓存数据。实现自定义缓存数据格式需要自定义Redis的RedisCacheManager和RedisTemplate,并使用Jackson2JsonRedisSerializer实现序列化数据转换为json格式,代码如下所示:
@Configuration
public class MyRedisConfig {
@Resource
//lettuce客户端连接工厂
private LettuceConnectionFactory lettuceConnectionFactory;
// 日志
private Logger logger= (Logger) LoggerFactory.getLogger(MyRedisConfig.class);
// json序列化器
private Jackson2JsonRedisSerializer<Account> jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Account.class);
//过期时间1天
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("account",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,Account> redisTemplate(RedisConnectionFactory redisConnectionFactory){
RedisTemplate<String,Account> 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();
}
}
编写好自己的RedisCacheManager和RedisTemplate并将其通过@Bean添加到容器中后,再次运行程序并发送请求。在浏览器中可以看到仍然可以得到表中的信息:
[{"id":1,"name":"Forlogen","money":1000.0},{"id":2,"name":"Kobe","money":1000.0},{"id":3,"name":"James","money":1000.0}]
再去查看Redis中保存的结果,可以看到此时结果就以json格式保存到缓存中。
执行根据ID查询的请求同样可以将结果以json格式保存到缓存中。
6.5 Redis常用操作
Redis常见的五大数据类型:String(字符串)、List(列表)、Set(集合)、Hash(散列)、ZSet(有序集合)。Spring Boot中提供了stringRedisTemplate用于操作各种数据类型的数据,相应的方法有:
stringRedisTemplate.opsForValue():用于操作String类型数据stringRedisTemplate.opsForList():用于操作ListstringRedisTemplate.opsForSet():用于操作SetstringRedisTemplate.opsForHash():用于操作HashstringRedisTemplate.opsForZSet():用于操作ZSet
下面通过一个简单的例子来看一下如何使用这些函数:
@RunWith(SpringRunner.class)
@SpringBootTest
class DyliangApplicationTests {
@Autowired
StringRedisTemplate stringRedisTemplate; //操作k-v都是字符串的
@Test
public void testRedis(){
//给redis中保存数据
stringRedisTemplate.opsForValue().append("msg","hello");
String msg = stringRedisTemplate.opsForValue().get("msg");
System.out.println(msg);
stringRedisTemplate.opsForList().leftPush("mylist","1");
stringRedisTemplate.opsForList().leftPush("mylist","2");
}
}
单元测试执行成功,然后去Redis中查看可以看到关于String和List类型的数据都成功保存在了缓存中。
7. 更多
更多操作可查看:Redis中文官方网站