Fork me on GitHub

spring-boot优雅的使用缓存

前言

如果说如何优化你的网站或者应用,大部分同学第一的反应可能就是缓存。缓存不是万能的,但是当用户和访问量加大的情况下,缓存是提高应用吞吐量非常有效的手段。

本文主要介绍如果通过spring-boot使用本地缓存,以guava cache为例。其实从spring3开始就已经提供了基于注解的缓存支持,其原理还是基于aop的思想,降低了缓存代码对我们应用代码的侵入。

spring cache相关接口及注解

从spring3开始,spring支持基于注解的缓存组件。其核心接口俩个,在spring-context包中:

  • Cache
  • CacheManager

spring中实现优雅使用缓存的方式还是基于注解的方式,其中常用的一些注解也是在spring-context包中。

接下来分别看下这些接口。

Cache 接口

Cache接口,提供操作缓存的定义(比如放入、读取、清理等。

其中Cache接口Spring也提供了很多默认的实现,它们在spring-context包和spring-context-support包中:

Cache接口:

public interface Cache {

    // cacheName,缓存的名字,默认实现中一般是CacheManager创建Cache的bean时传入cacheName
    String getName();

    // 获取实际使用的缓存,如:RedisTemplate、com.github.benmanes.caffeine.cache.Cache<Object, Object>。暂时没发现实际用处,可能只是提供获取原生缓存的bean,以便需要扩展一些缓存操作或统计之类的东西
    Object getNativeCache();

    // 通过key获取缓存值,注意返回的是ValueWrapper,为了兼容存储空值的情况,将返回值包装了一层,通过get方法获取实际值
    ValueWrapper get(Object key);

    // 通过key获取缓存值,返回的是实际值,即方法的返回值类型
    <T> T get(Object key, Class<T> type);

    // 通过key获取缓存值,可以使用valueLoader.call()来调使用@Cacheable注解的方法。当@Cacheable注解的sync属性配置为true时使用此方法。因此方法内需要保证回源到数据库的同步性。避免在缓存失效时大量请求回源到数据库
    <T> T get(Object key, Callable<T> valueLoader);

    // 将@Cacheable注解方法返回的数据放入缓存中
    void put(Object key, Object value);

    // 当缓存中不存在key时才放入缓存。返回值是当key存在时原有的数据
    ValueWrapper putIfAbsent(Object key, Object value);

    // 删除缓存
    void evict(Object key);

    // 删除缓存中的所有数据。需要注意的是,具体实现中只删除使用@Cacheable注解缓存的所有数据,不要影响应用内的其他缓存
    void clear();

    // 缓存返回值的包装
    interface ValueWrapper {

        // 返回实际缓存的对象
        Object get();
    }

    // 当{@link #get(Object, Callable)}抛出异常时,会包装成此异常抛出
    @SuppressWarnings("serial")
    class ValueRetrievalException extends RuntimeException {

        private final Object key;

        public ValueRetrievalException(Object key, Callable<?> loader, Throwable ex) {
            super(String.format("Value for key '%s' could not be loaded using '%s'", key, loader), ex);
            this.key = key;
        }

        public Object getKey() {
            return this.key;
        }
    }
}

CacheManager 接口

主要负责缓存Cache接口的管理,提供了根据cacheName获取缓存的接口以及获取所有cacheName的接口。

public interface CacheManager {

    // 通过cacheName创建Cache的实现bean,具体实现中需要存储已创建的Cache实现bean,避免重复创建,也避免内存缓存对象(如Caffeine)重新创建后原来缓存内容丢失的情况
    Cache getCache(String name);

    // 返回所有的cacheName
    Collection<String> getCacheNames();
}

cache 相关注解

@EnableCaching

启用Cache注解支持的总开关。

@CachePut

应用到写数据的方法上,如新增/修改方法,调用方法时会自动把相应的数据放入缓存。

Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
public @interface CachePut {
    // cacheNames,CacheManager就是通过这个名称创建对应的Cache实现bean
    @AliasFor("cacheNames")
    String[] value() default {};

    // 含义与`cacheNames`别名一样,等价于value()
    @AliasFor("value")
    String[] cacheNames() default {};

    // 缓存的key,支持SpEL表达式。默认是使用所有参数及其计算的hashCode包装后的对象(SimpleKey)
    String key() default "";

    // 缓存key生成器,spring4开始默认实现是SimpleKeyGenerator
    String keyGenerator() default "";

    // 指定使用的cacheManager
    String cacheManager() default "";

    // 缓存解析器
    String cacheResolver() default "";

    // 缓存的条件,支持SpEL表达式,当达到满足的条件时才缓存数据。在调用方法前后都会判断
    String condition() default "";

    // 满足条件时不更新缓存,支持SpEL表达式,只在调用方法后判断
    String unless() default "";
}

@CacheEvict

应用到移除数据的方法上,如删除方法,调用方法时会从缓存中移除相应的数据。

大部分定义可参考上面@CachePut注解,新增了2个属性:

@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
public @interface CacheEvict {

    // 此处省略大部分与CachePut相同的属性...

    // 是否要清除所有缓存的数据,为false时调用的是Cache.evict(key)方法;为true时调用的是Cache.clear()方法
    boolean allEntries() default false;

    // 是否在调用方法之前清除缓存
    boolean beforeInvocation() default false;
}

@Cacheable

应用到读取数据的方法上,即可缓存的方法,如查找方法:先从缓存中读取,如果没有再调用方法获取数据,然后把数据添加到缓存中。

大部分定义可参考上面@CachePut注解,新增了1个属性:

@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
public @interface Cacheable {
    // 此处省略大部分与CachePut相同的属性...

    // 回源到实际方法获取数据时,是否要保持同步,如果为false,调用的是Cache.get(key)方法;如果为true,调用的是Cache.get(key, Callable)方法
    boolean sync() default false;
}

@Caching

提供了注解的组合模式,可以在此注解中配置多个Cache注解。

@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
public @interface Caching {
    // @Cacheable 数组,可以配置多个Cacheable注解,实现同时查询多个缓存
    Cacheable[] cacheable() default {};

    // @CachePut 数组,可以配置多个CachePut注解,实现同时存放多个缓存
    CachePut[] put() default {};

    // @CacheEvict 数组,可以配置多个CacheEvict注解,实现同时删除多个缓存
    CacheEvict[] evict() default {};
}

guava cache

guava是Google提供的开源的java核心工具类库。其中包含很多好用的工具类,我们这里主要使用guava中的cache包。

为什么要使用guava的cache而不是简单的使用ConcurrentMap呢?或者说这二者的区别在哪?

答:Guava Cache与ConcurrentMap很相似,它们之间的一个根本区别在于缓存可以回收存储的元素。

guava提供了不同方式的缓存回收策略:

  • 基于容量的回收
  • 定时回收
  • 基于引用的回收

在spring-boot中使用guava cache

三步完成在spring-boot中使用guava cache:

  • maven依赖
  • 配置缓存类型
  • 代码中使用guava cache并单元测试

maven依赖

maven依赖特别少,由于spring-boot-starter默认并没有依赖上spring-context-support包,所以我们需要依赖上它。如下,引入spring-boot-starter-cache即可。以及引入本地缓存要用到的guava

<!-- cache -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-cache</artifactId>
</dependency>
<!-- guava -->
<dependency>
    <groupId>com.google.guava</groupId>
    <artifactId>guava</artifactId>
    <version>23.0</version>
</dependency>

配置缓存类型

由于我们使用guava作为缓存,只需要指明缓存类型为 guava即可。

spring:
  cache:
    type: guava

使用guava cache

一. 配置 CacheManager,并开启缓存功能。

@EnableCaching
@Configuration
public class CacheConfig {

    @Bean
    public CacheManager cacheManager() {
        GuavaCacheManager cacheManager = new GuavaCacheManager();
        cacheManager.setCacheBuilder(CacheBuilder.newBuilder()
                .expireAfterWrite(10, TimeUnit.SECONDS).maximumSize(1000));
        return cacheManager;
    }
}

如上,我配置了缓存写入后保持10秒

二. 使用注解优雅给接口加上缓存。在访问接口上加上注解。

@Service
@CacheConfig(cacheNames = "product")
public class ProductServiceImpl implements ProductService {

    @Resource
    private ProductMapper productMapper;

    @Override
    @Cacheable
    public Product getObjectById(Long id) {
        return productMapper.selectByPrimaryKey(id);
    }
}

三. 测试本地缓存。 首先开启mybatis的sql输出打印,修改application.yml的配置,增加如下:

mybatis:
  configuration:
    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl

测试本地缓存是否起效,以及缓存时间是否其效果。

@RunWith(SpringJUnit4ClassRunner.class)
@SpringBootTest(classes = App.class)
@Slf4j
public class SbpProductServiceTest {

    @Autowired
    private ProductService productService;

    @Test
    @Rollback
    public void getObjectById() throws InterruptedException {
        Product product1 = productService.getObjectById(1L);
        log.info("time:{} , first get product:{}", System.currentTimeMillis(), product1);
        Product product2 = productService.getObjectById(1L);
        log.info("time:{} , second get product:{}", System.currentTimeMillis(), product2);

        Thread.sleep(15000);

        Product product3 = productService.getObjectById(1L);
        log.info("time:{} , third get product:{}", System.currentTimeMillis(), product3);
    }
}

输出如下:

==>  Preparing: select id, code, type, full_name, alias_name, original_price, vip_price, storage, create_at, update_at from sbp_product where id = ? 
==> Parameters: 1(Long)
<==    Columns: id, code, type, full_name, alias_name, original_price, vip_price, storage, create_at, update_at
<==        Row: 1, BK100001, 1, java编程思想, thinking in java, 86.00, 75.00, 300, 1532260305712, 1532260305712
<==      Total: 1
Closing non transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@25791d40]
2018-07-22 19:53:05.534 [main] INFO  com.crw.service.SbpProductServiceTest - time:1532260385532 , first get product:SbpProduct(id=1, code=BK100001, type=1, fullName=java编程思想, aliasName=thinking in java, originalPrice=86.00, vipPrice=75.00, storage=300, createAt=1532260305712, updateAt=1532260305712)
2018-07-22 19:53:05.537 [main] INFO  com.crw.service.SbpProductServiceTest - time:1532260385537 , second get product:SbpProduct(id=1, code=BK100001, type=1, fullName=java编程思想, aliasName=thinking in java, originalPrice=86.00, vipPrice=75.00, storage=300, createAt=1532260305712, updateAt=1532260305712)
Creating a new SqlSession
SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@6812c8cc] was not registered for synchronization because synchronization is not active
JDBC Connection [com.mysql.jdbc.JDBC4Connection@6075b369] will not be managed by Spring
==>  Preparing: select id, code, type, full_name, alias_name, original_price, vip_price, storage, create_at, update_at from sbp_product where id = ? 
==> Parameters: 1(Long)
<==    Columns: id, code, type, full_name, alias_name, original_price, vip_price, storage, create_at, update_at
<==        Row: 1, BK100001, 1, java编程思想, thinking in java, 86.00, 75.00, 300, 1532260305712, 1532260305712
<==      Total: 1
Closing non transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@6812c8cc]
2018-07-22 19:53:20.547 [main] INFO  com.crw.service.SbpProductServiceTest - time:1532260400547 , third get product:SbpProduct(id=1, code=BK100001, type=1, fullName=java编程思想, aliasName=thinking in java, originalPrice=86.00, vipPrice=75.00, storage=300, createAt=1532260305712, updateAt=1532260305712)
2018-07-22 19:53:20.553 [Thread-4] INFO  org.springframework.web.context.support.GenericWebApplicationContext - Closing org.springframework.web.context.support.GenericWebApplicationContext@6853425f: startup date [Sun Jul 22 19:53:01 CST 2018]; root of context hierarchy

可以看到结果:

  1. 第一次调用打印了sql,此时还没有缓存。
  2. 第二次调用没打印sql,此时有本地缓存了。
  3. 第三次调用又打印sql,此时本地缓存失效。

结束语

再来回顾一下,本文介绍了:

  • spring cache的使用,它是AOP理念的一个很好的应用
  • guava的使用。by the way,从Spring5开始变不再支持guava的实现了,从spring-boot 1.5.x版本你就可以看到autoconfgure包下的GuavaCacheConfiguration以及被注解为@Deprecated了。推荐更好的实现是caffeine
  • spring-boot使用哦cache非常方便,三步完成。

本文参考了:

-------------本文结束,感谢您的阅读-------------
贵在坚持,如果您觉得本文还不错,不妨打赏一下~
0%