一、Redis
Redis是一个开源的使用ANSI C语言编写、支持网络、可基于内存亦可持久化的日志型、Key-Value的非关系型数据库,并提供多种语言的API。
由于Redis实现使用C语言编写,且采用了I/O多路复用,在响应速度与并发支持上都要优于关系型数据库,所以常被用来配合关系型数据库做高速缓存、共享Session、分布式锁、消息系统等。
Redis的重要性不言而喻,无论是其对于系统,还是对于自身技能而言,我们都应该好好掌握它,不过这里并不是Redis的教学,仅仅介绍Spring Boot如何集成·Redis。
二、redisTemplate方式使用Redis
要使用Redis,首先得引入Redis的依赖,Redis如此受欢迎,自然Spring Boot为它定制了专属的starter。
<!--redis依赖-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
然后在配置文件中,我们需要在spring.redis节点下配置redis的ip、端口、密码、超时时间等等配置。
spring:
redis:
host: localhost
port: 6379
password: 1234
timeout: 60000
在SpringBoot中,我们只要配置好对应的属性,就可以使用Redis了,且SpringBoot自动在容器中生成了一个RedisTemplate和一个StringRedisTemplate的Bean对象。但是,这个RedisTemplate的泛型是<Object,Object>,使用时需要频繁强转,编写代码并不是很方便,且这个RedisTemplate,key及value的序列化方式采用了JDK的序列化,会导致在库中的序列化数据看起来乱码。
一般业务中,我们常使用泛型为<String,Object>形式的RedisTemplate,因此我们自己来注入一个RedisTemplate<String,Object>的Bean。
@Configuration
@EnableCaching
public class CacheConfig {
@Bean
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory connectionFactory) {
RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
redisTemplate.setConnectionFactory(connectionFactory);
// 使用Jackson2JsonRedisSerializer来序列化和反序列化redis的value值(默认使用JDK的序列化方式)
Jackson2JsonRedisSerializer serializer = new Jackson2JsonRedisSerializer(Object.class);
ObjectMapper mapper = new ObjectMapper();
mapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
mapper.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
serializer.setObjectMapper(mapper);
redisTemplate.setValueSerializer(serializer);
// 使用StringRedisSerializer来序列化和反序列化redis的key值
redisTemplate.setKeySerializer(new StringRedisSerializer());
redisTemplate.afterPropertiesSet();
return redisTemplate;
}
}
@Configuration表示这是一个配置类,@EnableCaching用于开启缓存。当配置类@Configuration和@EnableCaching一起注解时,会触发一个post processor,这会扫描每一个spring bean,查看是否已经存在注解对应的缓存。如果找到了,就会自动创建一个代理拦截方法调用,使用缓存的bean执行处理。
@Bean表示注入一个Bean对象,其中使用Jackson2JsonRedisSerializer来序列化和反序列化Redis的value值,StringRedisSerializer来序列化和反序列化redis的key值。需要一提的事,当多个系统使用同一个(实际通常是同一个Redis集群)时,需要保证每个系统的序列化和反序列化方式一致。
接着我们创建一个CacheDao,提供了set与get方法用于添加和获取缓存,缓存时间这里设置了一分钟。
@Repository
public class CacheDao {
@Resource
private RedisTemplate<String, Object> template;
public void set(String key, Object value){
System.out.println("添加缓存key->" + key);
ValueOperations<String, Object> valueOperations = template.opsForValue();
// 1分钟过期
valueOperations.set(key, value, 1, TimeUnit.MINUTES);
}
public Object get(String key){
System.out.println("获取缓存key->" + key);
ValueOperations<String, Object> valueOperations = template.opsForValue();
return valueOperations.get(key);
}
}
那么缓存是否起到了作用呢,我们添加测试类UserApi,这是一个Controller,提供添加用户和获取用户的方法。
@RestController
@RequestMapping("user")
public class UserApi {
@Autowired
private UserService userService;
/**
* 添加用户
* @param user
*/
@PostMapping("addUser")
public String addUser(User user){
int id = userService.addUser(user);
return "添加用户成功,主键id:" + id;
}
/**
* 获取用户信息
* @param id
* @return
*/
@GetMapping("getUser")
public User getUser(@RequestParam(value = "id") int id){
return userService.getUser(id);
}
}
接下来是UserService,与之相对应的保存用户和获取用户的方法。IUserDao使用了mybatis,从数据库中获取,这里就不贴具体的代码了,而CacheDao则是上面展示过的,从缓存中读取。在添加用户的时候,我们将用户同时添加到缓存,或取用户的时候,我们将先从缓存中获取,如果缓存中不存在,则去数据库中查找,并同时进行缓存。
@Service
public class UserService {
@Autowired
private IUserDao userDao;
@Autowired
private CacheDao cacheDao;
/**
* 添加用户
* @param user
*/
public int addUser(User user){
userDao.add(user);
// 添加缓存
cacheDao.set("user:id:" + user.getId(), user);
return user.getId();
}
/**
* 获取用户信息
* @param id
* @return
*/
public User getUser(int id){
// 从缓存中获取
User user = (User)cacheDao.get("user:id:" + id);
if(user == null){
user = userDao.find(id);
cacheDao.set("user:id:" + user.getId(), user);
}
return user;
}
}
启动项目,下面我们通过swagger-ui进行测试(本项目已经集成了swagger2),我们添加张三、password的数据。
调用结果显示成功,且id为1。
然后我们去获取这条数据,使用/user/getUser接口,可以成功获取。
然后我们查看后台打印的信息,先是调用了缓存,然后又进行了查询数据库,诶……这好像不对啊,我刚刚添加时不是进行缓存了吗,为什么没有生效?
这是因为我在获取的时候已经超过了缓存生效时间1分钟,这时再次查询,就不会去数据库中查询了。
三、注解方式使用Redis
在Spring Boot项目中,注解满天飞,其实也可以使用注解形式进行缓存,该机制依赖于Spring CaChe,但是这种方式一般较少,因为其缓存机制依赖方法的入参和返回结果,缺乏一定的灵活性。
缓存的注解主要有@CachePut、@Cacheable、@CacheEvict,都作用于方法上。
- @CachePut,添加缓存,key可以从入参中获取,value为方法返回值,注意并不是注解的value属性,value属性是用于表示缓存的空间,例如@CachePut(value=“user”, key="‘anno:user?’ + #user.id"),当然这必须要求方法有返回值,否则缓存的数据为空。
- @Cacheable,既是获取,也是存储,存在缓存则从缓存中获取,不存在则进行缓存,缓存值为方法返回值
- @CacheEvict表示清除缓存。
在UserService中,我们新增updateUser更新用户,使用了@CacheEvict注解,getUserAnno获取用户,使用了@Cacheable。
/**
* 修改用户信息
* @param user
*/
// @CachePut,每次都会进行缓存添加,缓存值为方法返回值,该方法无返回值无法使用
// @CachePut(value="user", key="'anno:user:id:' + #user.id")
// 直接删除缓存,下次获取重新查库
@CacheEvict(value="user", key="'anno:user:id:' + #user.id")
public void updateUser(User user){
userDao.update(user);
}
/**
* 获取用户信息,注解缓存
* @param id
* @return
*/
// @Cacheable存在缓存则从缓存中获取,不存在则进行缓存,缓存值为方法返回值
@Cacheable(value="user", key="'anno:user:id:' + #id")
public User getUserAnno(int id){
return userDao.find(id);
}
多次调用getUserAnno方法,可以发现,仅第一遍进行了数据库访问,后面都没有调用数据库查询数据。我们接着再调用更新方法,然后在调用getUserAnno又从数据库中查取了。
源码地址:https://github.com/imyanger/springboot-project/tree/master/p10-springboot-redis