一个较大的项目,如果用户数量不断的增多,而程序里都是直接操作数据库的话,并定会造成数据库出现瓶颈,无法处理高并发的问题。此时使用缓存是解决问题的一个良好办法之一,读取缓存的数据的速度往往比连接数据库查询快很多。
在 pom.xml 配置文件加上 jar 依赖:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-cache</artifactId>
</dependency>
在SpringBoot主类中增加 @EnableCaching 注解开启缓存功能:
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cache.annotation.EnableCaching;
//@MapperScan("com.test.dao") //在启动类添加扫描 Mybatis 层的注解或者在 Dao 层添加 @Mapper 注解
@SpringBootApplication
@EnableCaching
public class MyTest02Application {
public static void main(String[] args) {
SpringApplication.run(MyTest02Application.class, args);
}
}
我们在 Dao 层的类里添加注解 @CacheConfig(cacheNames = "userDaoCache"),在查询数据库的方法里添加注解:@Cacheable 完整代码:
package com.test.dao;
import com.test.entity.User;
import org.apache.ibatis.annotations.Mapper;
import org.springframework.cache.annotation.CacheConfig;
import org.springframework.cache.annotation.Cacheable;
import java.util.List;
@CacheConfig(cacheNames = "userDaoCache")
@Mapper
public interface UserDao {
/**
* 新增用户
* @param user
* @return
*/
Integer addUserMybatis(User user);
/**
* 根据id删除用户
* @param id
* @return
*/
Integer deleteUserMybatis(Integer id);
/**
* 更新用户信息
* @param user
* @return
*/
Integer updateUserMybatis(User user);
/**
* 查询所有用户信息
* @return
*/
@Cacheable
List<User> getUserMybatis();
}
为了测试效果,我们在 Controller 层里查询方法打印一些标记:
/**
* 查询所有用户信息
* @return
*/
@RequestMapping(value = "/getUserMybatis")
public List<User> getUserMybatis(){
List<User> list = userServiceMybatis.getUserMybatis();
System.out.println("执行查询完毕,时间戳="+System.currentTimeMillis());
return list;
}
O的K,启动测试类,用 postman 测试查询功能:
查看控制台信息:
JDBC Connection [HikariProxyConnection@827904177 wrapping com.mysql.jdbc.JDBC4Connection@58cf54c7] will not be managed by Spring
==> Preparing: select * from t_user
==> Parameters:
<== Columns: id, name, signature
<== Row: 1, 流放深圳, 让天下没有难写的代码
<== Row: 4, 腾讯NBA, 让我遇见你
<== Row: 5, 农夫山泉, 大自然的搬运工
<== Row: 7, 小米, 为发烧而生
<== Total: 4
Closing non transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@44993039]
执行查询完毕,时间戳=1539915266526
然后,我们使用更新用户的功能,把id=4的用户修改:
然后,再次点击查询,再查看控制台信息:
查看查询的数据,发现并不是我们更新后的数据,而是缓存的旧数据:
说明:
①@CacheConfig(cacheNames = "userDaoCache"):配置了该数据访问对象中返回的内容将存储于名为userDaoCache的缓存对象中。
②@Cacheable:配置了 getUserMybatis 方法的返回值将被加入缓存。在查询时,会先从缓存中获取,若不存在才去查询数据库获取数据。
按住 Ctrl + 鼠标左键,进入 @Cacheable 注解,可以看到如下的内容:
@Target({ElementType.METHOD, ElementType.TYPE})
@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;
}
1、value、cacheNames:两个等同的参数,用于指定缓存存储的集合名。由于Spring 4中新增了@CacheConfig
key:缓存对象存储在Map集合中的key值,非必需,缺省按照函数的所有参数组合作为key值,若自己配置需使用SpEL表达式,比如:@Cacheable(key = "#A1"):使用函数第一个参数作为缓存的key值。
2、condition:缓存对象的条件,非必需,也需使用SpEL表达式,只有满足表达式条件的内容才会被缓存,比如:@Cacheable(key = "#A1", condition = "#A1.length() < 3"),表示只有当第一个参数的长度小于3的时候才会被缓存。
3、unless:另外一个缓存条件参数,非必需,需使用SpEL表达式。它不同于condition参数的地方在于它的判断时机,该条件是在函数被调用之后才做判断的,所以它可以通过对result进行判断。
4、keyGenerator:用于指定key生成器,非必需。若需要指定一个自定义的key生成器,我们需要去实现org.springframework.cache.interceptor.KeyGenerator接口,并使用该参数来指定。需要注意的是:该参数与key是互斥的
5、cacheManager:用于指定使用哪个缓存管理器,非必需。只有当有多个时才需要使用
6、cacheResolver:用于指定使用那个缓存解析器,非必需。需通过org.springframework.cache.interceptor.CacheResolver接口来实现自己的缓存解析器,并用该参数指定。
在SpringBoot中通过启动类的 @EnableCaching 注解自动化配置合适的缓存管理器(CacheManager),SpringBoot根据下面的顺序去侦测缓存提供者:Generic、JCache (JSR-107)、EhCache 2.x、Hazelcast、Infinispan、Redis、Guava、Simple。
除了按顺序监测外,我们可以通过配置文件 spring.cache.type 来指定缓存的类型。比如我们在 application.properties 配置文件里添加如下配置,指定使用 ehcache 缓存:
spring.cache.type=ehcache
spring.cache.ehcache.config=classpath:cache/ehcache.xml
在SpringBoot中开启EhCache比较简单,只需要在工程中加入 ehcache.xml 配置文件并在 pom.xml 中增加ehcache依赖,框架只要发现该文件,就会创建EhCache的缓存管理器。
在 pom.xml 配置文件中添加 jar 依赖:
<dependency>
<groupId>net.sf.ehcache</groupId>
<artifactId>ehcache</artifactId>
</dependency>
然后在 resources 目录下创建 cache 文件夹,在cache 文件夹下创建 ehcache.xml 文件:
<?xml version="1.0" encoding="UTF-8"?>
<ehcache updateCheck="false" name="defaultCache">
<!-- 磁盘存储位置:会在对应的目录下生成相应的缓存文件 -->
<diskStore path="src/main/resources/cache/temp"/>
<!-- 默认缓存配置. -->
<defaultCache maxEntriesLocalHeap="100" eternal="false" timeToIdleSeconds="300" timeToLiveSeconds="600"
overflowToDisk="true" maxEntriesLocalDisk="100000"/>
<!-- 用户系统缓存 -->
<cache name="cacheUserName" maxEntriesLocalHeap="100" eternal="true" overflowToDisk="true"/>
</ehcache>
说明:在 <cache name="***"> </cache> 定义好全局缓存的名字,就不需要在 Dao 层方法再指定名称了,也无需在方法里增加什么注解。因此,UserDao 就可以解放:
package com.test.dao;
import com.test.entity.User;
import org.apache.ibatis.annotations.Mapper;
import java.util.List;
@Mapper
public interface UserDao {
/**
* 新增用户
* @param user
* @return
*/
Integer addUserMybatis(User user);
/**
* 根据id删除用户
* @param id
* @return
*/
Integer deleteUserMybatis(Integer id);
/**
* 更新用户信息
* @param user
* @return
*/
Integer updateUserMybatis(User user);
/**
* 查询所有用户信息
* @return
*/
List<User> getUserMybatis();
}
在类 MybatisController 里增加一个测试缓存的方法 cacheTest,并修改 getUserMybatis 方法:
@Autowired(required = false)
private CacheManager cacheManager;
private final static String CACHE_NAME = "cacheUserName";
private final static String GET_CACHE_KEY = "getCacheKey";
/**
* 测试缓存
* @return
*/
@RequestMapping(value = "/cacheTest")
public List<User> cacheTest(){
Cache cache = cacheManager.getCache(CACHE_NAME);
if(cache.get(GET_CACHE_KEY) != null){
List<User> list = (List)cache.get(GET_CACHE_KEY).get();
return list;
}else{
return new ArrayList<>();
}
}
/**
* 查询所有用户信息
* @return
*/
@RequestMapping(value = "/getUserMybatis")
public List<User> getUserMybatis()throws Exception{
List<User> list;
Cache cache = cacheManager.getCache(CACHE_NAME);
//如果缓存里有数据,则取缓存的数据;如果缓存里没有数据,则查询数据库,并将结果放入缓存中。
if(cache.get(GET_CACHE_KEY) == null){
list = userServiceMybatis.getUserMybatis();
cache.put(GET_CACHE_KEY,list);
}else{
list = (List)cache.get(GET_CACHE_KEY).get();
}
System.out.println("执行查询完毕,时间戳="+System.currentTimeMillis());
return list;
}
说明:cacheUserName 对应的是配置文件 ehcache.xml 里定义的缓存name。
启动服务,用 postman 测试,http://localhost:9090/getUserMybatis:
发现有问题:
O的K,那就把 User 实体类序列化,很简单,就是实现序列化接口即可: implements Serializable
public class User implements Serializable
重启,再次测试 http://localhost:9090/getUserMybatis:完美无误。这时候在目录下也生成了缓存文件:
测试:http://localhost:9090/cacheTest,完美无误。
接下来学习一下,如何清空缓存:
/**
* 清空缓存
* @return
*/
@RequestMapping(value = "/clearCache")
public Map<String,Boolean> clearCache(){
Cache cache = cacheManager.getCache(CACHE_NAME);
cache.clear();
Map<String, Boolean> map = new HashMap<>();
map.put("flag", true);
return map;
}
重启,测试,先运行 http://localhost:9090/getUserMybatis,缓存里有数据,再测试:http://localhost:9090/clearCache
再测试查询用户: http://localhost:9090/getUserMybatis,在 debug 模式下打断点,可以看到缓存里没有数据,重新去数据库查询数据了。
这样的设计适用于一种场景:把用户经常访问的内容加入到缓存,能加快数据的获取,提高用户的使用体验。当需要修改内容时,可以清空缓存的数据。第一个用户点击获取,查询缓存没有数据了,就去数据库查询数据,然后加入缓存。为了保证“顺时”问题,可以使用数据覆盖的形式,不一定需要清空,可以使用 put 方法进行覆盖。