赞
踩
缓存可以通过将经常访问的数据存储在内存中,减少底层数据源如数据库的压力,从而有效提高系统的性能和稳定性。我想大家的项目中或多或少都有使用过,我们项目也不例外,但是最近在review公司的代码的时候写的很蠢且low, 大致写法如下:
- public User getById(String id) {
- User user = cache.getUser();
- if(user != null) {
- return user;
- }
- // 从数据库获取
- user = loadFromDB(id);
- cahce.put(id, user);
- return user;
- }
- 复制代码
其实Spring Boot 提供了强大的缓存抽象,可以轻松地向您的应用程序添加缓存。本文就讲讲如何使用 Spring 提供的不同缓存注解实现缓存的最佳实践。
现在大部分项目都是是SpringBoot项目,我们可以在启动类添加注解@EnableCaching
来开启缓存功能。
- @SpringBootApplication
- @EnableCaching
- public class SpringCacheApp {
-
- public static void main(String[] args) {
- SpringApplication.run(Cache.class, args);
- }
- }
- 复制代码
既然要能使用缓存,就需要有一个缓存管理器Bean,默认情况下,@EnableCaching
将注册一个ConcurrentMapCacheManager
的Bean,不需要单独的 bean 声明。ConcurrentMapCacheManage
r将值存储在ConcurrentHashMap
的实例中,这是缓存机制的最简单的线程安全实现。
默认的缓存管理器并不能满足需求,因为她是存储在jvm内存中的,那么如何存储到redis中呢?这时候需要添加自定义的缓存管理器。
- <dependency>
- <groupId>org.springframework.boot</groupId>
- <artifactId>spring-boot-starter-data-redis</artifactId>
- </dependency>
- 复制代码
- @Configuration
- @EnableCaching
- public class CacheConfig {
-
- @Bean
- public RedisConnectionFactory redisConnectionFactory() {
- return new LettuceConnectionFactory();
- }
-
- @Bean
- public CacheManager cacheManager() {
- RedisCacheConfiguration redisCacheConfiguration = RedisCacheConfiguration.defaultCacheConfig()
- .disableCachingNullValues()
- .serializeValuesWith(SerializationPair.fromSerializer(new GenericJackson2JsonRedisSerializer()));
-
- RedisCacheManager redisCacheManager = RedisCacheManager.builder(redisConnectionFactory())
- .cacheDefaults(redisCacheConfiguration)
- .build();
-
- return redisCacheManager;
- }
- }
- 复制代码
![](https://csdnimg.cn/release/blogv2/dist/pc/img/newCodeMoreWhite.png)
现在有了缓存管理器以后,我们如何在业务层面操作缓存呢?
我们可以使用@Cacheable
、@CachePut
或@CacheEvict
注解来操作缓存了。
该注解可以将方法运行的结果进行缓存,在缓存时效内再次调用该方法时不会调用方法本身,而是直接从缓存获取结果并返回给调用方。
例子1:缓存数据库查询的结果。
- @Service
- public class MyService {
-
- @Autowired
- private MyRepository repository;
-
- @Cacheable(value = "myCache", key = "#id")
- public MyEntity getEntityById(Long id) {
- return repository.findById(id).orElse(null);
- }
- }
- 复制代码
在此示例中,@Cacheable
注解用于缓存 getEntityById()
方法的结果,该方法根据其 ID
从数据库中检索 MyEntity 对象。
但是如果我们更新数据呢?旧数据仍然在缓存中?
然后@CachePut
出来了, 与 @Cacheable
注解不同的是使用 @CachePut
注解标注的方法,在执行前不会去检查缓存中是否存在之前执行过的结果,而是每次都会执行该方法,并将执行结果以键值对的形式写入指定的缓存中。@CachePut
注解一般用于更新缓存数据,相当于缓存使用的是写模式中的双写模式。
- @Service
- public class MyService {
-
- @Autowired
- private MyRepository repository;
-
- @CachePut(value = "myCache", key = "#entity.id")
- public void saveEntity(MyEntity entity) {
- repository.save(entity);
- }
- }
- 复制代码
标注了 @CacheEvict
注解的方法在被调用时,会从缓存中移除已存储的数据。@CacheEvict
注解一般用于删除缓存数据,相当于缓存使用的是写模式中的失效模式。
- @Service
- public class MyService {
-
- @Autowired
- private MyRepository repository;
-
- @CacheEvict(value = "myCache", key = "#id")
- public void deleteEntityById(Long id) {
- repository.deleteById(id);
- }
- }
- 复制代码
@Caching
注解用于在一个方法或者类上,同时指定多个 Spring Cache 相关的注解。
例子1:@Caching
注解中的evict
属性指定在调用方法 saveEntity
时失效两个缓存。
- @Service
- public class MyService {
-
- @Autowired
- private MyRepository repository;
-
- @Cacheable(value = "myCache", key = "#id")
- public MyEntity getEntityById(Long id) {
- return repository.findById(id).orElse(null);
- }
-
- @Caching(evict = {
- @CacheEvict(value = "myCache", key = "#entity.id"),
- @CacheEvict(value = "otherCache", key = "#entity.id")
- })
- public void saveEntity(MyEntity entity) {
- repository.save(entity);
- }
-
- }
- 复制代码
![](https://csdnimg.cn/release/blogv2/dist/pc/img/newCodeMoreWhite.png)
例子2:调用getEntityById
方法时,Spring会先检查结果是否已经缓存在myCache
缓存中。如果是,Spring
将返回缓存的结果而不是执行该方法。如果结果尚未缓存,Spring 将执行该方法并将结果缓存在 myCache
缓存中。方法执行后,Spring会根据@CacheEvict
注解从otherCache
缓存中移除缓存结果。
- @Service
- public class MyService {
-
- @Caching(
- cacheable = {
- @Cacheable(value = "myCache", key = "#id")
- },
- evict = {
- @CacheEvict(value = "otherCache", key = "#id")
- }
- )
- public MyEntity getEntityById(Long id) {
- return repository.findById(id).orElse(null);
- }
-
- }
- 复制代码
![](https://csdnimg.cn/release/blogv2/dist/pc/img/newCodeMoreWhite.png)
例子3:当调用saveData
方法时,Spring会根据@CacheEvict
注解先从otherCache
缓存中移除数据。然后,Spring 将执行该方法并将结果保存到数据库或外部 API。
方法执行后,Spring 会根据@CachePut
注解将结果添加到 myCache
、myOtherCache
和 myThirdCache
缓存中。Spring 还将根据@Cacheable
注解检查结果是否已缓存在 myFourthCache
和 myFifthCache
缓存中。如果结果尚未缓存,Spring 会将结果缓存在适当的缓存中。如果结果已经被缓存,Spring 将返回缓存的结果,而不是再次执行该方法。
- @Service
- public class MyService {
-
- @Caching(
- put = {
- @CachePut(value = "myCache", key = "#result.id"),
- @CachePut(value = "myOtherCache", key = "#result.id"),
- @CachePut(value = "myThirdCache", key = "#result.name")
- },
- evict = {
- @CacheEvict(value = "otherCache", key = "#id")
- },
- cacheable = {
- @Cacheable(value = "myFourthCache", key = "#id"),
- @Cacheable(value = "myFifthCache", key = "#result.id")
- }
- )
- public MyEntity saveData(Long id, String name) {
- // Code to save data to a database or external API
- MyEntity entity = new MyEntity(id, name);
- return entity;
- }
-
- }
- 复制代码
![](https://csdnimg.cn/release/blogv2/dist/pc/img/newCodeMoreWhite.png)
通过@CacheConfig
注解,我们可以将一些缓存配置简化到类级别的一个地方,这样我们就不必多次声明相关值:
- @CacheConfig(cacheNames={"myCache"})
- @Service
- public class MyService {
-
- @Autowired
- private MyRepository repository;
-
- @Cacheable(key = "#id")
- public MyEntity getEntityById(Long id) {
- return repository.findById(id).orElse(null);
- }
-
- @CachePut(key = "#entity.id")
- public void saveEntity(MyEntity entity) {
- repository.save(entity);
- }
-
- @CacheEvict(key = "#id")
- public void deleteEntityById(Long id) {
- repository.deleteById(id);
- }
- }
- 复制代码
![](https://csdnimg.cn/release/blogv2/dist/pc/img/newCodeMoreWhite.png)
condition
作用:指定缓存的条件(满足什么条件才缓存),可用 SpEL
表达式(如 #id>0
,表示当入参 id 大于 0 时才缓存)unless
作用 : 否定缓存,即满足 unless
指定的条件时,方法的结果不进行缓存,使用 unless
时可以在调用的方法获取到结果之后再进行判断(如 #result == null,表示如果结果为 null 时不缓存)- //when id >10, the @CachePut works.
- @CachePut(key = "#entity.id", condition="#entity.id > 10")
- public void saveEntity(MyEntity entity) {
- repository.save(entity);
- }
-
-
- //when result != null, the @CachePut works.
- @CachePut(key = "#id", condition="#result == null")
- public void saveEntity1(MyEntity entity) {
- repository.save(entity);
- }
- 复制代码
通过allEntries
、beforeInvocation
属性可以来清除全部缓存数据,不过allEntries
是方法调用后清理,beforeInvocation
是方法调用前清理。
- //方法调用完成之后,清理所有缓存
- @CacheEvict(value="myCache",allEntries=true)
- public void delectAll() {
- repository.deleteAll();
- }
-
- //方法调用之前,清除所有缓存
- @CacheEvict(value="myCache",beforeInvocation=true)
- public void delectAll() {
- repository.deleteAll();
- }
- 复制代码
Spring Cache注解中频繁用到SpEL表达式,那么具体如何使用呢?
SpEL 表达式的语法
Spring Cache可用的变量
通过Spring
缓存注解可以快速优雅地在我们项目中实现缓存的操作,但是在双写模式或者失效模式下,可能会出现缓存数据一致性问题(读取到脏数据),Spring Cache
暂时没办法解决。最后我们再总结下Spring Cache使用的一些最佳实践。
SpringBoot
支持多种缓存提供程序,包括 Ehcache
、Hazelcast
和 Redis
。Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。