Service 层缓存

Service 层缓存

应用中缓存对于提升应用整体性能的作用很大,我们会在很多地方用到,如浏览器缓存、数据缓存、服务层缓存,缓存的内容也有计数器、短信、常用的数据查询,越能提供更大、更稳健的分布式缓存、提升缓存命中率,对应用的运行帮助也越大。

本篇介绍的是在 Spring Boot 项目中如何集成多种缓存并简化开发的过程,在 service 层使用了数据库缓存做例子,以及 EhCache 和 Redis 作为缓存服务。

目录

集成 Cache

首先将应用和 Cache 集成,非常方便:

基础配置

  1. 添加依赖
<dependency>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter-cache</artifactId>
</dependency>
  1. 在主应用类 DemoApplication 上声明 @EnableCaching 开启缓存

支持的缓存服务

接下来添加缓存 Provider 来使用外部缓存,Spring Boot 目前(2018-04)支持这些 cache 实现

  1. Generic (自己定义 Cache 实现)
  2. JCache (JSR-107) (EhCache 3, Hazelcast, Infinispan, and others)
  3. EhCache 2.x
  4. Hazelcast
  5. Infinispan
  6. Couchbase
  7. Redis
  8. Caffeine (Guava 的升级)
  9. Simple (默认的 ConcurrentHashMap 实现)

注1:不配置任何 Cache Provider 默认使用内存中的 Concurrent Maps 来管理缓存

注2:在开发期间如果需要临时禁用缓存,可以设置缓存类型属性为 none:spring.cache.type=none

集成 EhCache2

EhCache2 是个 Java 进程内运行的缓存框架,使用相对简单但性能也不错,建议项目启动的时候先集成,注意 2 和 3 两个版本的集成不太一样。

  1. Spring Boot 集成 EhCache 非常简单,在 pom.xml 中添加缓存的依赖:
<dependency>
	<groupId>net.sf.ehcache</groupId>
	<artifactId>ehcache</artifactId>
</dependency>
  1. application.properties 配置
# EhCache2
spring.cache.type=ehcache
spring.cache.ehcache.config=classpath:ehcache.xml
  1. 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>
  1. 监听(可选)

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 差不多:

  1. 修改依赖 pom.xml,增加 JCache 标准的依赖:
<dependency>
	<groupId>javax.cache</groupId>
	<artifactId>cache-api</artifactId>
</dependency>
<dependency>
	<groupId>org.ehcache</groupId>
	<artifactId>ehcache</artifactId>
</dependency>
  1. 配置文件 application.properties
spring.cache.type=jcache
spring.cache.jcache.config=classpath:ehcache3.xml
spring.cache.jcache.provider=org.ehcache.jsr107.EhcacheCachingProvider
  1. 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

  1. 起 Redis 的服务:
docker run -d -p 6379:6379 --name redis redis
  1. 依赖
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
  1. 配置
# 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']

参考:《SpEL 表达式语法文档》

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");
    }
}

参考文章

上篇UReport 的缓存设置
下篇Spring Boot 和 AdminLTE 的集成