Spring 3.1
引入了激动人心的基于注释(annotation
)的缓存(cache
)技术,它本质上不是一个具体的缓存实现方案(例如 EHCache
或者 Redis
),而是一个对缓存使用的抽象,通过在既有代码中添加少量它定义的各种 annotation
,即能够达到缓存方法的返回对象的效果。
具备相当的好的灵活性,不仅能够使用 SpEL(Spring Expression Language)
来定义缓存的 key 和各种 condition,还提供开箱即用的缓存临时存储方案,也支持和主流的专业缓存例如 EHCache、Redis、Guava 的集成。
annotation
即可使得现有代码支持缓存Out-Of-The-Box
,不用安装和部署额外第三方组件即可使用缓存Spring Express Language
,能使用对象的任何属性或者方法来定义缓存的 key
和 condition
AspectJ
,并通过其实现任何方法的缓存支持key
和自定义缓存管理者,具有相当的灵活性和扩展性下面针对Spring Cache
使用前后给出了伪代码部分,具体中也许比这要更加复杂,但是Spring Cache
都可以很好的应对
使用前
我们需要硬编码,如果切换Cache Client
还需要修改代码,耦合度高,不易于维护
public String get(String key) {
String value = userMapper.selectById(key);
if (value != null) {
cache.put(key,value);
}
return value;
}
使用后
基于Spring Cache
注解,缓存由开发者自己配置,但不用参与到具体编码
@Cacheable(value = "user", key = "#key")
public String get(String key) {
return userMapper.selectById(key);
}
在 pom.xml
中添加 spring-boot-starter-data-redis
的依赖
org.springframework.boot
spring-boot-starter-data-redis
org.apache.commons
commons-pool2
org.springframework.boot
spring-boot-starter-test
test
在 application.properties
文件中配置如下内容,由于Spring Boot2.x
的改动,连接池相关配置需要通过spring.redis.lettuce.pool
或者 spring.redis.jedis.pool
进行配置了。使用了Spring Cache
后,能指定spring.cache.type
就手动指定一下,虽然它会自动去适配已有Cache
的依赖,但先后顺序会对Redis
使用有影响(JCache -> EhCache -> Redis -> Guava
)
spring.redis.host=localhost
spring.redis.password=battcn
# 一般来说是不用配置的,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
创建一个User
类,目的是为了模拟对象存储
package com.battcn.entity;
import java.io.Serializable;
/**
* @author Levin
* @since 2018/5/11 0007
*/
public class User implements Serializable {
private static final long serialVersionUID = 8655851615465363473L;
private Long id;
private String username;
private String password;
// TODO 省略get set
}
package com.battcn.service;
import com.battcn.entity.User;
/**
* @author Levin
* @since 2018/5/11 0011
*/
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);
}
###实现类
为了方便演示数据库操作,直接定义了一个Map
,这里的核心就是@Cacheable
、@CachePut
、@CacheEvict
三个注解
package com.battcn.service.impl;
import com.battcn.entity.User;
import com.battcn.service.UserService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.cache.annotation.CachePut;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.stereotype.Service;
import java.util.HashMap;
import java.util.Map;
/**
* @author Levin
* @since 2018/5/11 0011
*/
@Service
public class UserServiceImpl implements UserService {
private static final Map 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 方法");
}
}
@EnableCaching
必须要加,否则spring-data-cache
相关注解不会生效...
package com.battcn;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cache.annotation.EnableCaching;
/**
* @author Levin
*/
@SpringBootApplication
@EnableCaching
public class Chapter9Application {
public static void main(String[] args) {
SpringApplication.run(Chapter9Application.class, args);
}
}
完成准备事项后,编写一个junit
测试类来检验代码的正确性,有很多人质疑过Redis
线程安全性,故下面也提供了响应的测试案例,如有疑问欢迎指正
package com.battcn;
import com.battcn.entity.User;
import com.battcn.service.UserService;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;
/**
* @author Levin
* @since 2018/5/10 0010
*/
@RunWith(SpringRunner.class)
@SpringBootTest
public class Chapter9ApplicationTest {
private static final Logger log = LoggerFactory.getLogger(Chapter9ApplicationTest.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
中的缓存数据。其中 # 号代表这是一个 SpEL 表达式
,此表达式可以遍历方法的参数对象,具体语法可以参考 Spring 的相关文档手册。
2018-05-14 09:20:55.303 INFO 21176 --- [ main] com.battcn.service.impl.UserServiceImpl : 进入 saveOrUpdate 方法
2018-05-14 09:20:55.582 INFO 21176 --- [ main] io.lettuce.core.EpollProvider : Starting without optional epoll library
2018-05-14 09:20:55.584 INFO 21176 --- [ main] io.lettuce.core.KqueueProvider : Starting without optional kqueue library
2018-05-14 09:20:56.316 INFO 21176 --- [ main] com.battcn.Chapter9ApplicationTest : [saveOrUpdate] - [User{id=5, username='u5', password='p5'}]
2018-05-14 09:20:56.320 INFO 21176 --- [ main] com.battcn.Chapter9ApplicationTest : [get] - [User{id=5, username='u5', password='p5'}]
2018-05-14 09:20:56.322 INFO 21176 --- [ main] com.battcn.service.impl.UserServiceImpl : 进入 delete 方法
其它类型
下列的就是Redis
其它类型所对应的操作方式
根据条件操作缓存内容并不影响数据库操作,条件表达式返回一个布尔值,
true/false,当条件为
true,则进行缓存操作,否则直接调用方法执行的返回结果。
长度
: @CachePut(value = "user", key = "#user.id",condition = "#user.username.length() < 10")
只缓存用户名长度少于10的数据大小
: @Cacheable(value = "user", key = "#id",condition = "#id < 10")
只缓存ID小于10的数据组合
: @Cacheable(value="user",key="#user.username.concat(##user.password)")
提前操作:
@CacheEvict(value="user",allEntries=true,beforeInvocation=true)
加上beforeInvocation=true
后,不管内部是否报错,缓存都将被清除,默认情况为false
@Cacheable(根据方法的请求参数对其结果进行缓存)
@Cacheable(value="user",key="#userName")
)@Cacheable(value="user")
或者@Cacheable(value={"user1","use2"})
)@Cacheable(value = "user", key = "#id",condition = "#id < 10")
)@CachePut(根据方法的请求参数对其结果进行缓存,和
@Cacheable
不同的是,它每次都会触发真实方法的调用)
@CachEvict(根据条件对缓存进行清空)
@CacheEvict(value = "user", key = "#id", allEntries = true)
)@CacheEvict(value = "user", key = "#id", beforeInvocation = true)
)spring-cache
文档: https://docs.spring.io/spring/docs/5.0.5.RELEASE/spring-framework-reference/integration.html#cache-introductionspring-data-redis
文档: https://docs.spring.io/spring-data/redis/docs/2.0.1.RELEASE/reference/html/#new-in-2.0.0Redis
文档: https://redis.io/documentationRedis
中文文档: http://www.redis.cn/commands.html
目前很多大佬都写过关于 SpringBoot
的教程了,如有雷同,请多多包涵,本教程基于最新的 spring-boot-starter-parent:2.0.1.RELEASE
编写,包括新版本的特性都会一起介绍...