Spring Boot中使用EhCache实现缓存支持
- SpringBoot提供数据缓存功能的支持,提供了一系列的自动化配置,使我们可以非常方便的使用缓存。,相信非常多人已经用过cache了。因为数据库的IO瓶颈。一般情况下我们都会引入非常多的缓存策略,例如引入redis,引入hibernate的二级缓存等。SpringBoot在annotation的层面给我们实现了cache,得益于Spring的AOP。所有的缓存配置只是在annotation层面配置,完全没有侵入到我们的代码当中,就像我们的声明式事务一样。
- Spring定义了CacheManager和Cache接口统一不同的缓存技术。其中CacheManager是Spring提供的各种缓存技术的抽象接口。而Cache接口包含缓存的各种操作,当然我们一般情况下不会直接操作Cache接口。
Spring针对不同的缓存技术,需要实现不同的cacheManager,Spring定义了如下的cacheManger实现:
CacheManger | 描述 |
SimpleCacheManager | 使用简单的Collection来存储缓存,主要用于测试 |
ConcurrentMapCacheManager | 使用ConcurrentMap作为缓存技术(默认) |
NoOpCacheManager | 测试用 |
EhCacheCacheManager | 使用EhCache作为缓存技术,以前在hibernate的时候经常用 |
GuavaCacheManager | 使用google guava的GuavaCache作为缓存技术 |
HazelcastCacheManager | 使用Hazelcast作为缓存技术 |
JCacheCacheManager | 使用JCache标准的实现作为缓存技术,如Apache Commons JCS |
RedisCacheManager | 使用Redis作为缓存技术 |
Cache注解详解
@CacheConfig:主要用于配置该类中会用到的一些共用的缓存配置。在这里@CacheConfig(cacheNames = "users"):配置了该数据访问对象中返回的内容将存储于名为users的缓存对象中,我们也可以不使用该注解,直接通过@Cacheable自己配置缓存集的名字来定义。
@Cacheable 在方法执行前Spring先是否有缓存数据,如果有直接返回。如果没有数据,调用方法并将方法返回值存放在缓存当中。
@Cacheable:配置了findByName函数的返回值将被加入缓存。同时在查询时,会先从缓存中获取,若不存在才再发起对数据库的访问。该注解主要有下面几个参数:
- 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的时候才会被缓存,若做此配置上面的AAA用户就不会被缓存,读者可自行实验尝试。
- unless:另外一个缓存条件参数,非必需,需使用SpEL表达式。它不同于condition参数的地方在于它的判断时机,该条件是在函数被调用之后才做判断的,所以它可以通过对result进行判断。
- keyGenerator:用于指定key生成器,非必需。若需要指定一个自定义的key生成器,我们需要去实现org.springframework.cache.interceptor.KeyGenerator接口,并使用该参数来指定。需要注意的是:该参数与key是互斥的
- cacheManager:用于指定使用哪个缓存管理器,非必需。只有当有多个时才需要使用
- cacheResolver:用于指定使用那个缓存解析器,非必需。需通过org.springframework.cache.interceptor.CacheResolver接口来实现自己的缓存解析器,并用该参数指定。
@CachePut:配置于函数上,能够根据参数定义条件来进行缓存,它与@Cacheable不同的是,它每次都会将方法返回值放入缓存,所以主要用于数据新增和修改操作上。它的参数与@Cacheable类似,具体功能可参考上面对@Cacheable参数的解析。
@CacheEvict:配置于函数上,通常用在删除方法上,用来从缓存中移除相应数据。除了同@Cacheable一样的参数之外,它还有下面两个参数:
- allEntries:非必需,默认为false。当为true时,会移除所有数据
- beforeInvocation:非必需,默认为false,会在调用方法之后移除数据。当为true时,会在调用方法之前移除数据。
配置缓存
- 引入缓存依赖
在pom.xml中引入cache依赖,添加如下内容:
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-cache</artifactId> </dependency>
- 开启缓存
在Spring Boot主类中通过@EnableCaching注解自动化配置合适的缓存管理器(CacheManager),开启缓存功能,如下:
@SpringBootApplication @ComponentScan(basePackages={"com.gzh.*"}) @EnableCaching public class Application { public static void main(String[] args) { SpringApplication.run(Application.class, args); } }
- 使用注解缓存数据
在数据访问接口中,增加缓存配置注解,如:
@Mapper public interface UserMapper { @Select("SELECT * FROM T_USER WHERE id = #{id}") UserVO findById(@Param("id") int id); @Select("SELECT * FROM T_USER WHERE name = #{name,jdbcType=VARCHAR}") UserVO findByName(@Param("name") String name); @Results({ @Result(property="id",column="id"), @Result(property = "name", column = "name"), @Result(property = "age", column = "age") }) @Select("SELECT id, name, age FROM T_USER") List<UserVO> findAll(); @Insert("INSERT INTO T_USER(name, age) VALUES(#{name}, #{age})") int insert(@Param("name") String name, @Param("age") Integer age); @Update("UPDATE T_USER SET age=#{age,jdbcType=INTEGER},name=#{name,jdbcType=VARCHAR} WHERE id=#{id,jdbcType=INTEGER}") void update(UserVO userVO); @Delete("DELETE FROM T_USER WHERE id =#{id,jdbcType=INTEGER}") void delete(int id); @Delete("DELETE FROM T_USER") void deleteAll(); }
新建IUserService接口类,如下:
@CacheConfig(cacheNames="user") public interface IUserService { //有一个尤其需要注意的坑:Spring默认的SimpleKeyGenerator是不会将函数名组合进key中的,即多个方法设置@Cacheable("databaseCache"),输出的key是一样的 /** * 新增一个用户 * @param name * @param age */ @CachePut(value="user",keyGenerator="cacheKeyGenerator") void create(String name, int age); /** * 根据id删除一个用户 * @param name */ @CacheEvict(value="user",keyGenerator="cacheKeyGenerator") void deleteById(int id); /** * 删除所有信息 */ @CacheEvict(keyGenerator="cacheKeyGenerator",allEntries=true) void deleteAll(); /** * 更新用户信息 * @param userVO */ @CachePut(keyGenerator="cacheKeyGenerator",cacheNames="user") void update(UserVO userVO); /** * 获取用户列表 * @return */ @Cacheable(keyGenerator="cacheKeyGenerator") List<UserVO> findAll(); /** * 根据Id查询用户信息 * @param id * @return */ @Cacheable(keyGenerator="cacheKeyGenerator") UserVO findById(int id); /** * 根据名称查询用户信息 * @param name * @return */ @Cacheable(keyGenerator="cacheKeyGenerator") UserVO findByName(String name); }
备注:大家可以先不配置keyGenerator属性,可以指定简单key。keyGenerator属性是我后边测试所用。
UserService实现类:
@Service public class UserService implements IUserService { @Autowired private UserMapper mapper; @Override public void create(String name, int age) { mapper.insert(name, age); } @Override public void deleteById(int id) { mapper.delete(id); } @Override public void deleteAll() { mapper.deleteAll(); } @Override public List<UserVO> findAll() { List<UserVO> list = mapper.findAll(); return list; } @Override public void update(UserVO userVO) { mapper.update(userVO); } @Override public UserVO findById(int id) { UserVO userVO = mapper.findById(id); return userVO; } @Override public UserVO findByName(String name) { UserVO userVO = mapper.findByName(name); return userVO; } }
新建单元测试类:
@RunWith(SpringJUnit4ClassRunner.class) @SpringApplicationConfiguration(Application.class) public class CacheApplicationTest { private static final Logger LOG = Logger.getLogger(CacheApplicationTest.class); @Autowired private IUserService iUserService; @Autowired private CacheManager cacheManager; @Before public void setUp(){ //检查使用的Cache LOG.info("Cache Manager ===== "+cacheManager.getClass().getName()); //删除所有数据 iUserService.deleteAll(); //添加用户信息 iUserService.create("guanguan", 20); iUserService.create("lindapang", 21); iUserService.create("xiaoyan", 18); } @Test public void userAddTest(){ UserVO user = iUserService.findByName("guanguan"); LOG.info("第一次查询用户信息=="+user.toString()); user = iUserService.findByName("guanguan"); LOG.info("第二次查询用户信息=="+user.toString()); } }
执行单元测试,可以在控制台看到输入如下内容:
从日志中,我们发现spring boot开启的缓存已经生效,第一次都执行了访问数据库的操作,第二次执行缓存。
完成上边案例后,大家肯定会想,spring boot是如何实现缓存的,使用的是什么缓存,带着这个疑问,我们继续往下看。
其实常规的SpringBoot已经为我们自动配置了EhCache、Collection、Guava、ConcurrentMap等缓存,默认使用SimpleCacheConfiguration,即使用ConcurrentMapCacheManager。在Spring Boot中通过@EnableCaching注解自动化配置合适的缓存管理器(CacheManager),Spring Boot根据下面的顺序去侦测缓存提供者:
• Generic
• JCache
• EhCache
• Hazelcast
• Infinispan
• Redis
• Guava
• Simple
除了按顺序侦测外,我们也可以通过配置属性spring.cache.type来强制指定。SpringBoot的application.properties配置文件,使用spring.cache前缀的属性进行配置。
spring.cache.type=#缓存的技术类型
spring.cache.cache-names=应用程序启动创建缓存的名称
spring.cache.ehcache.config=ehcache的配置文件位置
spring.cache.infinispan.config=infinispan的配置文件位置
spring.cache.jcache.config=jcache配置文件位置
spring.cache.jcache.provider=当多个jcache实现类时,指定选择jcache的实现类
这里不适用默认的ConcurrentMapCache 而是使用 EhCache,看看如何配置来使用EhCache进行缓存管理。
配置EhCache缓存
- 添加EhCache缓存依赖
<!-- https://mvnrepository.com/artifact/net.sf.ehcache/ehcache --> <dependency> <groupId>net.sf.ehcache</groupId> <artifactId>ehcache</artifactId> <version>2.10.4</version> </dependency>
- 创建EhCache缓存配配置文件ehcache.xml
在src/main/resources目录下创建:ehcache.xml
<ehcache xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="http://ehcache.org/ehcache.xsd"> <!-- 指定一个文件目录,当EHCache把数据写到硬盘上时,将把数据写到这个文件目录下 --> <diskStore path="java.io.tmpdir"/> <!-- 设定缓存的默认数据过期策略 --> <cache name="users" maxElementsInMemory="10000" maxEntriesLocalHeap="200M" timeToLiveSeconds="600"/> <defaultCache maxElementsInMemory="10000" eternal="false" overflowToDisk="true" timeToIdleSeconds="10" timeToLiveSeconds="120" diskPersistent="false" memoryStoreEvictionPolicy="LRU" diskExpiryThreadIntervalSeconds="120"/> <!-- maxElementsInMemory 内存中最大缓存对象数,必须的配置 --> <!-- maxEntriesLocalHeap是用来限制当前缓存在堆内存上所能保存的最大元素数量的--> <!-- eternal:true表示对象永不过期,此时会忽略timeToIdleSeconds和timeToLiveSeconds属性,默认为false --> <!-- maxElementsOnDisk:硬盘中最大缓存对象数,若是0表示无穷大 --> <!-- overflowToDisk:true表示当内存缓存的对象数目达到了maxElementsInMemory界限后, 会把溢出的对象写到硬盘缓存中。注意:如果缓存的对象要写入到硬盘中的话,则该对象必须实现了Serializable接口才行。--> <!-- diskSpoolBufferSizeMB:磁盘缓存区大小,默认为30MB。每个Cache都应该有自己的一个缓存区。--> <!-- diskPersistent:是否缓存虚拟机重启期数据 --> <!-- diskExpiryThreadIntervalSeconds:磁盘失效线程运行时间间隔,默认为120秒 --> <!-- timeToIdleSeconds: 设定允许对象处于空闲状态的最长时间,以秒为单位。当对象自从最近一次被访问后, 如果处于空闲状态的时间超过了timeToIdleSeconds属性值,这个对象就会过期, EHCache将把它从缓存中清空。只有当eternal属性为false,该属性才有效。如果该属性值为0, 则表示对象可以无限期地处于空闲状态 --> <!-- timeToLiveSeconds:设定对象允许存在于缓存中的最长时间,以秒为单位。当对象自从被存放到缓存中后, 如果处于缓存中的时间超过了 timeToLiveSeconds属性值,这个对象就会过期, EHCache将把它从缓存中清除。只有当eternal属性为false,该属性才有效。如果该属性值为0, 则表示对象可以无限期地存在于缓存中。timeToLiveSeconds必须大于timeToIdleSeconds属性,才有意义 --> <!-- memoryStoreEvictionPolicy:当达到maxElementsInMemory限制时, Ehcache将会根据指定的策略去清理内存。可选策略有:LRU(最近最少使用,默认策略)、 FIFO(先进先出)、LFU(最少访问次数)。--> </ehcache>
- 在application.properties添加如下配置
spring.cache.type=ehcache
spring.cache.ehcache.config=classpath:ehcache.xml
- 在Spring Boot启动类上添加@EnableCaching注解
至此,我们所有的配置项都基本上配置完成。回到刚才IUserService接口类,我们使用缓存注解时使用到缓存的key,这个key是用来分辨同一个缓存中的缓存数据的。key是可以自己制定的,也可以通过自定义一个KeyGenerator来进行生成。
注解上key的几种形式如下:
基本形式:
1.@Cacheable(value="cacheName", key"#id")
2.public ResultDTO method(int id);
组合形式:
1.@Cacheable(value="cacheName", key"T(String).valueOf(#name).concat('-').concat(#password))
2.public ResultDTO method(int name, String password);
对象形式:
1.@Cacheable(value="cacheName", key"#user.id)
2.public ResultDTO method(User user);
自定义Key生成器:
1.@Cacheable(value="gomeo2oCache", keyGenerator = "keyGenerator")
2.public ResultDTO method(User user);
这里我们探讨下最后一种,自定义key。key可以为任何对象,我们要考虑的只有一件事,两个key对象,如何判断他们是否相等。所以很自然的我们想到重新实现它的hashCode和equals方法即可。
自定义keyGenerator
自定义的key生成器,我们需要去实现
org.springframework.cache.interceptor.KeyGenerator接口,并使用该参数来指定。需要注意的是:该参数与key是互斥的。
CacheKeyGenerator类代码如下:
@Component("cacheKeyGenerator") public class CacheKeyGenerator implements KeyGenerator{ @Override public Object generate(Object target, Method method, Object... params) { Object key=new BaseCacheKey(target,method,params); return key.toString(); } }
BaseCacheKey类代码如下:
public class BaseCacheKey implements Serializable { /** * */ private static final long serialVersionUID = -8517453845729052981L; private static final Logger LOG = Logger.getLogger(BaseCacheKey.class); private final Object[] params; private final int hashCode; private final String className; private final String methodName; public BaseCacheKey(Object target, Method method, Object[] elements) { this.className = target.getClass().getName(); this.methodName = getMethodName(method); this.params = new Object[elements.length]; System.arraycopy(elements, 0, this.params, 0, elements.length); this.hashCode = generatorHashCode(); } private String getMethodName(Method method) { StringBuilder builder = new StringBuilder(method.getName()); Class<?>[] types = method.getParameterTypes(); if (types.length != 0) { builder.append("("); for (Class<?> type : types) { String name = type.getName(); builder.append(name + ","); } builder.append(")"); } return builder.toString(); } @Override public boolean equals(Object obj) { if (this == obj) return true; if (obj == null) return false; if (getClass() != obj.getClass()) return false; BaseCacheKey o = (BaseCacheKey) obj; if (this.hashCode != o.hashCode()) return false; if (!Optional.ofNullable(o.className).orElse("").equals(this.className)) return false; if (!Optional.ofNullable(o.methodName).orElse("").equals(this.methodName)) return false; if (!Arrays.equals(params, o.params)) return false; return true; } @Override public final int hashCode() { return hashCode; } private int generatorHashCode() { final int prime = 31; int result = 1; result = prime * result + hashCode; result = prime * result + ((methodName == null) ? 0 : methodName.hashCode()); result = prime * result + Arrays.deepHashCode(params); result = prime * result + ((className == null) ? 0 : className.hashCode()); return result; } @Override public String toString() { LOG.info(Arrays.toString(params)); LOG.info(Arrays.deepToString(params)); return "BaseCacheKey [params=" + Arrays.deepToString(params) + ", className=" + className + ", methodName=" + methodName + "]"; } }
在IUserService接口类注解中使用keyGenerator="cacheKeyGenerator"。
测试类:
@RunWith(SpringJUnit4ClassRunner.class) @SpringApplicationConfiguration(Application.class) public class CacheApplicationTest { private static final Logger LOG = Logger.getLogger(CacheApplicationTest.class); @Autowired private IUserService iUserService; @Autowired private CacheManager cacheManager; @Before public void setUp(){ //检查使用的Cache LOG.info("Cache Manager ===== "+cacheManager.getClass().getName()); //删除所有数据 iUserService.deleteAll(); //添加用户信息 iUserService.create("guanguan", 20); iUserService.create("lindapang", 21); iUserService.create("xiaoyan", 18); } @Test public void userAddTest(){ UserVO user = iUserService.findByName("guanguan"); LOG.info("第一次查询用户信息=="+user.toString()); user = iUserService.findByName("guanguan"); LOG.info("第二次查询用户信息=="+user.toString()); } }
测试结果如下,可以看到缓存依旧生效:
Cache Manager ===== org.springframework.cache.ehcache.EhCacheCacheManager
可以观察到,此时CacheManager的实例是
org.springframework.data.redis.cache.RedisCacheManager,
在第一次查询的时候,执行了select语句;第二次查询没有执行select语句,说明是从缓存中获得了结果。
不过由于EhCache是进程内的缓存框架,在集群模式下时,各应用服务器之间的缓存都是独立的。在一些要求高一致性(任何数据变化都能及时的被查询到)的系统和应用中,就不能再使用EhCache来解决了。在Spring Boot的缓存支持中使用Redis进行数据缓存。