1. 简介
Spring 从 3.1 开始定义了 org.springframework.cache.Cache 和 org.springframework.cache.CacheManager 接口来统一不同的缓存技术, 并支持使用 JCache(JSR-107)注解简化我们开发;
Cache 接口为缓存的组件规范定义,包含缓存的各种操作集合。Cache 接 口 下 Spring 提 供 了 各 种 xxxCache 的 实 现 , 如 RedisCache , EhCacheCache , ConcurrentMapCache 等;
每次调用需要缓存功能的方法时,Spring 会检查检查指定参数的指定的目标方法是否已经被调用过。如果有就直接从缓存中获取方法调用后的结果,如果没有就调用方法并缓存结果后返回给用户,下次调用直接从缓存中获取。
使用 Spring 缓存抽象时我们需要关注以下两点:
- 确定方法需要被缓存以及他们的缓存策略
- 从缓存中读取之前缓存存储的数据
2. 简单使用
下面我们使用底层实现采用 Redis 的 Cache 缓存技术
2.1 导入依赖
1 2 3 4 5 6 7 8
| <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>
|
2.2 application.yml 配置缓存
1) 配置 Redis 连接信息:
1 2 3 4
| spring: redis: host: 192.168.160.129 port: 6379
|
2) 配置 cache 使用 redis:
1 2 3
| spring: cache: type: redis
|
2.3 开启缓存
在启动类上加上缓存开启注解 @EnableCaching
1 2 3 4 5 6 7 8 9
| @EnableCaching @SpringBootApplication public class GulimallProductApplication {
public static void main(String[] args) { SpringApplication.run(GulimallProductApplication.class, args); }
}
|
2.4 加缓存注解 @Cacheable
如下代码所示,在方法上加上 @Cacheable 注解后,会将该方法的返回值存储缓存中。当下一次请求调用该方法时,会先查询缓存是否有数据,有则直接将缓存中的数据进行返回。其中注解中的值 “category” 为缓存分区名称。该注解也可以指明 key 值,代表具体的缓存名称(可以这么理解吧~)
1 2 3 4 5 6 7 8 9 10 11
|
@Cacheable("category") @Override public List<CategoryEntity> getLevel1Categorys() { System.out.println("getLevel1Categorys....."); List<CategoryEntity> categoryEntities = baseMapper.selectList(new QueryWrapper<CategoryEntity>().eq("parent_cid", 0)); return categoryEntities; }
|
执行后,redis 中的数据如下所示,其存在如下默认行为:
- key 值 “SimpleKey []” 是默认生成的;
- TTL 的值为 -1,表示该缓存永不过期;
- 缓存的数据在为乱码数据,因为底层默认使用 JDK 进行序列化(源码有体现)
针对上诉的默认行为,可以有针对解决方法:
1) 可以在注解 @Cacheable()中指定 key 的值,比如下述代码即可指定 key 的值为 categoryKey,因为 key 支持使用表达式,所以字符串时需要加单引号:
1
| @Cacheable(value = "category", key = "'categoryKey'")
|
2) TTL 的值可以在配置文件中指定,比如下述代码配置:
1 2
| ## 设置 36s 后过期 spring.cache.redis.time-to-live=36000
|
重新执行后的效果如下:
针对缓存的数据在为乱码的问题,我们需要自定义配置类并更换底层的序列化机制,可以查看下文自定义缓存配置。
3. 自定义缓存配置
查看如下代码,可以使用自定义配置类 MyCacheConfig 实现自定义缓存配置:
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
| import org.springframework.boot.autoconfigure.cache.CacheProperties; import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.cache.annotation.EnableCaching; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.data.redis.cache.RedisCacheConfiguration; import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer; import org.springframework.data.redis.serializer.RedisSerializationContext; import org.springframework.data.redis.serializer.StringRedisSerializer;
@EnableConfigurationProperties(CacheProperties.class) @Configuration @EnableCaching public class MyCacheConfig {
@Bean RedisCacheConfiguration redisCacheConfiguration(CacheProperties cacheProperties) { RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig(); config = config.serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer())); config = config.serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(new GenericJackson2JsonRedisSerializer())); CacheProperties.Redis redisProperties = cacheProperties.getRedis(); if (redisProperties.getTimeToLive() != null) { config = config.entryTtl(redisProperties.getTimeToLive()); } if (redisProperties.getKeyPrefix() != null) { config = config.prefixKeysWith(redisProperties.getKeyPrefix()); } if (!redisProperties.isCacheNullValues()) { config = config.disableCachingNullValues(); } if (!redisProperties.isUseKeyPrefix()) { config = config.disableKeyPrefix(); } return config; } }
|
查看第 27 行代码,这里我们指定缓存中的值采用 json 序列化器进行序列化,加上以上配置后 getLevel1Categorys 方法代码(查看 2.4)执行结果如下所示:
另外,查看 MyCacheConfig 类中的第 38 - 40 行代码,其用来读取配置文件是否配置允许缓存空值,默认为 true
4. @CacheEvict
与 @Cacheable 相反,@CacheEvict 则是从缓存中删除数据。注解 @CacheEvict 中的 value 表示要删除的缓存分区的名称,key 表示要删除的缓存名称,示例如下:
1 2 3 4 5 6 7 8 9 10 11 12
|
@CacheEvict(value = "category", key = "'categoryKey'") @Transactional(rollbackFor = Exception.class) @Override public void updateDetail(CategoryEntity category) { this.updateById(category); categoryBrandRelationService.updateCategoryName(category.getCatId(), category.getName()); }
|
@CacheEvict(value = "category", key = "'categoryKey'")
的作用就是当调用 updateDetail 时,会将缓存分区 category 下名为 categoryKey 缓存删掉。我们同样可以删除整个分区下的所有缓存分区,注解写法为:@CacheEvict(value = "category", allEntries = true)
5. @Caching
@Caching 注解支持组合多步操作(多步删除缓存、多步添加缓存等),比如可以使用 @Caching 组合多个删除缓存 @CacheEvict 操作,示例如下所示:
1 2 3 4
| @Caching(evict = { @CacheEvict(value = "category", key = "'categoryKey'"), @CacheEvict(value = "category", key = "'getCatalogJson'") })
|
6. @CachePut
该注解的作用就是在更新完成数据库时,其会自动将更新方法返回的最新数据添加进缓存中,相当于采用双写模式来解决缓存一致性问题,比如下述方法:
1 2 3 4 5 6 7 8
|
@CachePut(value = "category", key = "'categoryKey'") public List<CategoryEntity> updateDetail(CategoryEntity category) { this.updateById(category); this.list(); }
|
7. SpringCache 使用总结
7.1 不足之处
1)读模式:
2)写模式:(缓存与数据库一致)
- 读写加锁;
- 引入Canal,感知到MySQL的更新去更新数据库;
- 读多写多,直接去数据库查询就行
总结:
- 常规数据:读多写少,即时性和一致性要求不高的数据,完全可以使用Spring-Cache;对于写模式,只要缓存的数据有过期时间就足够了。
- 特殊数据:特殊设计