前言
之前说了使用JdbcTemplate、MyBatis、Spring Data JPA三种连接数据库的方式。
对于一些不常变更,或者是数据库数据太多,需要优化请求速度的,我们可以使用二级缓存解决。
二级缓存分两种
- 本地缓存 比如 Ehcache
- 远程缓存 比如 Redis
我们可以根据实际情况选择对应的缓存。
Ehcache
在使用 Spring Data JPA 进行数据访问时,可以使用二级缓存来提高程序的性能。
注意
这里使用的不是基于Hibernate 的Ehcache实现。
使用注意
二级缓存也存在一些潜在的问题,如缓存的数据可能不是最新的(缓存不一致)、缓存数据的内存占用等。
因此,在使用二级缓存时,需要根据具体的业务场景和需求来决定是否使用以及如何配置和管理缓存。
以下演示了如何在 Spring Boot 应用程序中配置 Ehcache 作为二级缓存。
添加依赖
在 pom.xml 文件中添加 Ehcache 依赖
1 2 3 4 5 6 7 8
| <dependency> <groupId>net.sf.ehcache</groupId> <artifactId>ehcache</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-cache</artifactId> </dependency>
|
添加Ehcache 配置
创建 Ehcache 配置文件 ehcache.xml
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
| <?xml version="1.0" encoding="UTF-8"?> <ehcache xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="http://www.ehcache.org/ehcache.xsd" updateCheck="false"> <diskStore path="java.io.tmpdir"/> <defaultCache maxElementsInMemory="10000" maxElementsOnDisk="0" eternal="false" overflowToDisk="false" diskPersistent="false" timeToIdleSeconds="3" timeToLiveSeconds="3" diskSpoolBufferSizeMB="50" diskExpiryThreadIntervalSeconds="10" memoryStoreEvictionPolicy="LFU"/> <cache name="userService.getUserByName" maxEntriesLocalHeap="10000" eternal="false" overflowToDisk="false" diskPersistent="false" timeToIdleSeconds="3" timeToLiveSeconds="3"> </cache> </ehcache>
|
在上述示例代码中,创建了名为 userService.getUserByName 的缓存区域,设置了最大缓存数、缓存时间等属性。
参数解释
各个熟悉的含义
name 缓存空间名称(非缓存key)
maxElementsInMemory:设置了缓存的上限,最多存储多少个记录对象
maxElementsOnDisk:硬盘中最大缓存对象数,若是0表示无穷大
eternal:true表示对象永不过期,此时会忽略timeToIdleSeconds和timeToLiveSeconds属性,默认为false
overflowToDisk:true表示当内存缓存的对象数目达到了maxElementsInMemory界限后,会把溢出的对象写到硬盘缓存中。注意:如果缓存的对象要写入到硬盘中的话,则该对象必须实现了Serializable接口才行。
diskSpoolBufferSizeMB:磁盘缓存区大小,默认为30MB。每个Cache都应该有自己的一个缓存区。
diskPersistent:是否缓存虚拟机重启期数据,设置成true表示缓存虚拟机重启期数据
diskExpiryThreadIntervalSeconds:磁盘失效线程运行时间间隔,默认为120秒
timeToIdleSeconds: 设定允许对象处于空闲状态的最长时间,以秒为单位。当对象自从最近一次被访问后,如果处于空闲状态的时间超过了timeToIdleSeconds属性值,这个对象就会过期,EHCache将把它从缓存中清空。只有当eternal属性为false,该属性才有效。如果该属性值为0,则表示对象可以无限期地处于空闲状态
timeToLiveSeconds:设定对象允许存在于缓存中的最长时间,以秒为单位。
当对象自从被存放到缓存中后,如果处于缓存中的时间超过了 timeToLiveSeconds属性值,这个对象就会过期,EHCache将把它从缓存中清除。只有当eternal属性为false,该属性才有效。
如果该属性值为0,则表示对象可以无限期地存在于缓存中。timeToLiveSeconds必须大于timeToIdleSeconds属性,才有意义
memoryStoreEvictionPolicy:当达到maxElementsInMemory限制时,Ehcache将会根据指定的策略去清理内存。可选策略有:LRU(最近最少使用,默认策略)、FIFO(先进先出)
添加配置
在 application.properties 文件中启用二级缓存
1 2
| spring.cache.type=ehcache spring.cache.ehcache.config=classpath:ehcache.xml
|
添加配置类
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34
| package cn.psvmc.zapijpa.cache;
import org.springframework.boot.autoconfigure.cache.CacheProperties; import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.cache.ehcache.EhCacheCacheManager; import org.springframework.cache.ehcache.EhCacheManagerFactoryBean; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration;
import java.util.Objects;
@Configuration @EnableConfigurationProperties(CacheProperties.class) public class EhCacheConfiguration {
private final CacheProperties cacheProperties;
public EhCacheConfiguration(CacheProperties cacheProperties) { this.cacheProperties = cacheProperties; }
@Bean public EhCacheManagerFactoryBean ehCacheManagerFactory() { EhCacheManagerFactoryBean ehCacheManagerFactory = new EhCacheManagerFactoryBean(); ehCacheManagerFactory.setConfigLocation(cacheProperties.resolveConfigLocation(cacheProperties.getEhcache().getConfig())); ehCacheManagerFactory.setShared(true); return ehCacheManagerFactory; }
@Bean public EhCacheCacheManager ehCacheCacheManager(EhCacheManagerFactoryBean ehCacheManagerFactory) { return new EhCacheCacheManager(Objects.requireNonNull(ehCacheManagerFactory.getObject())); } }
|
代码配置
在Application上添加@EnableCaching,开启缓存。
设置缓存
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29
| import cn.psvmc.zapijpa.entity.UserEntity; import cn.psvmc.zapijpa.repository.UserRepository; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.cache.annotation.Cacheable; import org.springframework.cache.annotation.EnableCaching; import org.springframework.stereotype.Service;
import javax.transaction.Transactional; import java.util.List;
@Service @Transactional public class UserService { @Autowired private UserRepository userRepository; @Cacheable(cacheNames = "userService.getUserByName",key = "#name") public List<UserEntity> getUserByName(String name) { return userRepository.findByName(name); } @Transactional @CacheEvict(cacheNames = "userService.getUserByName",allEntries = true) public List<UserEntity> addAll(List<UserEntity> users) { return userRepository.saveAll(users); } }
|
Controller
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35
| import cn.psvmc.zapijpa.entity.UserEntity; import cn.psvmc.zapijpa.service.UserService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController;
import java.util.Arrays; import java.util.List;
@RestController @RequestMapping("/user") public class UserController { @Autowired private UserService userService;
@RequestMapping("/get_user") public List<UserEntity> getUser() { return userService.getUserByName("张"); }
@RequestMapping("/get_user2") public List<UserEntity> getUser2() { return userService.getUserByName("王"); } @RequestMapping("/add_users") public List<UserEntity> addUsers() { UserEntity u1 = new UserEntity("张三",10); UserEntity u2 = new UserEntity("李四",18); UserEntity u3 = new UserEntity("王五",22); UserEntity u4 = new UserEntity("赵六",16); List<UserEntity> userList = Arrays.asList(u1, u2, u3, u4); return userService.addAll(userList); } }
|
访问
http://localhost:8080/user/get_user
http://localhost:8080/user/get_user2
http://localhost:8080/user/add_users
这样上面的两个请求都会在第一次的时候查询数据库,后面的请求都用的缓存。
超时时间未生效
关键的问题在于没有指定缓存类型为ehcache,ehcache.xml文件压根就没有生效。
springboot使用默认的SimpleCacheConfiguration,不是用的ehcache。
解决方法如下:
application.properties添加如下
1 2
| spring.cache.type=ehcache spring.cache.ehcache.config=classpath:ehcache.xml
|
Redis
添加依赖
1 2 3 4 5 6 7 8 9
| <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-cache</artifactId> </dependency>
|
在Application上添加@EnableCaching,开启缓存。
添加配置
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| spring.cache.type=redis
spring.redis.database=0
spring.redis.host=cachetest.xhkjedu.com
spring.redis.port=6379
spring.redis.password=xhkjedud07
spring.redis.jedis.pool.max-idle=8
spring.redis.jedis.pool.max-wait=-1
spring.redis.jedis.pool.min-idle=0
spring.redis.timeout=10000
|
配置类
注意:
配置缓存管理器CacheManager有两种方式:
- 方式1:通过RedisCacheConfiguration.defaultCacheConfig()获取到默认的RedisCacheConfiguration对象,修改RedisCacheConfiguration对象的序列化方式等参数。
- 方式2:通过继承CachingConfigurerSupport类自定义缓存管理器,覆写各方法。
两种方式任选其一即可。
方式1
默认序列化在redis中保存的类似于这样,不太好排查
我们可以自定义配置类设置序列化方式
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111
| package cn.psvmc.zapijpa.cache;
import org.springframework.cache.CacheManager; import org.springframework.cache.interceptor.KeyGenerator; import org.springframework.cache.interceptor.SimpleKeyGenerator; 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.RedisConnectionFactory; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer; import org.springframework.data.redis.serializer.RedisSerializationContext; import org.springframework.data.redis.serializer.StringRedisSerializer; import java.time.Duration; import java.util.HashMap; import java.util.Map;
@Configuration public class RedisConfiguration {
@Bean public CacheManager cacheManager(RedisConnectionFactory redisConnectionFactory) { RedisCacheConfiguration redisCacheConfiguration = RedisCacheConfiguration.defaultCacheConfig(); redisCacheConfiguration = redisCacheConfiguration .serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(StringRedisSerializer.UTF_8)) .serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(serializer())) .disableCachingNullValues() .prefixCacheNameWith("spring-cache:") .entryTtl(Duration.ofMinutes(30L)); Map<String, RedisCacheConfiguration> map = new HashMap<>();
return RedisCacheManager .builder(redisConnectionFactory) .cacheDefaults(redisCacheConfiguration) .withInitialCacheConfigurations(map) .build(); }
@Bean public KeyGenerator keyGenerator() {
return (target, method, params) -> { StringBuilder sb = new StringBuilder(); sb.append(target.getClass().getSimpleName()).append(":"); sb.append(method.getName()).append(":"); Object key = SimpleKeyGenerator.generateKey(params); sb.append(key); return sb; }; }
@Bean public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) { GenericJackson2JsonRedisSerializer genericJackson2JsonRedisSerializer = serializer(); RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>(); redisTemplate.setKeySerializer(StringRedisSerializer.UTF_8); redisTemplate.setValueSerializer(genericJackson2JsonRedisSerializer); redisTemplate.setHashKeySerializer(StringRedisSerializer.UTF_8); redisTemplate.setHashValueSerializer(genericJackson2JsonRedisSerializer); redisTemplate.setConnectionFactory(redisConnectionFactory); return redisTemplate; }
public GenericJackson2JsonRedisSerializer serializer() { return new GenericJackson2JsonRedisSerializer(); } }
|
这样序列化的结果就方便查看了。
方式2
继承CachingConfigurerSupport的方式。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105
| package cn.psvmc.zapijpa.cache;
import com.fasterxml.jackson.annotation.JsonAutoDetect; import com.fasterxml.jackson.annotation.PropertyAccessor; import com.fasterxml.jackson.databind.ObjectMapper; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Value; import org.springframework.cache.CacheManager; import org.springframework.cache.annotation.CachingConfigurerSupport; import org.springframework.cache.annotation.EnableCaching; 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.cache.RedisCacheWriter; import org.springframework.data.redis.connection.RedisConnectionFactory; import org.springframework.data.redis.core.StringRedisTemplate; import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer; import org.springframework.data.redis.serializer.RedisSerializationContext; import org.springframework.lang.Nullable;
import java.lang.reflect.Method; import java.time.Duration; import java.util.HashMap; import java.util.Map;
@Configuration @EnableCaching public class RedisConfig extends CachingConfigurerSupport { @Value("${cache.expireTime}") private Integer cacheExpireTime; private final static Logger log = LoggerFactory.getLogger(RedisConfig.class);
@Bean public CacheManager cacheManager(RedisConnectionFactory connectionFactory) { StringRedisTemplate template = new StringRedisTemplate(connectionFactory); template.setValueSerializer(new Jackson2JsonRedisSerializer<>(Object.class)); template.setHashValueSerializer(new Jackson2JsonRedisSerializer<>(Object.class)); template.afterPropertiesSet(); return new RedisCacheManager( RedisCacheWriter.nonLockingRedisCacheWriter(connectionFactory), getDefaultTtlRedisCacheConfiguration(cacheExpireTime), getCustomizeTtlRedisCacheConfigurationMap() ); }
private RedisCacheConfiguration getDefaultTtlRedisCacheConfiguration(Integer seconds) { Jackson2JsonRedisSerializer<Object> jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer<>(Object.class); ObjectMapper om = new ObjectMapper(); om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY); om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL); jackson2JsonRedisSerializer.setObjectMapper(om); RedisCacheConfiguration redisCacheConfiguration = RedisCacheConfiguration.defaultCacheConfig(); redisCacheConfiguration = redisCacheConfiguration .serializeValuesWith( RedisSerializationContext .SerializationPair .fromSerializer(jackson2JsonRedisSerializer) ) .prefixCacheNameWith("spring-cache:") .entryTtl(Duration.ofSeconds(seconds)); return redisCacheConfiguration; }
private Map<String, RedisCacheConfiguration> getCustomizeTtlRedisCacheConfigurationMap() { Map<String, RedisCacheConfiguration> redisCacheConfigurationMap = new HashMap<>(); redisCacheConfigurationMap.put("dictionary", this.getDefaultTtlRedisCacheConfiguration(cacheExpireTime)); return redisCacheConfigurationMap; }
@Nullable @Override public KeyGenerator keyGenerator() { return new KeyGenerator() { @Override public Object generate(Object target, Method method, Object... params) { StringBuilder sb = new StringBuilder(); sb.append(target.getClass().getName()); sb.append("_"); sb.append(method.getName()); for (Object obj : params) { sb.append("_"); sb.append(obj.toString()); } log.info("缓存自动生成key:" + sb); return sb.toString(); } }; } }
|
配置文件中添加
代码配置
代码配置和上面一样就行
注意一点实体类必须实现序列化,如果自定义了配置类中大的序列化方式则不用实现。
1 2
| public class UserEntity implements Serializable { }
|
缓存失效时间
上面设置了全局的缓存失效时间
我们可以设置通过代码设置缓存的失效时间
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.stereotype.Component;
import java.util.concurrent.TimeUnit;
@Component public class RedisCacheConfig { @Autowired private RedisTemplate<String, Object> redisTemplate;
public void setExpireTime(String cacheKey, long expireSeconds) { redisTemplate.expire(cacheKey,expireSeconds, TimeUnit.SECONDS); } }
|
但是因为最终生成的的key是拼接的,目前没找到好的设置的方法。
缓存注解
启用缓存
需要缓存的地方在类上添加@EnableCaching
添加缓存
在方法上添加@Cacheable(cacheNames = "userService.getUserByName",key = "#name")
其中
cacheNames 时缓存的名称也可以使用value,使用Ehcache的时候,如果和XML配置中的对应,可以生效对应的规则,如果不对应会使用默认规则。
key 如果方法有参数,可以放在key上。这样参数不同都可以产生新的缓存。
缓存名称规则
缓存最终生成的key的规则是
prefix+cacheNames+”::”+key
其中
- cacheNames是必须要设置的
- key可以不设置,默认是根据方法的参数的值拼接的。也可以自定义默认的生成规则,或者指定生成器。
删除缓存
删除缓存@CacheEvict(cacheNames = "userService.getUserByName",allEntries = true)
当执行addAll的时候,因为用户改变了,所以我们清除缓存。
allEntries:是否清除这个缓存(cacheNames)中的所有数据。默认false。
无论我们缓存的时候是否设置了key,都要设置allEntries = true,否则无法删除缓存。
| 相关注解或概念 |
说明 |
| @EnableCaching |
开启基于注解的缓存 |
| @Cacheable |
主要针对方法配置,能够根据方法的请求参数对其结果进行缓存,缓存后再次调用方法则直接返回缓存内容。 |
| @CachePut |
保证方法被调用,并缓存结果。常用于更新数据。 |
| @CacheEvict |
清空缓存 |
@CacheConfig
所有的@Cacheable()里面都有一个value="xxx"的属性,这显然如果方法多了,写起来也是挺累的,我们可以在类上声明@CacheConfig
声明后在方法上只需要添加@Cacheable就可以了。
1 2 3 4 5 6 7 8
| @CacheConfig(cacheNames = "user") public class UserService { @Cacheable public List<UserEntity> getUserByName(String name) { return userRepository.findByName(name); } }
|
注意注解引用的类
1 2
| import org.springframework.cache.annotation.Cacheable; import org.springframework.cache.annotation.EnableCaching;
|
条件缓存
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| @Cacheable(value = "user", key = "#id", condition = "#id lt 10") public User conditionFindById(final Long id)
@CachePut(value = "user", key = "#id", condition = "#result.username ne 'zhang'") public User conditionSave(final User user)
@CachePut(value = "user", key = "#user.id", unless = "#result.username eq 'zhang'") public User conditionSave2(final User user)
@CacheEvict(value = "user", key = "#user.id", beforeInvocation = false, condition = "#result.username ne 'zhang'") public User conditionDelete(final User user)
|