在程序中可以使用缓存的技术来节省对数据库的开销。Spring Boot对缓存提供了很好的支持,我们几乎不用做过多的配置即可使用各种缓存实现。这里主要介绍Ehcache和Redis缓存实现。
1、准备工作 1.1 搭建一个SpringBoot项目 项目结构如下图所示:
然后yml中配置日志输出级别以观察SQL的执行情况:
1 2 3 4 5 logging: level: com: lyq: mapper: debug
其中com.lyq.mapper为MyBatis的Mapper接口路径
1.2 编写测试方法 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 @RunWith(SpringJUnit4ClassRunner.class) @SpringBootTest(classes = CacheApplication.class) public class ApplicationTest { @Autowired private StudentService studentService; @Test public void test () throws Exception { Student student1 = this .studentService.queryStuBySno("210" ); System.out.println("学号为[" + student1.getStuno() + "]的学生姓名为:" + student1.getStuname()); Student student2 = this .studentService.queryStuBySno("210" ); System.out.println("学号为[" + student2.getStuno() + "]的学生姓名为:" + student2.getStuname()); } }
测试运行test()
方法,结果如下:
1 2 3 4 5 6 7 8 2021-12-23 15:52:32.532 DEBUG 23860 --- [ main ] c.l.mapper.StudentMapper.queryStuBySno : ==> Preparing: select * from student where stuno = ? 2021-12-23 15:52:32.853 DEBUG 23860 --- [ main ] c.l.mapper.StudentMapper.queryStuBySno : ==> Parameters: 210 (String) 2021-12-23 15:52:32.894 DEBUG 23860 --- [ main ] c.l.mapper.StudentMapper.queryStuBySno : <== Total: 1 学号210的学生姓名为:zhangsan 2021-12-23 15:52:32.896 DEBUG 23860 --- [ main ] c.l.mapper.StudentMapper.queryStuBySno : ==> Preparing: select * from student where stuno = ? 2021-12-23 15:52:32.897 DEBUG 23860 --- [ main ] c.l.mapper.StudentMapper.queryStuBySno : ==> Parameters: 210 (String) 2021-12-23 15:52:32.900 DEBUG 23860 --- [ main ] c.l.mapper.StudentMapper.queryStuBySno : <== Total: 1 学号210的学生姓名为:zhangsan
可发现第二个查询虽然和第一个查询完全一样,但其还是对数据库进行了查询。接下来引入缓存来改善这个结果
2、使用缓存 2.1 缓存注解使用 2.1.1 引入依赖 要开启Spring Boot的缓存功能,需要在pom中引入spring-boot-starter-cache
:
1 2 3 4 <dependency > <groupId > org.springframework.boot</groupId > <artifactId > spring-boot-starter-cache</artifactId > </dependency >
2.1.2 开启缓存 在Spring Boot入口类中加入@EnableCaching
注解开启缓存功能
1 2 3 4 5 6 7 @SpringBootApplication @EnableCaching public class CacheApplication { public static void main (String[] args) { SpringApplication.run(CacheApplication.class, args); } }
2.1.3 加入缓存注解 在StudentService接口中加入缓存注解
1 2 3 4 5 6 7 8 9 10 11 12 @CacheConfig(cacheNames = "student") @Repository public interface StudentService { @CachePut(key = "#p0.sno") Student updateStu (Student student) ; @CacheEvict(key = "#p0", allEntries = true) void deleteStuBySno (String stuno) ; @Cacheable(key = "#p0") Student queryStuBySno (String stuno) ; }
我们在StudentService接口中加入了@CacheConfig
注解,queryStuBySno方法使用了注解@Cacheable(key="#p0")
,即将id作为redis中的key值。当我们更新数据的时候,应该使用@CachePut(key="#p0.sno")
进行缓存数据的更新,否则将查询到脏数据,因为该注解保存的是方法的返回值,所以这里应该返回Student。
其实现类:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 @Repository("studentService") public class StudentServiceImpl implements StudentService { @Autowired private StudentMapper studentMapper; @Override public Student updateStu (Student student) { studentMapper.updateStu(student); return studentMapper.queryStuBySno(student.getStuno()); } @Override public void deleteStuBySno (String stuno) { studentMapper.deleteStuBySno(stuno); } @Override public Student queryStuBySno (String stuno) { return studentMapper.queryStuBySno(stuno); } }
再次执行测试类方法,执行结果如下:
1 2 3 4 5 2021-12-23 17:08:46.579 DEBUG 1468 --- [ main ] c.l.mapper.StudentMapper.queryStuBySno : ==> Preparing: select * from student where stuno = ? 2021-12-23 17:08:46.849 DEBUG 1468 --- [ main ] c.l.mapper.StudentMapper.queryStuBySno : ==> Parameters: 210 (String) 2021-12-23 17:08:46.880 DEBUG 1468 --- [ main ] c.l.mapper.StudentMapper.queryStuBySno : <== Total: 1 学号为[210]的学生姓名为:zhangsan 学号为[210]的学生姓名为:zhangsan
2.2 缓存注解 在Spring Boot中可使用的缓存注解有:
2.2.1 @CacheConfig
主要用于配置该类中会用到的一些共用的缓存配置。在上面@CacheConfig(cacheNames = "student")
:配置了该数据访问对象中返回的内容将存储于名为student的缓存对象中,我们也可以不使用该注解,直接通过@Cacheable
自己配置缓存集的名字来定义;
2.2.2 @Cacheable
配置了queryStuBySno函数的返回值将被加入缓存。同时在查询时,会先从缓存中获取,若不存在才再发起对数据库的访问。该注解主要有下面几个参数:
value
、cacheNames
:两个等同的参数(cacheNames为Spring 4新增,作为value的别名),用于指定缓存存储的集合名。由于Spring 4中新增了@CacheConfig
,因此在Spring 3中原本必须有的value属性,也成为非必需项了;
key
:缓存对象存储在Map集合中的key值,非必需,缺省按照函数的所有参数组合作为key值,若自己配置需使用SpEL表达式,比如:@Cacheable(key = "#p0")
:使用函数第一个参数作为缓存的key值,更多关于SpEL表达式的详细内容可参考 ;
condition
:缓存对象的条件,非必需,也需使用SpEL表达式,只有满足表达式条件的内容才会被缓存,比如:@Cacheable(key = "#p0", condition = "#p0.length() < 3")
,表示只有当第一个参数的长度小于3的时候才会被缓存;
unless
:另外一个缓存条件参数,非必需,需使用SpEL表达式。它不同于condition参数的地方在于它的判断时机,该条件是在函数被调用之后才做判断的,所以它可以通过对result进行判断;
keyGenerator
:用于指定key生成器,非必需。若需要指定一个自定义的key生成器,我们需要去实现org.springframework.cache.interceptor.KeyGenerator
接口,并使用该参数来指定;
cacheManager
:用于指定使用哪个缓存管理器,非必需。只有当有多个时才需要使用;
cacheResolver
:用于指定使用那个缓存解析器,非必需。需通过org.springframework.cache.interceptor.CacheResolver接口来实现自己的缓存解析器,并用该参数指定;
2.2.3 @CachePut
配置于函数上,能够根据参数定义条件来进行缓存,其缓存的是方法的返回值,它与@Cacheable
不同的是,它每次都会真实调用函数,所以主要用于数据新增和修改操作上。它的参数与@Cacheable
类似,具体功能可参考上面对@Cacheable
参数的解析;
2.2.4 @CacheEvict
配置于函数上,通常用在删除方法上,用来从缓存中移除相应数据。除了同@Cacheable
一样的参数之外,它还有下面两个参数:
allEntries
:非必需,默认为false。当为true时,会移除所有数据;
beforeInvocation
:非必需,默认为false,会在调用方法之后移除数据。当为true时,会在调用方法之前移除数据。
2.3 缓存实现 要使用Spring Boot的缓存功能,还需要提供一个缓存的具体实现。Spring Boot根据下面的顺序去侦测缓存实现:
Generic
JCache (JSR-107)
EhCache 2.x
Hazelcast
Infinispan
Redis
Guava
Simple
除了按顺序侦测外,我们也可以通过配置属性spring.cache.type
来强制指定。
接下来主要介绍基于Redis和Ehcache的缓存实现。
2.3.1 Redis 2.3.1.1 Redis准备工作 下载Redis ,Redis 支持 32 位和 64 位。这个需要系统平台的实际情况选择,这里我基于docker应用容器框架,拉取redis最新镜像,并启动容器
1 2 3 4 5 docker pull redis #拉取镜像 docker run -p 6379:6379 -v $PWD/data:/data -d redis:3.2 redis-server --appendonly yes #启动redis容器 docker exec -it redis01 bash #进入redis容器 redis-cli #执行redis-cli客户端命令
2.3.1.2 引入依赖 1 2 3 4 5 <dependency > <groupId > org.springframework.boot</groupId > <artifactId > spring-boot-starter-data-redis</artifactId > </dependency >
2.3.1.3 配置redis 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 spring: redis: database: 0 host: localhost port: 6379 pool: max-active: 8 max-wait: -1 max-idle: 8 min-idle: 0 timeout: 0
更多关于Spring Boot Redis可参考配置
2.3.1.4 redis配置类 通常我们都使用Json序列化后存入Redis,而SpringBoot1.x和SpringBoot2.x版本在自定义CacheManager
有很大的区别,需要自行研读源码。
在此简单说明,但不做源码详细分析。
在SpringBoot1.x中,RedisCacheManager
是可以使用RedisTemplate
作为参数注入的
1 2 3 4 5 @Bean public CacheManager cacheManager (RedisTemplate redisTemplate) { RedisCacheManager cacheManager = new RedisCacheManager(redisTemplate); return cacheManager; }
但在SpringBoot2.x中,有很大的不同,RedisCacheManager
构造器如下,已经无法再使用RedisTemplate
进行构造
官方文档中:
说明现在配置RedisCacheManager
需要一个RedisCacheConfiguration
来作为配置对象,通过RedisCacheConfiguration
这个对象来指定对应的序列化策略
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 @Configuration public class RedisConfig extends CachingConfigurerSupport { @Bean public KeyGenerator keyGenerator () { return new KeyGenerator() { @Override public Object generate (Object target, java.lang.reflect.Method method, Object... params) { StringBuffer sb = new StringBuffer(); sb.append(target.getClass().getName()); sb.append(method.getName()); for (Object obj : params) { sb.append(obj.toString()); } return sb.toString(); } }; } @Bean public CacheManager cacheManager (RedisConnectionFactory factory) { RedisSerializer<String> redisSerializer = new StringRedisSerializer(); 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); RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig() .entryTtl(Duration.ofDays(1 )) .serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(redisSerializer)) .serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(jackson2JsonRedisSerializer)) .disableCachingNullValues(); RedisCacheManager cacheManager = RedisCacheManager.builder(factory) .cacheDefaults(config) .build(); return cacheManager; } }
参考Redis的json序列化配置
2.3.1.5 测试 再次执行测试类方法,执行结果如下:
1 2 3 4 5 2021-12-24 14:48:38.169 DEBUG 10052 --- [ main ] c.l.mapper.StudentMapper.queryStuBySno : ==> Preparing: select * from student where stuno = ? 2021-12-24 14:48:38.660 DEBUG 10052 --- [ main ] c.l.mapper.StudentMapper.queryStuBySno : ==> Parameters: 210 (String) 2021-12-24 14:48:38.701 DEBUG 10052 --- [ main ] c.l.mapper.StudentMapper.queryStuBySno : <== Total: 1 学号为[210]的学生姓名为:zhangsan 学号为[210]的学生姓名为:zhangsan
2.3.2 Ehcache 2.3.2.1 引入依赖 1 2 3 4 5 <dependency > <groupId > net.sf.ehcache</groupId > <artifactId > ehcache</artifactId > </dependency >
2.3.2.2 新建ehcache.xml 在src/main/resources目录下新建ehcache.xml:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 <?xml version="1.0" encoding="UTF-8"?> <ehcache xmlns:xsi ="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation ="ehcache.xsd" > <defaultCache maxElementsInMemory ="10000" eternal ="false" timeToIdleSeconds ="3600" timeToLiveSeconds ="0" overflowToDisk ="false" diskPersistent ="false" diskExpiryThreadIntervalSeconds ="120" /> <cache name ="student" maxEntriesLocalHeap ="2000" eternal ="false" timeToIdleSeconds ="3600" timeToLiveSeconds ="0" overflowToDisk ="false" statistics ="true" /> </ehcache >
ehcache说明 关于Ehcahe的一些说明:
name
:缓存名称。
maxElementsInMemory
:缓存最大数目
maxElementsOnDisk
:硬盘最大缓存个数。
eternal
:对象是否永久有效,一但设置了,timeout将不起作用。
overflowToDisk
:是否保存到磁盘。
timeToIdleSeconds
:设置对象在失效前的允许闲置时间(单位:秒)。仅当eternal=false
对象不是永久有效时使用,可选属性,默认值是0,也就是可闲置时间无穷大。
timeToLiveSeconds
:设置对象在失效前允许存活时间(单位:秒)。最大时间介于创建时间和失效时间之间。仅当eternal=false
对象不是永久有效时使用,默认是0,也就是对象存活时间无穷大。
diskPersistent
:是否缓存虚拟机重启期数据,默认值为false。
diskSpoolBufferSizeMB
:这个参数设置DiskStore(磁盘缓存)的缓存区大小。默认是30MB。每个Cache都应该有自己的一个缓冲区。
diskExpiryThreadIntervalSeconds
:磁盘失效线程运行时间间隔,默认是120秒。
memoryStoreEvictionPolicy
:当达到maxElementsInMemory限制时,Ehcache将会根据指定的策略去清理内存。默认策略是LRU(最近最少使用)。你可以设置为FIFO(先进先出)或是LFU(较少使用)。
clearOnFlush
:内存数量最大时是否清除。
memoryStoreEvictionPolicy:Ehcache
的三种清空策略:FIFO ,first in first out,这个是大家最熟的,先进先出。LFU , Less Frequently Used,就是上面例子中使用的策略,直白一点就是讲一直以来最少被使用的。如上面所讲,缓存的元素有一个hit属性,hit值最小的将会被清出缓存。LRU ,Least Recently Used,最近最少使用的,缓存的元素有一个时间戳,当缓存容量满了,而又需要腾出地方来缓存新的元素的时候,那么现有缓存元素中时间戳离当前时间最远的元素将被清出缓存。
2.3.2.3 配置application.yml 1 2 3 4 spring: cache: ehcache: config: 'classpath:ehcache.xml'
2.3.2.4 测试 对于Ehcache来说,更新方法加不加@CachePut
注解,结果都一样。
点击这里 查看源码