spring cache不是具体的缓存解决方案,而是对缓存操作的抽象。可以这么理解:spring cache是对缓存增删改查这些操作的抽象,按照spring cache提供的api可以很方便地操作缓存。
本文使用spring + mybatis + redis。不建议使用mybatis的二级缓存。理由如下:
1、单表更新可能导致复杂的多表查询读取到脏数据。
2、缓存的key可读性极差,出现bug时不好调试。如下图:
使用spring提供的@Cacheable、@CachePut、@CacheEvict可以很方便的操作缓存
@Cacheable 用在查询方法上。当标记在方法上时表示该方法支持缓存,Spring会将此方法的返回值缓存起来。下一次执行时,能在缓存中读取到数据就直接返回缓存数据,不再执行方法体里面的代码。
@CachePut 用在增加、更新方法上。与@Cacheable不同的是使用@CachePut标注的方法在执行前不会去检查缓存中是否存在之前执行过的结果,而是每次都会执行该方法,并将执行结果以键值对的形式存入指定的缓存中。
@CacheEvict 用在删除方法上。删除缓存。
介绍这些注解中的两个元素
value:缓存的名称,必须指定。在redis中是一个有序集合zset
key: 缓存key,不填写则按照keyGenerator()来生成,建议手动填写key,不用keyGenerator()生成的key。如果指定要按照 SpEL 表达式编写。
写这篇博文的唯一目的是教大家使用spel编写合理、可读性强的缓存key。spel表达式:@表示查找IOC容器中的bean,#引用参数
项目代码地址:https://pan.baidu.com/s/1jwzHLMR1MKcexySTARuYsw
开始上代码
新建spring boot项目,pom.xml配置如下:
<?xml version="1.0" encoding="UTF-8"?>
<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 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.example</groupId>
<artifactId>cache</artifactId>
<version>0.0.1-SNAPSHOT</version>
<packaging>jar</packaging>
<name>cache</name>
<description>Demo project for Spring Boot</description>
<!-- 定义公共资源版本 -->
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>1.5.6.RELEASE</version>
<relativePath />
</parent>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-log4j2</artifactId>
</dependency>
<!--测试-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
</dependency>
<!--database-->
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>1.1.1</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.44</version>
</dependency>
<!--redis-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
</dependencies>
<build>
<resources>
<resource>
<!--解决resource中的资源文件不打包-->
<directory>src/main/resources</directory>
<includes>
<include>**/*.*</include>
</includes>
<filtering>false</filtering>
</resource>
<resource>
<!--解决mapper.xml不打包-->
<directory>src/main/java</directory>
<includes>
<include>**/*.xml</include>
</includes>
<filtering>false</filtering>
</resource>
</resources>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<!--该配置必须-->
<fork>true</fork>
</configuration>
</plugin>
</plugins>
</build>
</project>
application.yml 配置如下
server:
port: 8080
logging:
level:
root: info, WARN, DEBUG
com:
example:
cache: DEBUG #打印sql
spring:
# 数据库配置
datasource:
url: jdbc:mysql://localhost:3306/cpq
username: root
password: cpq..123
driver-class-name: com.mysql.jdbc.Driver
redis:
# redis数据库索引(默认为0)
database: 0
# redis服务器地址(默认为localhost)
host: localhost
# redis端口(默认为6379)
port: 6379
# redis访问密码(默认为空)
password:
# redis连接超时时间(单位为毫秒)
timeout: 10000
# redis连接池配置
pool:
# 最大可用连接数(默认为8,负数表示无限)
max-active: 8
# 最大空闲连接数(默认为8,负数表示无限)
max-idle: 8
# 最小空闲连接数(默认为0,该值只有为正数才有作用)
min-idle: 0
# 从连接池中获取连接最大等待时间(默认为-1,单位为毫秒,负数表示无限)
max-wait: -1
启动类配置如下:
package com.example.cache;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@MapperScan("com.example.cache.*.mapper") //扫描mapper
@EnableAutoConfiguration
@SpringBootApplication
public class CacheApplication {
public static void main(String[] args) {
SpringApplication.run(CacheApplication.class, args);
}
}
缓存配置类如下
package com.example.cache.redis;
import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.PropertyAccessor;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.cache.CacheManager;
import org.springframework.cache.annotation.CachingConfigurerSupport;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.cache.interceptor.KeyGenerator;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.cache.RedisCacheManager;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;
@Configuration
@EnableCaching //开启缓存
public class RedisConfig extends CachingConfigurerSupport {
//不建议使用,默认key生成策略为类全名+方法名+参数
@Bean
public KeyGenerator keyGenerator() {
return (target, method, params) -> {
//StringBuffer是线程安全的
StringBuffer sb = new StringBuffer();
sb.append(target.getClass().getName());
sb.append(":");
sb.append(method.getName());
for (Object obj : params) {
sb.append(":");
sb.append(obj.toString());
}
return sb.toString().replaceAll("\\.", ":");
};
}
@Bean
public CacheManager cacheManager(RedisTemplate redisTemplate) {
RedisCacheManager cacheManager = new RedisCacheManager(redisTemplate);
//设置缓存过期时间,单位是秒
cacheManager.setDefaultExpiration(60*60*24*30);
return cacheManager;
}
@Bean
public RedisTemplate redisTemplate(RedisConnectionFactory factory) {
RedisTemplate template = new RedisTemplate();
template.setConnectionFactory(factory);
//key使用StringRedisSerializer
StringRedisSerializer strSerializer = new StringRedisSerializer();
template.setKeySerializer(strSerializer);
template.setHashKeySerializer(strSerializer);
Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);
ObjectMapper om = new ObjectMapper();
om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
jackson2JsonRedisSerializer.setObjectMapper(om);
//value使用Jackson2JsonRedisSerializer
template.setValueSerializer(jackson2JsonRedisSerializer);
template.setHashValueSerializer(jackson2JsonRedisSerializer);
return template;
}
}
新建一个存储redis key的常量类
package com.example.cache.redis;
import org.springframework.stereotype.Component;
//定义redis key的前缀常量类,使用冒号分割
@Component //此类必须注入到ioc中才能被spel表达式通过@找到
public class RedisConst {
public static final String SPRING_CACHE = "SPRING:CACHE:";
}
mode和mapper使用generator生成的,model的sql
CREATE DATABASE /*!32312 IF NOT EXISTS*/`cpq` /*!40100 DEFAULT CHARACTER SET utf8 */;
USE `cpq`;
DROP TABLE IF EXISTS `spring_cache`;
CREATE TABLE `spring_cache` (
`id` varchar(36) COLLATE utf8_unicode_ci NOT NULL,
`name` varchar(64) COLLATE utf8_unicode_ci NOT NULL,
`num` int(11) NOT NULL,
`date` datetime DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci;
SpringCacheService.java类如下
package com.example.cache.springcache.service;
import com.example.cache.redis.RedisConst;
import com.example.cache.springcache.mapper.SpringCacheMapper;
import com.example.cache.springcache.model.SpringCache;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.cache.annotation.CachePut;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.stereotype.Service;
@Service
public class SpringCacheService {
@Autowired
SpringCacheMapper springCacheMapper;
/**
* 插入数据
* key: @表示查找IOC容器中的bean,#引用参数
*/
@CachePut(value = RedisConst.SPRING_CACHE, key = "@redisConst.SPRING_CACHE + #record.getId()")
//@CachePut(value = RedisConst.SPRING_CACHE) //不建议省略key
public SpringCache insertSelective(SpringCache record){
springCacheMapper.insertSelective(record);
return record;
}
//更新
@CachePut(value = RedisConst.SPRING_CACHE, key = "@redisConst.SPRING_CACHE + #record.getId()")
public SpringCache updateByPrimaryKey(SpringCache record){
springCacheMapper.updateByPrimaryKey(record);
return record;
}
//查找
@Cacheable(value = RedisConst.SPRING_CACHE, key = "@redisConst.SPRING_CACHE + #id" )
public SpringCache selectByPrimaryKey(String id){
System.out.println("******************没找到缓存,执行本方法******************");
return springCacheMapper.selectByPrimaryKey(id);
}
//删除
/****** 增删改查的key都是"SPRING:CACHE:实际id" *****/
@CacheEvict(value = RedisConst.SPRING_CACHE, key = "@redisConst.SPRING_CACHE + #id")
public int deleteByPrimaryKey(String id){
return springCacheMapper.deleteByPrimaryKey(id);
}
/**
* 清空RedisConst.SPRING_CACHE的缓存
*
* 执行过程:通过RedisConst.SPRING_CACHE找到一个zset,zset中存储了此缓存下的key。
*/
@CacheEvict(value = RedisConst.SPRING_CACHE, allEntries=true)
public void deleteAllEntries(){
}
}
测试类
package com.example.cache.springcache;
import com.example.cache.springcache.model.SpringCache;
import com.example.cache.springcache.service.SpringCacheService;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;
import java.util.Date;
import java.util.Random;
import java.util.UUID;
@RunWith(SpringRunner.class)
@SpringBootTest
public class Test1 {
@Autowired
SpringCacheService springCacheService;
//测试插入
@Test
public void insert(){
SpringCache springCache = new SpringCache();
springCache.setId(UUID.randomUUID().toString());
springCache.setName("name1111");
springCache.setNum(new Random().nextInt(100));
springCache.setDate(new Date());
springCacheService.insertSelective(springCache);
}
//测试、更新
@Test
public void update(){
SpringCache springCache = new SpringCache();
springCache.setId("f143a4c7-536c-4cc2-b82d-6258a56ecd7a");
springCache.setName("name改变");
springCache.setNum(new Random().nextInt(100));
springCache.setDate(new Date());
springCacheService.updateByPrimaryKey(springCache);
}
//测试、获取
@Test
public void get(){
SpringCache sc = springCacheService.selectByPrimaryKey("aae78c61-81af-4471-bfce-06e5c09f364b");
System.out.println(sc);
}
//测试、删除
@Test
public void delete(){
springCacheService.deleteByPrimaryKey("4c77c71c-c2c9-4803-9d1f-dbe8a3ba5a15");
}
//测试、删除缓存名下的所有缓存
@Test
public void deleteAll(){
springCacheService.deleteAllEntries();
}
}
执行多次insert之后,我们看下redis中的数据
@Cacheable、@CachePut、@CacheEvict注解中value、key:
value在redis中也是一个的键,键指向一个zset
key直接指向SpringCacheService.java方法返回的数据,zset中存储了key
通过上图也可以看出,使用
@CachePut(value = RedisConst.SPRING_CACHE, key = "@redisConst.SPRING_CACHE + #record.getId()")
@Cacheable(value = RedisConst.SPRING_CACHE, key = "@redisConst.SPRING_CACHE + #id" )
这种方式编写key,可读性很强,尤其在生产环境中查找问题会非常方便