Service 层缓存
应用中缓存对于提升应用整体性能的作用很大,我们会在很多地方用到,如浏览器缓存、数据缓存、服务层缓存,缓存的内容也有计数器、短信、常用的数据查询,越能提供更大、更稳健的分布式缓存、提升缓存命中率,对应用的运行帮助也越大。
本篇介绍的是在 Spring Boot 项目中如何集成多种缓存并简化开发的过程,在 service 层使用了数据库缓存做例子,以及 EhCache 和 Redis 作为缓存服务。
目录
集成 Cache
首先将应用和 Cache 集成,非常方便:
基础配置
- 添加依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-cache</artifactId>
</dependency>
- 在主应用类 DemoApplication 上声明
@EnableCaching
开启缓存
支持的缓存服务
接下来添加缓存 Provider 来使用外部缓存,Spring Boot 目前(2018-04)支持这些 cache 实现
- Generic (自己定义 Cache 实现)
- JCache (JSR-107) (EhCache 3, Hazelcast, Infinispan, and others)
- EhCache 2.x
- Hazelcast
- Infinispan
- Couchbase
- Redis
- Caffeine (Guava 的升级)
- Simple (默认的 ConcurrentHashMap 实现)
注1:不配置任何 Cache Provider 默认使用内存中的
Concurrent Maps
来管理缓存
注2:在开发期间如果需要临时禁用缓存,可以设置缓存类型属性为 none:
spring.cache.type=none
集成 EhCache2
EhCache2 是个 Java 进程内运行的缓存框架,使用相对简单但性能也不错,建议项目启动的时候先集成,注意 2 和 3 两个版本的集成不太一样。
- Spring Boot 集成 EhCache 非常简单,在
pom.xml
中添加缓存的依赖:
<dependency>
<groupId>net.sf.ehcache</groupId>
<artifactId>ehcache</artifactId>
</dependency>
- application.properties 配置
# EhCache2
spring.cache.type=ehcache
spring.cache.ehcache.config=classpath:ehcache.xml
- EhCache 配置文件
ehcache.xml
<ehcache xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="http://ehcache.org/ehcache.xsd">
<cache name="users"
maxEntriesLocalHeap="200"
timeToLiveSeconds="600">
</cache>
<cache name="userByLogin"
maxEntriesLocalHeap="200"
timeToLiveSeconds="600">
</cache>
<cache name="userById"
maxEntriesLocalHeap="200"
timeToLiveSeconds="600">
</cache>
</ehcache>
这里定义了三个缓存对象:users、userByLogin、userById,后面会用到。
还可以配置默认缓存(可选):
<defaultCache
maxEntriesLocalHeap="0"
eternal="false"
timeToIdleSeconds="1200"
timeToLiveSeconds="1200">
<!--<terracotta/>-->
</defaultCache>
- 监听(可选)
EhCache2 提供了 CacheEventListener 接口来开发监听缓存事件的方法,可以定义:
public class CustomerCacheEventListener implements CacheEventListener {
@Override
public void notifyElementRemoved(Ehcache ehcache, Element element) throws CacheException {
log.info("cache removed. key = {}, value = {}", element.getObjectKey(), element.getObjectValue());
}
@Override
public void notifyElementPut(Ehcache ehcache, Element element) throws CacheException {
log.info("cache put. key = {}, value = {}", element.getObjectKey(), element.getObjectValue());
}
@Override
public void notifyElementUpdated(Ehcache ehcache, Element element) throws CacheException {
log.info("cache updated. key = {}, value = {}", element.getObjectKey(), element.getObjectValue());
}
@Override
public void notifyElementExpired(Ehcache ehcache, Element element) {
log.info("cache expired. key = {}, value = {}", element.getObjectKey(), element.getObjectValue());
}
@Override
public void notifyElementEvicted(Ehcache ehcache, Element element) {
log.info("cache evicted. key = {}, value = {}", element.getObjectKey(), element.getObjectValue());
}
@Override
public void notifyRemoveAll(Ehcache ehcache) {
log.info("all elements removed. cache name = {}", ehcache.getName());
}
@Override
public Object clone() throws CloneNotSupportedException {
throw new CloneNotSupportedException();
}
@Override
public void dispose() {
log.info("cache dispose.");
}
}
Factory
public class CustomerCacheEventListenerFactory extends CacheEventListenerFactory {
@Override
public CacheEventListener createCacheEventListener(Properties properties) {
return new CustomerCacheEventListener();
}
}
然后在配置中应用 Factory
<cache name="users"
maxEntriesLocalHeap="200"
timeToLiveSeconds="600">
<cacheEventListenerFactory class="cn.wilmar.eevee.config.cache.CustomerCacheEventListenerFactory" />
</cache>
集成 EhCache3
EhCache3 相比 EhCache2 更新更快,并且集成了 Terracotta 集群平台,但是有些功能未完全实现。集成 EhCache3 和 2 差不多:
- 修改依赖
pom.xml
,增加 JCache 标准的依赖:
<dependency>
<groupId>javax.cache</groupId>
<artifactId>cache-api</artifactId>
</dependency>
<dependency>
<groupId>org.ehcache</groupId>
<artifactId>ehcache</artifactId>
</dependency>
- 配置文件
application.properties
spring.cache.type=jcache
spring.cache.jcache.config=classpath:ehcache3.xml
spring.cache.jcache.provider=org.ehcache.jsr107.EhcacheCachingProvider
- EhCache 配置文件
ehcache3.xml
<config xmlns='http://www.ehcache.org/v3'
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:jsr107="http://www.ehcache.org/v3/jsr107"
xsi:schemaLocation="http://www.ehcache.org/v3 http://www.ehcache.org/schema/ehcache-core-3.0.xsd
http://www.ehcache.org/v3/jsr107 http://www.ehcache.org/schema/ehcache-107-ext-3.0.xsd">
<cache alias="users">
<expiry>
<ttl unit="seconds">600</ttl>
</expiry>
<heap unit="entries">200</heap>
<jsr107:mbeans enable-statistics="true"/>
</cache>
...
</config>
JCache 是 JSC-107 Java 缓存规范。由于可能混有多种 Cache 类,Spring Boot 建议定义 config 和 provider 这两个属性
推荐设定 cache-template,添加 Listener
<cache-template name="default">
<expiry>
<ttl>600</ttl>
</expiry>
<listeners>
<listener>
<class>cn.wilmar.eevee.config.cache.EhCache3EventLogger</class>
<event-firing-mode>ASYNCHRONOUS</event-firing-mode>
<event-ordering-mode>UNORDERED</event-ordering-mode>
<events-to-fire-on>CREATED</events-to-fire-on>
<events-to-fire-on>UPDATED</events-to-fire-on>
<events-to-fire-on>EXPIRED</events-to-fire-on>
<events-to-fire-on>REMOVED</events-to-fire-on>
<events-to-fire-on>EVICTED</events-to-fire-on>
</listener>
</listeners>
<resources>
<heap>100</heap>
<offheap unit="MB">10</offheap>
<!--<disk persistent="true" unit="MB">20</disk>-->
</resources>
<jsr107:mbeans enable-statistics="true"/>
</cache-template>
EhCache3 的 Listener(和 EhCache2 不同)
public class EhCache3EventLogger implements CacheEventListener {
@Override
public void onEvent(CacheEvent cacheEvent) {
log.info("\nEvent: {} \nKey: {} \noldValue: {} \nnewValue: {}", cacheEvent.getType(), cacheEvent.getKey(), cacheEvent.getOldValue(), cacheEvent.getNewValue());
}
}
集成 Redis
- 起 Redis 的服务:
docker run -d -p 6379:6379 --name redis redis
- 依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
- 配置
# Redis
spring.cache.type=redis
spring.redis.host=127.0.0.1
spring.redis.port=6379
spring.redis.database=0
spring.cache.cache-names=users, userByLogin, userById
spring.cache.redis.time-to-live=860400
数据缓存、登入session 已经都存储到 Redis 了,可以实现重启保持登入且各对象缓存保存着。但是还有个 spring security 的 persistent_login 的表存储的 Token,再看怎么保存。
除了自己构建,更建议使用云供应商提供的 Redis 服务
其他 provider
还有其他很多优秀的缓存服务,视不同使用场景谨慎选择,建议开发环境 EhCache2,生产 Redis
其他配置不一一介绍:
参考 spring-boot-sample-cache,用 profile 跑不同的 cache
开启 Hibernate 二级缓存
针对 EhCache、EhCache3、Redis,应用 Hibernate 不同的缓存设置依赖
<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-ehcache</artifactId>
</dependency>
<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-jcache</artifactId>
</dependency>
<dependency>
<groupId>com.github.debop</groupId>
<artifactId>hibernate-redis</artifactId>
<version>2.3.2</version>
</dependency>
设置
spring.jpa.properties.hibernate.cache.use_query_cache=true
spring.jpa.properties.hibernate.cache.use_second_level_cache=true
spring.jpa.properties.hibernate.cache.use_structured_entries=true
spring.jpa.properties.hibernate.cache.region_prefix=hibernate_
# EhCache2
spring.jpa.properties.hibernate.cache.region.factory_class=org.hibernate.cache.ehcache.SingletonEhCacheRegionFactory
spring.jpa.properties.hibernate.cache.provider_configuration_file_resource_path=ehcache.xml
# EhCache3
spring.jpa.properties.hibernate.cache.region.factory_class=org.hibernate.cache.jcache.internal.JCacheRegionFactory
spring.jpa.properties.hibernate.cache.provider_configuration_file_resource_path=ehcache3.xml
# Redis
spring.jpa.properties.hibernate.cache.region.factory_class=org.hibernate.cache.redis.hibernate52.SingletonRedisRegionFactory
spring.jpa.properties.hibernate.cache.provider_configuration_file_resource_path=hibernate-redis.properties
这样就可以同时使用业务层缓存和 ORM 缓存了。
使用 Cache
集成了 Cache,接下来就是如何在开发中使用了,这里从几个关键的 Cache 使用操作注解说明开始
主要注解
@Cacheable
@CachePut
@CacheEvict
@Caching
@CacheConfig
- @Cacheable 方法或类上,表示可缓存,如果命中直接返回缓存不执行方法内容
- @CachePut 也是表示可缓存,但是内容总是执行的
- @CacheEvict 清除缓存
- @Caching 同时使用多个 Cache 注解
- @CacheConfig 统一配置 cacheNames
注:在 Service 和 Repository 里面都可以使用 Cache 的注解,只是如果使用了 JpaRepository 的话没有实现做不了细节的控制,建议在 Service 里面混合使用注解和 CacheManager
Cacheable
表示方法可缓存 @Cacheable
@Cacheable(USERS_ALL_CACHE)
public List<User> findAllUsers() {
需要 Key 的场景
@Cacheable(value = USER_BY_ID_CACHE, key = "#id")
@Transactional(readOnly = true)
public User getUserById(long id) {
key 上还可以加些常量前缀,用来区分在同一个缓存中不同类型,像这样:
@Cacheable(key = "'id:' + #id")
@Transactional(readOnly = true)
public User getUser(long id) {
key 的语法,支持:
''
常量#
变量名#p0
代表第一个参数- map 获取 key:
#p0['id']
CacheEvict
删除所有缓存 @CacheEvict(allEntries = true)
目前新增、更新、删除操作,都可以全删除缓存,以保障所有缓存同步(或者用 key)
复杂一点的如:
@CacheEvict(value="shops:detail",key="'id:'+#p0['id']",condition="#p0['id']>0")
public Shop getById(Map<String, Object> param);
CachePut
@CachePut(value="shops:detail",key="'id:'+#p0['id']")
public Shop update(Map<String, Object> param);
大多数情况下还需要清除别的缓存,参考 Caching
Caching
需要进行多个操作同时声明可以用 @Caching
来组合,以下示例为:新增或修改对象时根据 id、username 更新缓存,并清除名为 USERS_ALL_CACHE 的缓存
@Caching(put = {
@CachePut(cacheNames = USER_BY_ID_CACHE, key = "#user.id"),
@CachePut(cacheNames = USER_BY_LOGIN_CACHE, key = "#user.username")},
evict = {@CacheEvict(cacheNames = USERS_ALL_CACHE, allEntries = true)})
public User createOrUpdateUser(User user) {
注意 在注解上写 key 由于要用常量,不能应用
SimpleKey.EMPTY.toString()
,只能evict allEntries
CacheManager
有些场景,方法参数里可能没有 Cache 的 key,就只能通过 CacheManager
手动处理:
cacheManager.getCache(USER_BY_ID_CACHE).evict(user.getId());
cacheManager.getCache(USER_BY_LOGIN_CACHE).evict(user.getUsername());
cacheManager.getCache(USERS_ALL_CACHE).clear();
内部实现可以转成内部 ConcurrentMapCache.getCache.getNativeCache() 进行查看
ConcurrentMapCache concurrentMapCache = (ConcurrentMapCache) cacheManager.getCache(USERS_ALL_CACHE);
System.out.println("cacheManager.getCache(USERS_ALL_CACHE) = " + cacheManager.getCache(USERS_ALL_CACHE));
System.out.println("concurrentMapCache.getNativeCache() = " + concurrentMapCache.getNativeCache());
在没有设置 key 的时候默认增加的是一个 SimpleKey.EMPTY
对象(之前的版本是 0)
所以对于只有一个对象存储的 Cache,evict 的时候既可以 clear()
,也可以 evict(SimpleKey.EMPTY)
同步,单线程 @Cacheable(sync=true)
UserService 查询
@Service
public class UserService {
// 基本的三个 cache:所有对象集合缓存、byId、ByKey
private static final String USERS_ALL_CACHE = "users";
private static final String USER_BY_ID_CACHE = "userById";
private static final String USER_BY_LOGIN_CACHE = "userByLogin";
private final UserMapper userMapper;
private final CacheManager cacheManager;
public UserService(UserMapper userMapper, CacheManager cacheManager) {
this.userMapper = userMapper;
this.cacheManager = cacheManager;
}
@Cacheable(USERS_ALL_CACHE)
@Transactional(readOnly = true)
public List<User> getAll() {
return userMapper.selectAll();
}
}
再改一下 UserController,就可以测试缓存了,访问 http://localhost:8080/users ,观察后台是否发起了新的查询,看缓存是否生效。
Cache 测试
Cache 的测试并不复杂,可以用 Rule Log 日志,也可以直接用 CacheMananger 查询判断缓存是否生效
@Test
public void findAll() {
Cache cache = cacheManager.getCache(USERS_ALL_CACHE);
assertThat(cache, notNullValue());
cache.clear();
List<User> users = userService.findAll();
assertThat(Objects.requireNonNull(cache.get(SimpleKey.EMPTY)).get(), equalTo(users));
}
UserService 完整 CRUD 例子
这里的 UserMapper 是 DAO 层 MyBatis 的 mapper 提供的方法,当然也可以用 JPA 或者 Hibernate 来提供
// 查询所有,存入 cache: users
@Cacheable(USERS_ALL_CACHE)
@Transactional(readOnly = true)
public List<User> getAll() {
return userMapper.selectAll();
}
// 新增保存,更新 byId 和 byUsername 的两个 cache,清空 users cache
@Caching(put = {
@CachePut(cacheNames = USER_BY_ID_CACHE, key = "#user.id"),
@CachePut(cacheNames = USER_BY_LOGIN_CACHE, key = "#user.username")},
evict = {@CacheEvict(cacheNames = USERS_ALL_CACHE, allEntries = true)})
public User save(User user) {
userMapper.insert(user);
return user;
}
// 更新保存,同上
@Caching(put = {
@CachePut(cacheNames = USER_BY_ID_CACHE, key = "#user.id"),
@CachePut(cacheNames = USER_BY_LOGIN_CACHE, key = "#user.username")},
evict = {@CacheEvict(cacheNames = USERS_ALL_CACHE, allEntries = true)})
public User update(User user) {
userMapper.updateByPrimaryKey(user);
return user;
}
// 根据 id 删除对象,手动调用 CacheManager 清空相关所有 cache,就是上面说的方法参数里面没有用来清空的 key 参数场景
public void delete(Long id) {
User user = userMapper.selectByPrimaryKey(id);
if (user == null) {
return;
}
userMapper.deleteByPrimaryKey(user);
cacheManager.getCache(USER_BY_ID_CACHE).evict(user.getId());
cacheManager.getCache(USER_BY_LOGIN_CACHE).evict(user.getUsername());
cacheManager.getCache(USERS_ALL_CACHE).clear();
}
// 根据 user id 查询对象,存入 userById cache
@Cacheable(value = USER_BY_ID_CACHE, key = "#id")
@Transactional(readOnly = true)
public User getOne(Long id) {
return userMapper.selectByPrimaryKey(id);
}
// 根据 user username,存入 userByLogin
@Cacheable(cacheNames = USER_BY_LOGIN_CACHE, key = "#username")
@Transactional(readOnly = true)
public User getByUsername(String username) {
return userMapper.selectByUsername(username);
}
Controller 改改,都直接调用 mapper。如果要加入验证逻辑,可以在 web 层调用相关 service
...
@PostMapping("/users")
public void save(@RequestBody User user) {
if (userService.getByUsername(user.getUsername()) != null) {
throw new RuntimeException("exist");
}
userService.save(user);
}
...
改完就可以不用依赖 UserMapper 了,完了可以针对 Controller 发起请求各种测试,观察缓存的变化情况(Redis 可以用 RedisDesktopMananger)
测试脚本
供参考的测试 curl 脚本
# 查询所有用户
curl http://localhost:8080/users
# 查询单个用户
curl http://localhost:8080/users/1
# 新建用户
curl -X POST -H "Content-Type:application/json" -d '{"id":3, "username":"annaleaf", "password":"333333"}' http://localhost:8080/users
# 更新用户
curl -X PUT -H "Content-Type:application/json" -d '{"id":1, "username":"yinguowei", "password":"222222"}' http://localhost:8080/users
# 删除用户
curl -X DELETE http://localhost:8080/users/1
监控和管理
对监控的管理可以再加入很多控制,比如 Cache 的 metric,手动或定时的清除,Cache 时间的监听
查看 Cache 数量
可以通过开启 Spring Boot Actuator 相关端点来查看 Cache 的使用情况:
management.endpoints.web.exposure.include=*
访问:http://localhost:8080/actuator/metrics/cache.size
手动清除缓存
可以设置手动或定时任务清除所有或部分 Cache
public void purgeAllCaches() {
logger.info("CacheClearTask.purgeAllCaches");
cacheManager.getCacheNames().parallelStream().forEach(
name -> Objects.requireNonNull(cacheManager.getCache(name)).clear());
}
CacheManagerCheck
写了一个用于检查当前的 CacheManager,参考:CacheManagerCheck.java
@Component
public class CacheManagerCheck implements CommandLineRunner {
private static final Logger logger = LoggerFactory.getLogger(CacheManagerCheck.class);
private final CacheManager cacheManager;
public CacheManagerCheck(CacheManager cacheManager) {
this.cacheManager = cacheManager;
}
@Override
public void run(String... strings) throws Exception {
logger.info("\n\n" + "=========================================================\n"
+ "Using cache manager: " + this.cacheManager.getClass().getName() + "\n"
+ "=========================================================\n\n");
}
}