前言
如果说如何优化你的网站或者应用,大部分同学第一的反应可能就是缓存
。缓存不是万能的,但是当用户和访问量加大的情况下,缓存是提高应用吞吐量非常有效的手段。
本文主要介绍如果通过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
可以看到结果:
- 第一次调用打印了sql,此时还没有缓存。
- 第二次调用没打印sql,此时有本地缓存了。
- 第三次调用又打印sql,此时本地缓存失效。
结束语
再来回顾一下,本文介绍了:
- spring cache的使用,它是AOP理念的一个很好的应用
- guava的使用。by the way,从Spring5开始变不再支持guava的实现了,从spring-boot 1.5.x版本你就可以看到
autoconfgure
包下的GuavaCacheConfiguration
以及被注解为@Deprecated
了。推荐更好的实现是caffeine。 - spring-boot使用哦cache非常方便,三步完成。
本文参考了: