本文正在参与「金石计划」

前语

缓存能够经过将常常拜访的数据存储在内存中,削减底层数据源如数据库的压力,然后有效进步系统的功能和稳定性。我想大家的项目中或多或少都有运用过,咱们项目也不例外,可是最近在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 供给的不同缓存注解完结缓存的最佳实践。

启用缓存@EnableCaching

现在大部分项目都是是SpringBoot项目,咱们能够在启动类增加注解@EnableCaching来开启缓存功能。

@SpringBootApplication
@EnableCaching
public class SpringCacheApp {
    public static void main(String[] args) {
        SpringApplication.run(Cache.class, args);
    }
}

既然要能运用缓存,就需要有一个缓存管理器Bean,默许情况下,@EnableCaching 将注册一个ConcurrentMapCacheManager的Bean,不需要独自的 bean 声明。ConcurrentMapCacheManager将值存储在ConcurrentHashMap的实例中,这是缓存机制的最简单的线程安全完结。

自定义缓存管理器

默许的缓存管理器并不能满意需求,由于她是存储在jvm内存中的,那么怎么存储到redis中呢?这时候需要增加自定义的缓存管理器。

  1. 增加依赖
<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
  1. 配置Redis缓存管理器
@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;
    }
}

现在有了缓存管理器以后,咱们怎么在事务层面操作缓存呢?

咱们能够运用@Cacheable@CachePut@CacheEvict 注解来操作缓存了。

@Cacheable

该注解能够将办法运转的成果进行缓存,在缓存时效内再次调用该办法时不会调用办法自身,而是直接从缓存获取成果并回来给调用方。

SpringBoot项目中使用缓存Cache的正确姿势!!!

比如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

然后@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 注解的办法在被调用时,会从缓存中移除已存储的数据。@CacheEvict 注解一般用于删去缓存数据,相当于缓存运用的是写形式中的失效形式。

SpringBoot项目中使用缓存Cache的正确姿势!!!

@Service
public class MyService {
    @Autowired
    private MyRepository repository;
     @CacheEvict(value = "myCache", key = "#id")
    public void deleteEntityById(Long id) {
        repository.deleteById(id);
    }
}

@Caching

@Caching 注解用于在一个办法或者类上,同时指定多个 Spring Cache 相关的注解。

SpringBoot项目中使用缓存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);
    }
}

比如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);
    }
}

比如3:当调用saveData办法时,Spring会依据@CacheEvict注解先从otherCache缓存中移除数据。然后,Spring 将履行该办法并将成果保存到数据库或外部 API。

办法履行后,Spring 会依据@CachePut注解将成果增加到 myCachemyOtherCachemyThirdCache 缓存中。Spring 还将依据@Cacheable注解查看成果是否已缓存在 myFourthCachemyFifthCache 缓存中。假如成果尚未缓存,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;
    }
}

@CacheConfig

经过@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);
    }
}

Condition & Unless

  • 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);
}

整理悉数缓存

经过allEntriesbeforeInvocation属性能够来铲除悉数缓存数据,不过allEntries是办法调用后整理,beforeInvocation是办法调用前整理。

//办法调用完结之后,整理所有缓存
@CacheEvict(value="myCache",allEntries=true)
public void delectAll() {
    repository.deleteAll();
}
//办法调用之前,铲除所有缓存
@CacheEvict(value="myCache",beforeInvocation=true)
public void delectAll() {
    repository.deleteAll();
}

SpEL表达式

Spring Cache注解中频频用到SpEL表达式,那么详细怎么运用呢?

SpEL 表达式的语法

SpringBoot项目中使用缓存Cache的正确姿势!!!

Spring Cache可用的变量

SpringBoot项目中使用缓存Cache的正确姿势!!!

最佳实践

经过Spring缓存注解能够快速优雅地在咱们项目中完结缓存的操作,可是在双写形式或者失效形式下,或许会呈现缓存数据共同性问题(读取到脏数据),Spring Cache 暂时没办法处理。最终咱们再总结下Spring Cache运用的一些最佳实践。

  • 只缓存常常读取的数据:缓存能够显着进步功能,但只缓存常常拜访的数据很重要。很少或从不拜访的缓存数据会占用名贵的内存资源,然后导致功能问题。
  • 依据应用程序的特定需求选择合适的缓存供给程序和战略。SpringBoot 支持多种缓存供给程序,包含 EhcacheHazelcastRedis
  • 运用缓存时请注意潜在的线程安全问题。对缓存的并发拜访或许会导致数据不共同或不正确,因此选择线程安全的缓存供给程序并在必要时运用恰当的同步机制非常重要。
  • 防止过度缓存。缓存对于进步功能很有用,但过多的缓存实践上会耗费名贵的内存资源,然后危害功能。在缓存频频运用的数据和答应垃圾搜集不常用的数据之间取得平衡很重要。
  • 运用恰当的缓存逐出战略。运用缓存时,重要的是定义恰当的缓存逐出战略以确保在必要时从缓存中删去旧的或陈腐的数据。
  • 运用恰当的缓存键规划。缓存键对于每个数据项都应该是仅有的,而且应该考虑或许影响缓存数据的任何相关参数,例如用户 ID、时间或位置。
  • 常规数据(读多写少、即时性与共同性要求不高的数据)完全能够运用 Spring Cache,至于写形式下缓存数据共同性问题的处理,只要缓存数据有设置过期时间就足够了。
  • 特别数据(读多写多、即时性与共同性要求非常高的数据),不能运用 Spring Cache,主张考虑特别的规划(例如运用 Cancal 中间件等)。

欢迎重视个人公众号【JAVA旭阳】交流学习!