不管是什么类型的应用程序,都离不开数据,即便如现在的手机APP,我们依然需要使用数数据库,对于不懂的人,当然,我们可以告诉他们一些高大上的概念,但是作为专业人士,就一定要明白背后的真实原理到底是什么。
关于cache的故事,这里就不做过多的介绍了,我们直接进入主题,关于Spring Boot中对cache的支持。
一、Spring 缓存支持
首先,在Spring中,定义了org.springframework.cache.CacheManager
和org.springframework.cache.Cache
接口用来统一不同的缓存的技术。其中,CacheManager
是Spring提供的各种缓存技术抽象接口,Cache接口包含缓存的各种操作(增加、删除、获取,我们一般不会直接和此接口打交道)。
1.Spring支持的CacheManager
针对不同的缓存技术,需要实现不同的CacheManager,Spring定义了如下所示的CacheManager实现,第一步、先找到CacheManager接口:
只有两个方法。我们看看哪些方法实现了它:
说明:
SimpleCacheManager:使用简单的Collection集合来存储,一般用于测试
JCacheCacheManager:使用JCacheCache组件进行缓存
EhCacheCacheManager:使用EhCacheCache组件进行缓存
CompositeCacheManager:使用CompositeCache组件进行缓存
CaffeineCacheManager:使用CaffeineCache组件缓存,目前官方推荐使用
NoOpCacheManager:测试使用,不缓存数据
TransactionAwareCacheManagerProxy:这个是事务管理里的代理适配类,和Spring的事务管理绑定使用
ConcurrentMapCacheManager:使用ConcurrentMapCacheManager来存储缓存数据
第二步,在我们单独使用任意一个实现cacheManager接口的实现时候,都需要需注册实现的CacheManager的Bean,例如:
@Bean
public EhCacheCacheManager cacheManager(CacheManager ehCacheCacheManager) {
return new EhCacheCacheManager(ehCacheCacheManager);
}
当然,每种缓存技术都有很多的额外配置,但配置cacheManager是必不可少的。
2.声名式缓存注解
Spring提供了4个注解来声明缓存规则(是应用AOP的绝佳例子),如下所示:
@Cacheable:声明方法是可缓存的。将结果存储到缓存中以便后续使用相同参数调用时不需执行实际的方法。直接从已有的缓存中取值。
@CacheEvict:用于从缓存中清除过期、指定或未使用的数据。 此外,还提供了一个额外的参数allEntries 。表示是否需要清除缓存中的所有元素。默认为false,表示不需要。当指定了allEntries为true时,Spring Cache将忽略指定的key。
@CachePut:如果缓存需要更新,且不干扰方法的执行,可就要使用它。它标注的方法在执行前不会去检查缓存中是否存在之前执行过的结果,而是每次都会执行该方法,并将执行结果以键值对的形式存入指定的缓存中。
@Caching:重组多个缓存操作以应用于方法,具体没怎么用过。
@CacheConfig:有时候一个类中可能会有多个缓存操作,而这些缓存操作可能是重复的,可使用配置文件的方式。
其中,@Cacheable、@CachePut、@CacheEvit都有value属性,指定的是要使用的缓存名称;key属性指定的是数据在缓存中的存储的键。
3.开启声名式缓存支持
开启声名式缓存支持很简单,只需在配置类上使用@EnableCaching注解即可,例如:
@Configuration
@EnableCaching
public class config {
...
}
二、Spring Boot的支持
在Spring中使用缓存技术的关键是配置CacheManager,而Spring Boot为我们自动配置了多个CacheManager的实现。换句话说,使用boot来开启缓存的快捷性就体现出来了。
Spring Boot的CacheManager的自动配置放置在org.springframework.boot.autoconfigure.cache包中,如下图所示:
从图中我们能看出,Spring Boot为我们自动配置了——
EhCacheCacheConfiguration(使用EhCache)
GenericCacheConfiguration(使用Collection)
GuavaCacheConfiguration(使用Guava)
HazelcastCacheConfiguration(使用Hazelcast)
InfinispanCacheConfiguration(使用Infinispan)
JCacheCacheConfiguration(使用JCache)
NoOpCacheConfiguration(不使用存储)
RedisCacheConfiguration(使用Redis)
SimpleCacheConfiguration(使用ConcurrentMap)
在不做任何额外配置的情况下,默认使用的是SimpleCacheConfiguration
,即使用ConcurrentMapCacheManager
。Spring Boot支持以“spring.cache”为前缀的属性来配置缓存。
spring.cache.type= # 可选generic, ehcache, hazelcast, infinispan, jcache, redis, guava, simple, none
spring.cache.cache-names= # 程序启动时创建缓存名称
spring.cache.caffeine.spec= #配置Caffeine Cache(没有默认实现原来的Guava Cache,现在官方推荐使用它)
spring.cache.ehcache.config= # ehcache配置文件地址
spring.cache.infinispan.config= # infinispan 配置文件地址
spring.cache.jcache.config=# jcache 配置文件地址
spring.cache.jcache.provider= #当多个 jcache实现在类路径中的时候,指定jcache实现
spring.cache.couchbase.expiration= #分布式存储couchbase过期时间
spring.cache.redis.cache-null-values= #是否允许缓存null值
spring.cache.redis.key-prefix= #缓存的前缀
spring.cache.redis.time-to-live= #到期时间。默认情况下,这些条目永不过期
spring.cache.redis.use-key-prefix= #在写入Redis时是否使用密钥前缀
在Spring Boot环境下,使用缓存技术只需在项目中导入相关缓存技术的依赖包,并在配置类使用@EnableCaching开启缓存支持即可。
不过要注意的是,2.0版本后部分过去常用的自动配置,官方没有再默认实现了,而是让用户自行实现,为的是能更好的按需定制。
三、实战
本例将以Spring Boot默认的ConcurrentMapCacheManager作为缓存技术,演示@Cacheable、@CachePut、@CacheEvit。
第一步:建立Spring Boot项目
这里我们已经实战过多次,先建立控制器ManController
,代码如下:
@SpringBootApplication
@RestController
@EnableCaching
public class ManController {
@Autowired
private ManServiceMybatis manService;
public static void main(String[] args) {
SpringApplication.run(ManController.class,args);
}
@RequestMapping("/save/{values}")//类似mickjoust,22,123131
public Man put(@PathVariable("values") String mockString){
String[] str = mockString.split("[,]");
Man m = new Man();
m.setName(str[0]);
m.setAge(Integer.valueOf(str[1]));
m.setPhone(str[2]);
return manService.save(m);
}
@RequestMapping("/{id}")
public Man cacheable(@PathVariable("id") int id){
Man man = new Man();
return manService.findOne(man);
}
@RequestMapping("/evit/{id}")
public String evit(@PathVariable("id") int id){
manService.remove(id);
return "ok";
}
}
第二步:建立数据服务
这里我们简单复习一下Mybatis+Spring Boot的集成,同时在本地安装一个MYSQL数据库进行测试。首先,dao接口代码ManDao:
@Mapper
public interface ManDao<T,ID> {
void save(T t);
void deleteById(ID id);
T findById(ID id);
}
其次,Mapper映射的XML文件ManDaoMapper.xml
:
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<!-- namespace必须指向Dao接口 -->
<mapper namespace="com.hjf.boot.demo.cache.ManDao">
<!-- 所有列 -->
<sql id="Column_list">
id,
name,
age,
phone
</sql>
<sql id="insertCol">
name,
age,
phone
</sql>
<sql id="insertColValues">
#{name},
#{age},
#{phone}
</sql>
<sql id ="TableName">
mandata
</sql>
<resultMap id="resultList" type="Man" >
<id column="id" property="id" />
<result column="name" property="name" />
<result column="age" property="age" />
<result column="phone" property="phone" />
</resultMap>
<parameterMap id="params" type="Man">
<parameter property="name"/>
<parameter property="age" />
<parameter property="phone" />
</parameterMap>
<!-- 创建数据 -->
<insert id="save" parameterType="Man" useGeneratedKeys="true" keyProperty="id">
INSERT INTO
<include refid="TableName" />
(<include refid="insertCol" />)
VALUES
(<include refid="insertColValues" />)
</insert>
<!-- 根据ID查询数据 -->
<select id="findById" parameterType="Man" resultType="Man">
SELECT
<include refid="Column_list" />
FROM
<include refid="TableName"/>
WHERE id = #{id}
</select>
<!-- 根据ID删除 -->
<delete id="deleteById" parameterType="Man" >
DELETE FROM
<include refid="TableName" />
WHERE id = #{id}
</delete>
</mapper>
最后,在application.properties中快速配置jdbc的链接(三板斧最后的starter配置的作用就体现出来了,过去还要配更多的文件):
spring.datasource.driver-class-name=com.mysql.jdbc.Driver
//因为host的原因需要加上+8区的时间差&serverTimezone=GMT%2B8,没有这个问题的可以不要
spring.datasource.url=jdbc:mysql://localhost:3306/testdata?useUnicode=true&characterEncoding=UTF-8&serverTimezone=GMT%2B8
spring.datasource.username=mickjoust
spring.datasource.password=123456
spring.datasource.max-active=10
spring.datasource.max-idle=5
spring.datasource.min-idle=0
mybatis.mapper-locations=classpath:/*Mapper.xml
mybatis.type-aliases-package=com.hjf.boot.demo.cache
最后的最后,安装mysql,并建一个表名为testdata。
第三步:开启缓存
建一个service,支持新增、删除、查询,并使用声明式注解开启缓存。
ManServiceMybatis.class
代码:
@Service
public class ManServiceMybatis {
@Autowired
private ManDao<Man,Integer> manDao;
@CachePut(value = "man",key="#man.id") //1
public Man save(Man man) {
manDao.save(man);//不需要单独添加,直接映射到对象里的id
System.out.println("为id、key为:"+man.getId()+"数据做了缓存");
return man;
}
@CacheEvict(value = "man") //2
public void remove(int id) {
System.out.println("删除了id、key为"+id+"的数据缓存");
manDao.deleteById(id);
}
@Cacheable(value="man",key="#man.id") //3
public Man findOne(Man man) {
Man manr = manDao.findById(man.getId());
System.out.println("为id、key为:"+manr.getId()+"数据做了缓存");
return Optional.of(manr).orElse(null);
}
}
第四步:测试
1、创建数据,很简单,使用我们约定的方式用字符串来模拟body数据。(为什么要这样?因为避免使用postman这样的工具)
这时,已经被缓存了。
2、然后,我们读取39号数据数据,如下图:
没有打印,说明创建时的缓存起作用了。接下来,我们再查下其它数据,比如,2号数据,如下图:
有数据,但是我们要看看后台有没有打印?有,做了读取缓存。
再查询就不会有打印了,说明已经被缓存起来了。
3、删除数据
我们再试试删除时,这个删除是同时把缓存也清除了。
看到了吧,已经删除了。
再查询,就会报错了。
小结
本文从Spring对cache的支持实现说起,然后说到了Spring Boot中支持的对CacheManager接口的默认实现与集成,然后实战了一个标准的cache缓存的web api的例子。
参考资源
1、Spring Boot 官方文档:Cache小节
2、CacheManager 接口文档
3、Spring 5.x官方文档:Cache小节
我的其它穿越门——持续践行,我们一路同行。
头条号:说言风语
简书ID:mickjoust
公号:说言风语