目录
2.1 使用EhCacheCacheManager —— 仅支持2.x版本
2.2 使用JCacheCacheManager —— 仅支持3.x版本
EhCache是一个以Java实现的开源本地缓存框架,但也支持分布式部署。它符合JSR107标准,具有简单、高性能的特点,官方宣称是为大型高并发系统设计,广泛地与Hibernate、Spring等开源框架结合使用。由于EhCache 3.x分布式部署需要结合Terracotta服务器使用,这里就不再介绍了。
EhCache目前的最新版本是3.7.0(需要Java 8以上版本),文中除非显式说明,否则本文的介绍均基于该版本。
1.基本使用
使用时,需要使用Maven引入ehcache包:
<dependency>
<groupId>org.ehcache</groupId>
<artifactId>ehcache</artifactId>
<version>3.7.0</version>
</dependency>
EhCache的核心是CacheManager,每个CacheManager可包含多个Cache。因此,EhCache的创建过程就是CacheManager和Cache的配置过程,3.x版本有如下几种配置方式:
1.1 XML配置
首先需要创建一个XML配置文件,这里在resource目录下创建名为config.xml的文件,以下是示例配置:
<?xml version="1.0" encoding="UTF-8"?>
<ehcache:config
xmlns:ehcache="http://www.ehcache.org/v3">
<ehcache:cache alias="myCache">
<ehcache:key-type>java.lang.String</ehcache:key-type>
<ehcache:value-type>java.lang.String</ehcache:value-type>
<ehcache:expiry>
<ehcache:tti unit="minutes">20</ehcache:tti>
</ehcache:expiry>
<ehcache:heap>200</ehcache:heap>
</ehcache:cache>
</ehcache:config>
config标签代表一个CacheManager定义,cache代表一个Cache定义,key-type和value-type分别代表键和值的类型,这里全部是String类型,expiry代表缓存存活时间设置,这里设置了TTI为20分钟,heap代表缓存容量,默认为entries,即存放多少个缓存对象,如果选择B、KB、MB、GB等,就是不限数量,但是限制缓存总大小。
其他的标签及其作用可参见:XML配置
然后读取配置文件,创建缓存管理器和缓存对象:
URL url=getClass().getResource("config.xml");
Configuration conf=new XmlConfiguration(url);
CacheManager cacheManager=CacheManagerBuilder.newCacheManager(conf);
cacheManager.init();
Cache cache=cacheManager.getCache("myCache",String.class,String.class);
cache.put("hello","world");
System.out.println(cache.get("hello"));
cacheManager.close();
该段代码会输出“world”。
因为都是存储键值对,所以Cache的API和Map差不多。CacheManager需要显式关闭,也可以使用try-resource形式,实现自动关闭:
try(CacheManager cacheManager=CacheManagerBuilder.newCacheManager(conf)){
...
}catch(Exception e){
...
}
1.2 硬编码
XML配置虽然容易理解,但也很麻烦,尤其是Cache的键、值类型已经在XML中定义过了,实例化时还要再定义一次,多此一举,完全可以舍弃XML,直接使用硬编码形式配置缓存,下面的配置效果和上面的XML配置完全一致:
CacheConfiguration conf=CacheConfigurationBuilder
.newCacheConfigurationBuilder(String.class,String.class, ResourcePoolsBuilder.heap(200L))
.withExpiry(ExpiryPolicyBuilder.timeToLiveExpiration(Duration.ofMinutes(20L)))
.build();
CacheManager manager=CacheManagerBuilder.newCacheManagerBuilder()
.withCache("myCache",conf)
.build(true);
Cache cache=manager.getCache("myCache",String.class,String.class);
cache.put("hello","world");
System.out.println(cache.get("hello"));
manager.close();
实际上就是把XML配置变成了一个CacheConfiguration对象而已,使用起来还是挺麻烦的
1.3 直接创建Cache
如果是创建大量有共性的缓存,使用前两种方式或许还不错,但是如果创建的缓存不多,或者没什么共同点,那么还不如在创建Cache的时候再指定其特性,可以使用UserManagedCache:
UserManagedCache cache= UserManagedCacheBuilder
.newUserManagedCacheBuilder(String.class,String.class)
.withExpiry(ExpiryPolicyBuilder.timeToLiveExpiration(Duration.ofMinutes(20L)))
.withResourcePools(ResourcePoolsBuilder.heap(200L))
.build(true);
cache.put("hello","world");
System.out.println(cache.get("hello"));
可以看到,这种方式代码量少了不少,看着很简洁。
2.结合Spring使用
缓存一般不会单独使用,而是配合其它框架,EhCache最常见的使用场景就是作为Hibernate等ORM组件的二级缓存,不过和Spring的结合也不少见,Spring Cache就为其提供了支持。
2.1 使用EhCacheCacheManager —— 仅支持2.x版本
由于这里仅支持2.x版本,因此这里将依赖包换为2.10.0版本,同时引入Spring相关依赖:
<dependency>
<groupId>net.sf.ehcache</groupId>
<artifactId>ehcache</artifactId>
<version>2.10.0</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-core</artifactId>
<version>5.0.7.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>5.0.7.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context-support</artifactId>
<version>5.0.7.RELEASE</version>
</dependency>
由于2.x版本和3.x版本配置文件有差异,需要重新创建,这里需要注意的是,ehcache.xsd文件无法自动获取,需要手动下载下来放到classpath下:
<?xml version="1.0" encoding="UTF-8"?>
<ehcache xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="ehcache.xsd">
<defaultCache maxElementsInMemory="10000" memoryStoreEvictionPolicy="LRU"/>
<cache name="myCache" timeToLiveSeconds="7200" maxEntriesLocalHeap="200"/>
</ehcache>
Spring配置文件中需要配置EhCacheManagerFactoryBean和EhCacheCacheManager两个Bean:
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:cache="http://www.springframework.org/schema/cache"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/cache
http://www.springframework.org/schema/cache/spring-cache.xsd">
<cache:annotation-driven/>
<bean id="cacheManagerFactory" class="org.springframework.cache.ehcache.EhCacheManagerFactoryBean">
<property name="configLocation" value="classpath:config.xml"/>
</bean>
<bean id="cacheManager" class="org.springframework.cache.ehcache.EhCacheCacheManager">
<property name="cacheManager" ref="cacheManagerFactory"/>
</bean>
<bean id="cache" class="cache.DoCache"/>
</beans>
cache:annotation-driven表示开启Spring Cache注解。下面是用注解配置的Bean类,包含两个方法,一个用来存入缓存,一个用来清除缓存:
public class DoCache {
@Cacheable(value="myCache")
public String putHello(){
return "Hello World";
}
@CacheEvict(allEntries = true,value="myCache")
public void removeHello(){
}
}
最后就是创建Spring容器和CacheManager,调用Bean方法进行测试:
ApplicationContext context=new ClassPathXmlApplicationContext("classpath:applicationContext.xml");
DoCache doCache= (DoCache) context.getBean("cache");
EhCacheCacheManager cacheManager= (EhCacheCacheManager) context.getBean("cacheManager");
Cache cache=cacheManager.getCacheManager().getCache("myCache");
doCache.putHello();
System.out.println(cache.get(cache.getKeys().get(0)));
doCache.removeHello();
System.out.println(cache.getSize());
注意这里要先调用EhCacheCacheManager的getCacheManager,再getCache,或者直接获取名为cacheManagerFactory的Bean。输出如下:
[ key = SimpleKey [], value=Hello World, version=1, hitCount=1, CreationTime = 1555055664418, LastAccessTime = 1555055664418 ]
0
Spring Cache提供了五个常用注解:
- @Cacheable:会将返回值存入缓存,如果没有参数,则默认key为SimpleKey.EMPTY,只有第一次会真正调用方法,之后再调用到方法时,会直接从缓存中取值返回,类似的有@CacheResult
- @CachePut:基本类似于@Cacheable,不同之处在于,被该注解标记的方法每次都会真实调用,并将产生的值作为新的缓存值更新到缓存中
- @CacheEvict:触发缓存实效操作,通过cacheName和key指定要清除的缓存,或者用allEntries清除所有缓存,类似的有@CacheRemove、@CacheRemoveAll
- @Caching:可以设置多个@Cacheable、@CachePut、@CacheEvict
- @CacheConfig:作用于类上,用于配置cacheName等公共属性
2.2 使用JCacheCacheManager —— 仅支持3.x版本
由于EhCache 3.x基于JSR107实现,因此需要额外引入javax.cache包,完整的依赖列表如下:
<dependency>
<groupId>org.ehcache</groupId>
<artifactId>ehcache</artifactId>
<version>3.7.0</version>
</dependency>
<dependency>
<groupId>javax.cache</groupId>
<artifactId>cache-api</artifactId>
<version>1.1.0</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-core</artifactId>
<version>5.0.7.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>5.0.7.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context-support</artifactId>
<version>5.0.7.RELEASE</version>
</dependency>
首先需要修改Spring配置文件,将EhCacheManagerFactoryBean和EhCacheCacheManager替换为JCache实现:
<cache:annotation-driven/>
<bean id="cacheManagerFactory" class="org.springframework.cache.jcache.JCacheManagerFactoryBean">
<property name="cacheManagerUri" value="classpath:config.xml"/>
</bean>
<bean id="cacheManager" class="org.springframework.cache.jcache.JCacheCacheManager">
<property name="cacheManager" ref="cacheManagerFactory"/>
</bean>
<bean id="cache" class="cache.DoCache"/>
接下来,在EhCache 3.x配置文件基础上进行修改,增加JSR107配置:
<?xml version="1.0" encoding="UTF-8"?>
<ehcache:config
xmlns:ehcache="http://www.ehcache.org/v3"
xmlns:jsr107="http://www.ehcache.org/v3/jsr107">
<ehcache:service>
<jsr107:defaults>
<jsr107:cache name="mycache" template="defaultCache"/>
</jsr107:defaults>
</ehcache:service>
<ehcache:cache-template name="defaultCache">
<ehcache:expiry>
<ehcache:tti unit="minutes">20</ehcache:tti>
</ehcache:expiry>
<ehcache:heap>200</ehcache:heap>
</ehcache:cache-template>
</ehcache:config>
可以看到,这里没有配置键值类型。接下来,CacheManager和Cache的构造方式也要改变:
ApplicationContext context=new ClassPathXmlApplicationContext("classpath:applicationContext.xml");
DoCache doCache= (DoCache) context.getBean("cache");
JCacheCacheManager manager= (JCacheCacheManager) context.getBean("cacheManager");
Cache cache=manager.getCacheManager()
.createCache("myCache",new MutableConfiguration<>());
doCache.sayHello();
cache.forEach(o-> System.out.println(((Cache.Entry)o).getValue()));
doCache.removeHello();
System.out.println(cache.iterator().hasNext());
此处变为调用getBean获取CacheManager,调用createCache创建Cache对象,且必须保证createCache发生在操作缓存之前。上述代码运行后的输出如下,表明运行成功:
Hello World
false
3.作为二级缓存使用
多级缓存在分布式系统中很常用,主要作用是通过多次缓冲,不断减缓数据流入的速度,例如一个Redis+Tomcat的应用,系统内部使用了Mybatis,那么请求首先到达Redis,未命中再进入Tomcat缓存,仍未命中再进入Mybatis缓存,此时如果还没有命中,才会去数据库取数据。如果没有这样的多级缓存,一旦Redis由于流量过大宕机,就会导致流量直接打向数据库,造成雪崩。通过Tomcat和Mybatis的拦截,至少可以在一定程度上减缓数据库收到的冲击。
Java Web应用最常用的Hibernate和Mybatis都支持二级缓存的配置,不过这里的二级缓存除了作为一级缓存的后备,存放一些热度或并发度较低的数据之外,还作为多个Session的共享缓存。
3.1 Hibernate:同时支持EhCache2、3
类似于Spring,Hibernate可以同时支持EhCache2、3,如果要使用EhCache2,需要引入hibernate-ehcache包,如果使用EhCache3,需要引入hibernate-jcache包。下面以3.x为例,EhCache的配置文件和1.1节相同。
首先需要配置hibernate的配置文件,这里使用注解来配置映射,所以mapper标签没有指定xml文件,此外hibernate-configuration不能带xmlns属性,否则会提示找不到元素:
<?xml version="1.0" encoding="utf-8" ?>
<hibernate-configuration>
<session-factory>
//设置JDBC属性
<property name="hibernate.connection.driver_class">com.mysql.jdbc.Driver</property>
<property name="hibernate.connection.username">root</property>
<property name="hibernate.connection.password">123456</property>
<property name="hibernate.connection.url">jdbc:mysql://localhost:3306/test</property>
//进行一些配置,如是否打印sql语句等
<property name="show_sql">true</property>
<property name="format_sql">true</property>
<property name="hbm2ddl.auto">update</property>
<property name="hibernate.connection.autocommit">true</property>
//启用二级缓存
<property name="hibernate.cache.use_second_level_cache">true</property>
//启用查询缓存
<property name="hibernate.cache.use_query_cache">true</property>
//以下两条为二级缓存实现类的配置
<property name="hibernate.cache.provider_configuration_file_resource_path">config.xml</property>
<property name="hibernate.cache.region.factory_class">org.hibernate.cache.jcache.internal.JCacheRegionFactory</property>
//绑定session,以便通过getSession获取对象
<property name="hibernate.current_session_context_class">thread</property>
//开启统计
<property name="hibernate.generate_statistics">true</property>
//配置实体映射
<mapping class="cache.User"/>
</session-factory>
</hibernate-configuration>
接下来是编写Entity,@Cache的usage属性配置了缓存策略:
@Entity(name = "user")
@Cacheable
@Cache(usage = CacheConcurrencyStrategy.NONSTRICT_READ_WRITE)
public class User implements Serializable {
private static final long serialVersionUID = -570936907944909799L;
@Id
private int id;
@Column(name="username")
private String userName;
@Column(name = "password")
private String passWord;
//下面是setter、getter和toString
}
然后进行两次查询,以测试缓存效果:
public static void main(String[] args) {
Configuration conf=new Configuration();
conf.configure();
SessionFactory factory=conf.buildSessionFactory();
Session session1=factory.openSession();
session1.beginTransaction();
Query query=session1.createQuery("from user");
query.setCacheable(true);
System.out.println(query.list());
session1.getTransaction().commit();
Session session2=factory.openSession();
session2.beginTransaction();
User user = session2.load(User.class,1);
System.out.println(user);
Statistics s=factory.getStatistics();
System.out.println("put:"+s.getSecondLevelCachePutCount());
System.out.println("hit:"+s.getSecondLevelCacheHitCount());
System.out.println("miss:"+s.getSecondLevelCacheMissCount());
}
可以看到,这里进行的两次查询分别属于两个Session,由于一级缓存是绑定在Session作用域的,所以第二次查询时不会用到它,而是从二级缓存中取值,最后一段的统计信息中,如果hit值不为0,说明确实是从缓存中取的值。输出如下:
Hibernate:
select
user0_.id as id1_0_,
user0_.password as password2_0_,
user0_.username as username3_0_
from
user user0_
[User{id=1, userName='zhangsan', passWord='123456'}, User{id=2, userName='lisi', passWord='123abc'}]
User{id=1, userName='zhangsan', passWord='123456'}
put:2
hit:1
miss:0
3.2 Mybatis:仅支持2.x
mybatis提供了mybatis-ehcache包,但是没有找到mybatis-jcache包,因此这里认为Mybatis仅支持ehcache 2.x。下面是一个使用示例,EhCache配置文件和2.1节相同,需要注意的是,和Hibernate不同,这里还需要引入mybatis本体。此处的例子是在 学习Mybatis(1):独立使用 的基础上进行修改。
首先是修改mybatis_config.xml文件,增加日志配置,以便观察缓存效果,其他配置保持不变:
<configuration>
<settings>
...
<setting name="logImpl" value="STDOUT_LOGGING"></setting>
</settings>
...
</configuration>
然后是修改UserService.xml,增加缓存配置:
<mapper namespace="mybatis.UserService">
<cache type="org.mybatis.caches.ehcache.EhcacheCache">
<property name="maxEntriesLocalHeap" value="100"/>
<property name="timeToLiveSeconds" value="3600"/>
</cache>
...
</mapper>
这里的属性设置是默认设置,如果用户编写了ehcache的配置文件,会优先读取配置文件。然后就是进行两次查询操作:
public static void main(String[] args) throws IOException {
SqlSessionFactory sqlSessionFactory=new SqlSessionFactoryBuilder().build(Resources.getResourceAsStream("mybatis_config.xml"));
SqlSession sqlSession=sqlSessionFactory.openSession();
UserService userMapper=sqlSession.getMapper(UserService.class);
User user=userMapper.getUser(1);
System.out.println(user);
sqlSession.close();
SqlSession sqlSession2=sqlSessionFactory.openSession();
UserService userMapper2=sqlSession2.getMapper(UserService.class);
User user2=userMapper2.getUser(1);
System.out.println(user2);
sqlSession2.close();
}
这里也是建立了两个SqlSession进行查询,如果二级缓存生效,则日志会显示命中:
==> Preparing: select * from User where id=?;
==> Parameters: 1(Integer)
<== Columns: id, username, password
<== Row: 1, zhangsan, 123456
<== Total: 1
User{id=1, userName='zhangsan', passWord='123456'}
Resetting autocommit to true on JDBC Connection [com.mysql.jdbc.JDBC4Connection@5c1a8622]
Closing JDBC Connection [com.mysql.jdbc.JDBC4Connection@5c1a8622]
Returned connection 1545242146 to pool.
Cache Hit Ratio [cache.UserService]: 0.5
User{id=1, userName='zhangsan', passWord='123456'}
可以看到“Cache Hit”字样,说明缓存生效。顺带一提,这里如果注释掉mybatis_config.xml中cacheEnable的配置,二级缓存依然生效,也印证了我在 学习Mybatis(8):Mybatis缓存机制的内部实现 中所说的,二级缓存不需要显式开启,而是默认就启用了的