本文正在参与「金石计划」
前语
缓存能够经过将常常拜访的数据存储在内存中,削减底层数据源如数据库的压力,然后有效进步系统的功能和稳定性。我想大家的项目中或多或少都有运用过,咱们项目也不例外,可是最近在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 声明。ConcurrentMapCacheManage
r将值存储在ConcurrentHashMap
的实例中,这是缓存机制的最简单的线程安全完结。
自定义缓存管理器
默许的缓存管理器并不能满意需求,由于她是存储在jvm内存中的,那么怎么存储到redis中呢?这时候需要增加自定义的缓存管理器。
- 增加依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
- 配置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
该注解能够将办法运转的成果进行缓存,在缓存时效内再次调用该办法时不会调用办法自身,而是直接从缓存获取成果并回来给调用方。
比如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
注解一般用于删去缓存数据,相当于缓存运用的是写形式中的失效形式。
@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 相关的注解。
比如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
注解将成果增加到 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;
}
}
@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);
}
整理悉数缓存
经过allEntries
、beforeInvocation
属性能够来铲除悉数缓存数据,不过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 表达式的语法
Spring Cache可用的变量
最佳实践
经过Spring
缓存注解能够快速优雅地在咱们项目中完结缓存的操作,可是在双写形式或者失效形式下,或许会呈现缓存数据共同性问题(读取到脏数据),Spring Cache
暂时没办法处理。最终咱们再总结下Spring Cache运用的一些最佳实践。
- 只缓存常常读取的数据:缓存能够显着进步功能,但只缓存常常拜访的数据很重要。很少或从不拜访的缓存数据会占用名贵的内存资源,然后导致功能问题。
- 依据应用程序的特定需求选择合适的缓存供给程序和战略。
SpringBoot
支持多种缓存供给程序,包含Ehcache
、Hazelcast
和Redis
。 - 运用缓存时请注意潜在的线程安全问题。对缓存的并发拜访或许会导致数据不共同或不正确,因此选择线程安全的缓存供给程序并在必要时运用恰当的同步机制非常重要。
- 防止过度缓存。缓存对于进步功能很有用,但过多的缓存实践上会耗费名贵的内存资源,然后危害功能。在缓存频频运用的数据和答应垃圾搜集不常用的数据之间取得平衡很重要。
- 运用恰当的缓存逐出战略。运用缓存时,重要的是定义恰当的缓存逐出战略以确保在必要时从缓存中删去旧的或陈腐的数据。
- 运用恰当的缓存键规划。缓存键对于每个数据项都应该是仅有的,而且应该考虑或许影响缓存数据的任何相关参数,例如用户 ID、时间或位置。
- 常规数据(读多写少、即时性与共同性要求不高的数据)完全能够运用 Spring Cache,至于写形式下缓存数据共同性问题的处理,只要缓存数据有设置过期时间就足够了。
- 特别数据(读多写多、即时性与共同性要求非常高的数据),不能运用 Spring Cache,主张考虑特别的规划(例如运用 Cancal 中间件等)。
欢迎重视个人公众号【JAVA旭阳】交流学习!