文章目录
一、缓存简介
Spring 3.1 引入了激动人心的基于注释(annotation)的缓存(cache)技术,它本质上不是一个具体的缓存实现方案(例如 EHCache 或者 Redis),而是一个对缓存使用的抽象,通过在既有代码中添加少量它定义的各种 annotation,即能够达到缓存方法的返回对象的效果。
目前 Spring Boot 支持的缓存有如下几种:
- JCache (JSR-107)
- EhCache 2.x
- Hazelcast
- lnfinispan
- Couchbase
- Redis
- Caffeine
- Simple
目前常用的缓存实现 Ehcache 2.x和 Redis。由于 Spring 早己将缓存领域统一 ,因此无论使用哪种缓存实现,不同的只是缓存配置,开发者使用的缓存注解是一致的( Spring 缓存注解和各缓存实现的关系就像 JDBC和各种数据库驱动的关系)。
这里学习使用Redis作为缓存实现。
二、Redis缓存
1、添加依赖
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency> <dependency> <groupId>org.apache.commons</groupId> <artifactId>commons-pool2</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency>
2、属性配置
在application.properties中添加配置:
spring.redis.host=localhost spring.redis.password= # 一般来说是不用配置的,Spring Cache 会根据依赖的包自行装配 spring.cache.type=redis # 连接超时时间(毫秒) spring.redis.timeout=10000 # Redis默认情况下有16个分片,这里配置具体使用的分片 spring.redis.database=0 # 连接池最大连接数(使用负值表示没有限制) 默认 8 spring.redis.lettuce.pool.max-active=8 # 连接池最大阻塞等待时间(使用负值表示没有限制) 默认 -1 spring.redis.lettuce.pool.max-wait=-1 # 连接池中的最大空闲连接 默认 8 spring.redis.lettuce.pool.max-idle=8 # 连接池中的最小空闲连接 默认 0 spring.redis.lettuce.pool.min-idle=0
3、 Cache核心注解
spring-boot-starter-cache 是 Spring Boot 体系内提供使⽤ Spring Cache的Starter 包。其中最核⼼的三个注解:@Cacheable、@CacheEvict、@CachePut。
3.1、@Cacheable
@Cacheable⽤来声明⽅法是可缓存的,将结果存储到缓存中以便后续使⽤相同参数调⽤时不需执⾏实际的⽅法,直接从缓存中取值。@Cacheable 可以标记在⼀个⽅法上,也可以标记在⼀个类上。当标记在⼀个⽅法上时表示该⽅法是⽀持缓存的,当标记在⼀个类上时则表示该类所有的⽅法都是⽀持缓存的。
例如:
@RequestMapping("/hello") @Cacheable(value="helloCache") public String hello(String name) { System.out.println("没有⾛缓存!"); return "hello "+name; }
@Cacheable ⽀持如下⼏个参数。
- value:缓存的名称。
- key:缓存的 key,可以为空,如果指定要按照 SpEL 表达式编写;如果不指定,则缺省按照⽅法的所有参数进⾏组合。
- condition:触发条件,只有满⾜条件的情况才会加⼊缓存,默认为空,既表示全部都加⼊缓存,⽀持SpEL。
把上⾯的⽅法稍微更改:
@RequestMapping("/condition") @Cacheable(value="condition",condition="#name.length() <= 4") public String condition(String name) { System.out.println("没有⾛缓存!"); return "hello "+name; }
3.2、@CachePut
项⽬运⾏中会对数据库的信息进⾏更新,如果仍然使⽤ @Cacheable 就会导致数据库的信息和缓存的信息不⼀致。在以往的项⽬中,⼀般更新完数据库后,再⼿动删除掉 Redis 中对应的缓存,以保证数据的⼀致性。Spring 提供了另外的⼀种解决⽅案,可以以优雅的⽅式去更新缓存。
与 @Cacheable 不同的是使⽤ @CachePut 标注的⽅法在执⾏前,不会去检查缓存中是否存在之前执⾏过的结果,⽽是每次都会执⾏该⽅法,并将执⾏结果以键值对的形式存⼊指定的缓存中。
@CachePut 配置⽅法:
- value 缓存的名称。
- key 缓存的 key,可以为空,如果指定要按照 SpEL 表达式编写,如果不指定,则缺省按照⽅法的所有参数进⾏组合。
- condition 缓存的条件,可以为空,使⽤ SpEL 编写,返回 true 或者 false,只有为 true 才进⾏缓存。
3.3、@CacheEvict
@CacheEvict 是⽤来标注在需要清除缓存元素的⽅法或类上的,当标记在⼀个类上时表示其中所有的⽅法的执⾏都会触发缓存的清除操作。@CacheEvict 可以指定的属性有 value、key、condition、allEntries 和beforeInvocation,其中 value、key 和 condition 的语义与 @Cacheable 对应的属性类似。即 value 表示清除操作是发⽣在哪些 Cache 上的(对应 Cache 的名称);key 表示需要清除的是哪个 key,如未指定则会使⽤默认策略⽣成的 key;condition 表示清除操作发⽣的条件。
3.4、总结
- @Cacheable 作⽤和配置⽅法
主要针对⽅法配置,能够根据⽅法的请求参数对其结果进⾏缓存:
主要参数 | 解 释 | 举例 |
value | 缓存的名称,在 spring 配置⽂件中定义,必须指定⾄少⼀个 | 如 @Cacheable(value=“mycache”)或者 @Cacheable(value={“cache1”,“cache2”} |
key | 缓存的 key,可以为空,如果指定要按照 SpEL表达式编写,如果不指定,则缺省按照⽅法的所有参数进⾏组合 | 如@Cacheable(value=“testcache”,key="#userName") |
condition | 缓存的条件,可以为空,使⽤ SpEL 编写,返回true 或者 false,只有为 true 才进⾏缓存 | 如@Cacheable(value=“testcache”,condition="#userName.length()>2") |
- @CachePut 作⽤和配置⽅法
@CachePut 的作⽤是主要针对⽅法配置,能够根据⽅法的请求参数对其结果进⾏缓存,和 @Cacheable 不同的是,它每次都会触发真实⽅法的调⽤。
主要参数 | 解释 | 举例 |
value | 缓存的名称,在 spring 配置⽂件中定义,必须指定⾄少⼀个 | 如@Cacheable(value=“mycache”)或者 @Cacheable(value={“cache1”,“cache2”} |
key | 缓存的 key,可以为空,如果指定要按照 SpEL表达式编写,如果不指定,则缺省按照⽅法的所有参数进⾏组合 | 如@Cacheable(value=“testcache”,key="#userName") |
condition | 缓存的条件,可以为空,使⽤ SpEL 编写,返回true 或者 false,只有为 true 才进⾏缓存 | 如@Cacheable(value=“testcache”,condition="#userName.length()>2") |
- @CacheEvict 作⽤和配置⽅法
主要针对⽅法配置,能够根据⼀定的条件对缓存进⾏清空。
主要参数 | 解释 | 举例 |
value | 缓存的名称,在 spring 配置⽂件中定义,必须指定⾄少⼀个 | 如 @CachEvict(value=“mycache”)或者 @CachEvict(value={“cache1”,“cache2”} |
key | 缓存的 key,可以为空,如果指定要按照SpEL 表达式编写,如果不指定,则缺省按照⽅法的所有参数进⾏组合 | 如@CachEvict(value=“testcache”,key="#userName") |
condition | 缓存的条件,可以为空,使⽤ SpEL 编写,返回 true 或者 false,只有为 true 才清空缓存 | 如@CachEvict(value=“testcache”,condition="#userName.length()>2") |
allEntries | 是否清空所有缓存内容,缺省为 false,如果指定为 true,则⽅法调⽤后将⽴即清空所有缓存 | 如@CachEvict(value=“testcache”,allEntries=true) |
beforeInvocation | 是否在⽅法执⾏前就清空,缺省为 false,如果指定为 true,则在⽅法还没有执⾏的时候就清空缓存,缺省情况下,如果⽅法执⾏抛出异常,则不会清空缓存 | 如@CachEvict(value=“testcache”,beforeInvocation=true) |
4、Cache具体使用
4.1、实体类
定义一个User类,模拟对象存储:
public class User implements Serializable { private static final long serialVersionUID = 8655851615465363473L; private Long id; private String username; private String password; //省略getter、setter }
4.2、服务层
- 接口:
public interface UserService { /** * 删除 * * @param user 用户对象 * @return 操作结果 */ User saveOrUpdate(User user); /** * 添加 * * @param id key值 * @return 返回结果 */ User get(Long id); /** * 删除 * * @param id key值 */ void delete(Long id); }
- 实现类:
实现类里用到了最核心的三个注解:@Cacheable、@CachePut、@CacheEvict
@Service public class UserServiceImpl implements UserService { private static final Map<Long, User> DATABASES = new HashMap<>(); static { DATABASES.put(1L, new User(1L, "u1", "p1")); DATABASES.put(2L, new User(2L, "u2", "p2")); DATABASES.put(3L, new User(3L, "u3", "p3")); } private static final Logger log = LoggerFactory.getLogger(UserServiceImpl.class); @Cacheable(value = "user", key = "#id") @Override public User get(Long id) { // TODO 假设它是从数据库读取出来的 log.info("进入 get 方法"); return DATABASES.get(id); } @CachePut(value = "user", key = "#user.id") @Override public User saveOrUpdate(User user) { DATABASES.put(user.getId(), user); log.info("进入 saveOrUpdate 方法"); return user; } @CacheEvict(value = "user", key = "#id") @Override public void delete(Long id) { DATABASES.remove(id); log.info("进入 delete 方法"); } }
4.4、启动类
启动类需要开启缓存配置@EnableCaching
@SpringBootApplication @EnableCaching public class SpringbootCacheRedisApplication { public static void main(String[] args) { SpringApplication.run(SpringbootCacheRedisApplication.class, args); } }
4.5、测试
@RunWith(SpringRunner.class) @SpringBootTest public class CacheRedisTest { private static final Logger log = LoggerFactory.getLogger(CacheRedisTest.class); @Autowired private UserService userService; @Test public void get() { final User user = userService.saveOrUpdate(new User(5L, "u5", "p5")); log.info("[saveOrUpdate] - [{}]", user); final User user1 = userService.get(5L); log.info("[get] - [{}]", user1); userService.delete(5L); } }
运行结果:
可以看到增删改查中,查询是没有日志输出的,因为它直接从缓存中获取的数据,而添加、修改、删除都是会进入方法内执行具体的业务代码,然后通过切面去删除掉Redis中的缓存数据。