springboot 缓存
基于注释(annotation)的缓存(cache)技术,它本质上不是一个具体的缓存实现方案,而是一个对缓存使用的抽象,通过在既有代码中添加少量它定义的各种 annotation,即能够达到缓存方法的返回对象的效果。
Spring 的缓存技术还具备相当的灵活性,不仅能够使用 SpEL(Spring Expression Language)来定义缓存的 key 和各种 condition,还提供开箱即用的缓存临时存储方案,也支持和主流的专业缓存例如 EHCache 集成。Spring boot 默认使用的是 SimpleCacheConfiguration,即使用 ConcurrentMapCacheManager 来实现缓存。
相关配置
application.properties
server.port=9090
spring.datasource.url=jdbc:mysql://localhost:3306/test_cache?autoReconnect=true&useUnicode=true&allowMultiQueries=true&useSSL=false&characterEncoding=utf-8
spring.datasource.username=root
spring.datasource.password=root
spring.datasource.platform=mysql
spring.messages.encoding=UTF-8
#jpa
spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.MySQL5Dialect
spring.jpa.open-in-view=true
spring.jpa.hibernate.use-new-id-generator-mappings=true
spring.jpa.properties.hibernate.show_sql=true
spring.jpa.properties.hibernate.naming-strategy=org.hibernate.cfg.ImprovedNamingStrategy
spring.jpa.properties.hibernate.format_sql=true
spring.jpa.hibernate.ddl-auto=update
# logs
spring.output.ansi.enabled=detect
logging.file=logs/log-v1.log
logging.level.com.welooky.cache.demo.*=INFO
logging.level.org.springframework.web=INFO
logging.level.org.hibernate=INFO
#ehcache的配置
spring.cache.ehcache.config=classpath:config/ehcache.xml
dependencies {
compile('org.springframework.boot:spring-boot-starter-cache')
//compile('org.springframework.boot:spring-boot-starter-data-redis')
compile('net.sf.ehcache:ehcache')
compile('org.springframework.boot:spring-boot-starter-web')
compile('org.springframework.boot:spring-boot-starter-data-jpa')
runtime('mysql:mysql-connector-java')
testCompile('org.springframework.boot:spring-boot-starter-test')
}
ehcache.xml
<?xml version="1.0" encoding="UTF-8"?>
<ehcache
updateCheck="true"
monitoring="autodetect"
dynamicConfig="true">
<diskStore path="java.io.tmpdir"/>
<defaultCache
maxElementsInMemory="500"
eternal="false"
timeToIdleSeconds="300"
timeToLiveSeconds="1200"
overflowToDisk="true"
diskSpoolBufferSizeMB="100"/>
<cache name="account"
maxElementsInMemory="1000"
memoryStoreEvictionPolicy="LRU"
timeToIdleSeconds="30000"
timeToLiveSeconds="10000"
diskSpoolBufferSizeMB="400"
overflowToDisk="true"/>
<!--
maxElementsInMemory : 缓存最大个数、
eternal : 对象是否永久有效,一但设置了,timeout将不起作用
timeToIdleSeconds : 设置对象在失效前的允许闲置时间(单位:秒)。
仅当eternal=false对象不是永久有效时使用,可选属性,默认值是0,也就是可闲置时间无穷大
timeToLiveSeconds : 设置对象在失效前允许存活时间(单位:秒)。
最大时间介于创建时间和失效时间之间。仅当eternal=false对象不是永久有效时使用,默认是0.也就是对象存活时间无穷大。
overflowToDisk : 当内存中对象数量达到maxElementsInMemory时,Ehcache将会对象写到磁盘中
diskSpoolBufferSizeMB : 这个参数设置DiskStore(磁盘缓存)的缓存区大小。默认是30MB。每个Cache都应该有自己的一个缓冲区。
maxElementsOnDisk : 硬盘最大缓存个数。
diskPersistent : 是否缓存虚拟机重启期数据
diskExpiryThreadIntervalSeconds : 磁盘失效线程运行时间间隔,默认是120秒。
memoryStoreEvictionPolicy : 当达到maxElementsInMemory限制时,Ehcache将会根据指定的策略去清理内存。
默认策略是LRU(最近最少使用)。你可以设置为FIFO(先进先出)或是LFU(较少使用)
clearOnFlush: 内存数量最大时是否清除。
-->
</ehcache>
- 使用@EnableCaching 启用 Cache 注解支持;
- 实现 CachingConfigurer,然后注入需要的 cacheManager 和 keyGenerator;从 spring4 开始默认的 keyGenerator 是 SimpleKeyGenerator;
- Spring cache 利用了 Spring AOP 的动态代理技术
注解说明
@Cacheable
主要针对方法配置,能够根据方法的请求参数对其结果进行缓存。
- value :
缓存的名称,在 spring 配置文件中定义,必须指定至少一个
@Cacheable(value="account") 或者@Cacheable(value={"cache1","cache2"}
- key :
缓存的 key,可以为空,如果指定要按照 SpEL 表达式编写,如果不指定,则按照方法的所有参数进行组合
@Cacheable(value="account",key="#userName")
- condition:
缓存的条件,可以为空,使用 SpEL。返回 true 或者 false,只有为 true 才进行缓存
@Cacheable(value="account",condition="#userName.length()>2")
- unless
缓存的条件,非必需,使用 SpEL。该条件是在函数被调用之后才做判断的,它可以对 result 进行判断。
@CachePut
配置于函数上,能够根据参数定义条件来进行缓存,它与@Cacheable 不同的是,它可以既保证方法被调用,
又可以实现结果被缓存。所以主要用于数据新增和修改操作上,同时能够起到更新缓存的作用。它的参数与@Cacheable 类似,具体功能可参考上面对@Cacheable 参数的说明。
@CacheEvict
配置于函数上,通常用在删除方法上,用来从缓存中移除相应数据。
- value
缓存的名称,在 spring 配置文件中定义,必须指定至少一个
@CachEvict(value="mycache") 或者 @CachEvict(value={"cache1","cache2"}
- key
缓存的 key,可以为空,如果指定要按照 SpEL 表达式编写,如果不指定,则缺省按照方法的所有参数进行组合
@CachEvict(value="account",key="#userName")
- condition
缓存的条件,可以为空,使用 SpEL 编写,返回 true 或者 false,只有为 true 才清空缓存
@CachEvict(value="account",condition="#userName.length()>2")
- allEntries
是否清空所有缓存内容,缺省为 false,如果指定为 true,则方法调用后将立即清空所有缓存
@CachEvict(value="account",allEntries=true)
- beforeInvocation
非必需,默认为 false,会在调用方法之后移除数据,如果方法执行中抛出异常,则不会清空缓存。为 true 时,会在调用方法之前移除数据。
@CachEvict(value="account",beforeInvocation=true)
@CacheConfig
是类级别的注解,表示该类中方法定义的所有缓存操作默认使用的缓存 name。如果方法上有@Cacheable 注解,会覆盖它。
- spring cache 是基于动态生成的 proxy 代理机制来对方法的调用进行切面,这里关键点是对象的引用问题,如果对象的方法是内部调用(即 this 引用)而不是外部引用,则会导致 proxy 失效,那么我们的切面就失效,也就是说上面定义的各种注释包括 @Cacheable、@CachePut 和 @CacheEvict 都会失效,缓存不起作用。
- 和内部调用问题类似,非 public 方法如果想实现基于注释的缓存,必须采用基于 AspectJ 的 AOP 机制
@Service
public class AccountService {
@Resource
private AccountRepository accountRepository;
private Logger logger = LoggerFactory.getLogger(AccountService.class);
private static final String CACHE_ACCOUNT = "account";
public void saveUser(String username, String password) {
Account account = new Account(username, password);
accountRepository.save(account);
}
@Cacheable(value = CACHE_ACCOUNT, key = "#root.caches[0].name + ':' +#username")
public Account findByUsername(String username) {
logger.info("select info from db");
return accountRepository.findByUsername(username);
}
@CachePut(value = CACHE_ACCOUNT, key = "#root.caches[0].name + ':' +#username")
public Account updateUserInfo(String username, String password) {
Account account = accountRepository.findByUsername(username);
account.setPassword(password);
logger.info("update info inside db");
return accountRepository.save(account);
}
@CacheEvict(value = CACHE_ACCOUNT, key = "#scope+':'+#username")
public void clearCache(String username, String scope) {
logger.info("clear cache");
}
@Cacheable(value = CACHE_ACCOUNT, keyGenerator = "aKeyGenerator")
public List<Account> findAll() {
return accountRepository.findAll();
}
@Cacheable(value = CACHE_ACCOUNT, keyGenerator = "bKeyGenerator")
public Account getAccountById(Long id) {
return accountRepository.getOne(id);
}
@CacheEvict(value = CACHE_ACCOUNT, keyGenerator = "bKeyGenerator")
public void deleteAccountById(Long id) {
accountRepository.deleteById(id);
}
}
知识索引:
Redis
Redis是一个开源,先进的key-value存储,并用于构建高性能,可扩展的Web应用程序的完美解决方案。
Redis从它的许多竞争继承来的三个主要特点:
Redis数据库完全在内存中,使用磁盘仅用于持久性。
相比许多键值数据存储,Redis拥有一套较为丰富的数据类型。
Redis可以将数据复制到任意数量的从服务器。
Redis 优势
异常快速:Redis的速度非常快,每秒能执行约11万集合,每秒约81000+条记录。
支持丰富的数据类型:Redis支持最大多数开发人员已经知道像列表,集合,有序集合,散列数据类型。这使得它非常容易解决各种各样的问题,因为我们知道哪些问题是可以处理通过它的数据类型更好。
操作都是原子性:所有Redis操作是原子的,这保证了如果两个客户端同时访问的Redis服务器将获得更新后的值。
多功能实用工具:Redis是一个多实用的工具,可以在多个用例如缓存,消息,队列使用(Redis原生支持发布/订阅),任何短暂的数据,应用程序,如Web应用程序会话,网页命中计数等
Redis 安装
-
mac
- 执行 brew install redis
- 启动 redis,可以使用后台服务启动 brew services start redis。或者直接启动:redis-server /usr/local/etc/redis.conf
- $ redis-cli
-
ubuntu
- $ sudo apt-get update
- $ sudo apt-get install redis-server
- $ redis-server
- $ redis-cli
-
linux
- $ wget
http://download.redis.io/releases/redis-4.0.6.tar.gz
- $ tar xzf redis-4.0.6.tar.gz
- $ cd redis-4.0.6
- $ make
- $ cd src
- $ ./redis-server
- $ ./redis-cli
- $ wget
知识索引:centos install redis
Redis 配置
-
$ redis-cli
:该命令会连接本地的 redis 服务 - 连接远程:
$ redis-cli -h host -p port -a password
- 查看所有配置:
config get *
- 查看某个配置:
config get requirepass
- 修改某个配置:
config set config_setting_name new_config_value
临时设置:config set
永久设置:config rewrite,将目前服务器的参数配置写入redis.conf
知识索引:
Redis 命令
- 删除当前数据库中的所有 Key:
flushdb
- 删除所有数据库中的 key:
flushall
- 删除指定 key:
del
key
项目配置
去掉 Elcache 的相关配置,使项目的缓存切换到 Redis
application.properties
# redis
# Redis数据库索引(默认为0)
spring.redis.database=0
# Redis服务器地址
spring.redis.host=localhost
# Redis服务器连接端口
spring.redis.port=6379
# Redis服务器连接密码(默认为空)
spring.redis.password=
# 连接池最大连接数(使用负值表示没有限制)
spring.redis.jedis.pool.max-active=10
# 连接池最大阻塞等待时间(使用负值表示没有限制)
spring.redis.jedis.pool.max-wait=-1ms
# 连接池中的最大空闲连接
spring.redis.jedis.pool.max-idle=8
# 连接池中的最小空闲连接
spring.redis.jedis.pool.min-idle=0
# 连接超时时间(毫秒)
spring.redis.timeout=1000ms
修改 gradle
dependencies {
compile('org.springframework.boot:spring-boot-starter-cache')
compile('org.springframework.boot:spring-boot-starter-data-redis')
//compile('net.sf.ehcache:ehcache')
}
RedisConfig
package com.welooky.cache.demo.config;
import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.PropertyAccessor;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.welooky.cache.demo.entity.Account;
import org.springframework.cache.CacheManager;
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.cache.annotation.CachePut;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.cache.interceptor.KeyGenerator;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.cache.RedisCacheConfiguration;
import org.springframework.data.redis.cache.RedisCacheManager;
import org.springframework.data.redis.connection.jedis.JedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.RedisSerializationContext;
import org.springframework.data.redis.serializer.StringRedisSerializer;
import java.lang.reflect.Method;
@Configuration
public class RedisConfig {
@Bean("aKeyGenerator")
public KeyGenerator aKeyGenerator() {
return (target, method, params) -> {
StringBuilder sb = new StringBuilder();
sb.append(target.getClass().getName());
sb.append(".").append(method.getName());
StringBuilder paramsSb = new StringBuilder();
for (Object param : params) {
if (param != null) {
paramsSb.append("_").append(param.toString());
}
}
if (paramsSb.length() > 0) {
sb.append("_").append(paramsSb);
}
return sb.toString();
};
}
@Bean("bKeyGenerator")
public KeyGenerator bKeyGenerator() {
return new KeyGenerator() {
@Override
public Object generate(Object target, Method method, Object... params) {
StringBuilder sb = new StringBuilder();
String[] value = new String[1];
Cacheable cacheable = method.getAnnotation(Cacheable.class);
if (cacheable != null) {
value = cacheable.value();
}
CachePut cachePut = method.getAnnotation(CachePut.class);
if (cachePut != null) {
value = cachePut.value();
}
CacheEvict cacheEvict = method.getAnnotation(CacheEvict.class);
if (cacheEvict != null) {
value = cacheEvict.value();
}
sb.append(value[0]);
for (Object obj : params) {
sb.append(":").append(obj.toString());
}
return sb.toString();
}
};
}
@Bean
public JedisConnectionFactory connectionFactory() {
return new JedisConnectionFactory();
}
@Bean
public CacheManager cacheManager(JedisConnectionFactory connectionFactory) {
ObjectMapper om = new ObjectMapper();
om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
GenericJackson2JsonRedisSerializer jackson2JsonRedisSerializer = new GenericJackson2JsonRedisSerializer(om);
RedisCacheConfiguration redisCacheConfiguration = RedisCacheConfiguration.defaultCacheConfig()
.serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer()))
.serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(jackson2JsonRedisSerializer));
return RedisCacheManager.builder(connectionFactory)
.cacheDefaults(redisCacheConfiguration)
.build();
}
@Bean
public RedisTemplate<String, Account> accountTemplate(JedisConnectionFactory connectionFactory) {
RedisTemplate<String, Account> template = new RedisTemplate<>();
template.setConnectionFactory(connectionFactory);
template.setKeySerializer(new StringRedisSerializer());
template.setValueSerializer(new Jackson2JsonRedisSerializer<>(Account.class));
return template;
}
}
使用 keyGenerator
@Cacheable(value = "account", keyGenerator = "aKeyGenerator")
public List<Account> findAll() {
return accountRepository.findAll();
}
test
@Resource
private RedisTemplate<String, Account> redisTemplate;
@Test
public void testRedisTemplate() {
redisTemplate.opsForValue().set("andy", new Account("mingXi", "mingxi"));
Assert.assertEquals("mingXi", redisTemplate.opsForValue().get("andy").getUsername());
}
生成的缓存 key 为:account::com.welooky.cache.demo.service.AccountService.findAll
使用命令
keys *
查看所有的缓存 key.
@Configuration
public class RedisConfig {
@Resource
private LettuceConnectionFactory lettuceConnectionFactory;
@Bean
public CacheManager cacheManager() {
ObjectMapper om = new ObjectMapper();
om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
GenericJackson2JsonRedisSerializer jackson2JsonRedisSerializer = new GenericJackson2JsonRedisSerializer(om);
RedisCacheConfiguration redisCacheConfiguration = RedisCacheConfiguration.defaultCacheConfig()
.entryTtl(Duration.ofDays(30))
.serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer()))
.serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(jackson2JsonRedisSerializer));
return RedisCacheManager.builder(lettuceConnectionFactory)
.cacheDefaults(redisCacheConfiguration)
.build();
}
}
implementation 'com.fasterxml.jackson.datatype:jackson-datatype-hibernate5:2.9.8'
@Configuration
public class WebMvcConfig extends WebMvcConfigurerAdapter {
@Override
public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
List<MediaType> supportedMediaTypes = new ArrayList<>();
supportedMediaTypes.add(MediaType.APPLICATION_JSON);
supportedMediaTypes.add(MediaType.TEXT_PLAIN);
MappingJackson2HttpMessageConverter converter = new MappingJackson2HttpMessageConverter();
converter.setObjectMapper(new HibernateAwareObjectMapper());
converter.setPrettyPrint(true);
converter.setSupportedMediaTypes(supportedMediaTypes);
converters.add(converter);
super.configureMessageConverters(converters);
}
}
public class HibernateAwareObjectMapper extends ObjectMapper {
public HibernateAwareObjectMapper() {
registerModule(new Hibernate5Module());
}
}
知识索引: