1. 前言
一个系统在于数据库交互的过程中,内存的速度远远快于硬盘速度,当我们重复地获取相同数据时,我们一次又一次地请求数据库或远程服务,者无疑时性能上地浪费(这会导致大量时间被浪费在数据库查询或者远程方法调用上致使程序性能恶化),于是有了“缓存”。 本文将介绍在spring boot项目开发中怎样使用spring提供的Spring Cache 与最近很火的 Redis 数据库来实现数据的缓存。
- Git源码
2. SpringCache简介
Spring Cache
是Spring
框架提供的对缓存使用的抽象类,支持多种缓存,比如Redis
、EHCache
等,集成很方便。同时提供了多种注解来简化缓存的使用,可对方法进行缓存。
2.1 关于SpringCache 注解的简单介绍
- @Cacheable:标记在一个方法上,也可以标记在一个类上。主要是缓存标注对象的返回结果,标注在方法上缓存该方法的返回值,标注在类上,缓存该类所有的方法返回值。 参数: value缓存名、 key缓存键值、 condition满足缓存条件、unless否决缓存条件
- @CacheEvict:从缓存中移除相应数据。
- @CachePut:方法支持缓存功能。与@Cacheable不同的是使用@CachePut标注的方法在执行前不会去检查缓存中是否存在之前执行过的结果,而是每次都会执行该方法,并将执行结果以键值对的形式存入指定的缓存中。
- @Caching:多个Cache注解使用,比如新增用户时,删除用户属性等需要删除或者更新多个缓存时,集合以上三个注解。
2.2 SpEL上下文数据
Spring Cache提供了一些供我们使用的SpEL上下文数据,下表直接摘自Spring官方文档:
名字 | 位置 | 描述 | 示例 |
---|---|---|---|
methodName | root对象 | 当前被调用的方法名 | #root.methodName |
method | root对象 | 当前被调用的方法 | #root.method.name |
target | root对象 | 当前被调用的目标对象 | #root.target |
targetClass | root对象 | 当前被调用的目标对象类 | #root.targetClass |
args | root对象 | 当前被调用的方法的参数列表 | #root.args[0] |
caches | root对象 | 当前方法调用使用的缓存列表(如@Cacheable(value={"cache1", "cache2"})),则有两个cache | #root.caches[0].name |
argument name | 执行上下文 | 当前被调用的方法的参数,如findById(Long id),我们可以通过#id拿到参数 | #user.id |
result | 执行上下文 | 方法执行后的返回值(仅当方法执行之后的判断有效,如‘unless’,'cache evict'的beforeInvocation=false) | #result |
其他关于 Cache 详细配置或注解,请参考文章基于Redis的Spring cache 缓存介绍或spring官方文档
3. Redis简介
Redis 是完全开源免费的,遵守BSD协议,是一个高性能的key-value数据库。
Redis 与其他 key - value 缓存产品有以下三个特点:
- Redis支持数据的持久化,可以将内存中的数据保存在磁盘中,重启的时候可以再次加载进行使用。
- Redis不仅仅支持简单的key-value类型的数据,同时还提供list,set,zset,hash等数据结构的存储。
- Redis支持数据的备份,即master-slave模式的数据备份。
Redis 的安装和使用请自行Google 。
4. 实践--SpringCache和Redis集成
4.1 步骤
我们要把一个查询函数加入缓存功能,大致需要三步。
- 一、在函数执行前,我们需要先检查缓存中是否存在数据,如果存在则返回缓存数据。
- 二、如果不存在,就需要在数据库的数据查询出来。
- 三、最后把数据存放在缓存中,当下次调用此函数时,就可以直接使用缓存数据,减轻了数据库压力。
本实例没有存入MySQL数据库,主要是为了方便实践,实际使用中大家可以把service层中的方法改为数据库操作代码即可。
4.2 具体操作
添加依赖
org.springframework.boot
spring-boot-starter-data-redis
复制代码
注: 其实我们从官方文档可以看到spring-boot-starter-data-redis 已经包含了jedis客户端,我们在使用jedis连接池的时候不必再添加jedis依赖。
- 官方文档Spring Data Redis
配置SpringCache,Redis连接等信息
- SpringCache配置--RedisConfig.java
import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.PropertyAccessor;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.cache.RedisCacheManager;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;
/**
* @Author: MaoLin
* @Date: 2019/3/26 17:04
* @Version 1.0
*/
@Configuration
@EnableCaching
public class RedisConfig {
/**
* 申明缓存管理器,会创建一个切面(aspect)并触发Spring缓存注解的切点(pointcut)
* 根据类或者方法所使用的注解以及缓存的状态,这个切面会从缓存中获取数据,将数据添加到缓存之中或者从缓存中移除某个值
* @return
*/
@Bean
public RedisCacheManager cacheManager(RedisConnectionFactory redisConnectionFactory) {
return RedisCacheManager.create(redisConnectionFactory);
}
@Bean
public RedisTemplate redisTemplate(RedisConnectionFactory factory) {
// 创建一个模板类
RedisTemplate template = new RedisTemplate();
// 将刚才的redis连接工厂设置到模板类中
template.setConnectionFactory(factory);
// 设置key的序列化器
template.setKeySerializer(new StringRedisSerializer());
// 设置value的序列化器
//使用Jackson 2,将对象序列化为JSON
Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);
//json转对象类,不设置默认的会将json转成hashmap
ObjectMapper om = new ObjectMapper();
om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
jackson2JsonRedisSerializer.setObjectMapper(om);
template.setValueSerializer(jackson2JsonRedisSerializer);
return template;
}
}
复制代码
- redis配置--application.yml
server:
port: 8080
spring:
# redis相关配置
redis:
database: 0
host: localhost
port: 6379
password: 123456
jedis:
pool:
# 连接池最大连接数(使用负值表示没有限制)
max-active: 8
# 连接池最大阻塞等待时间(使用负值表示没有限制)
max-wait: -1ms
# 连接池中的最大空闲连接
max-idle: 8
# 连接池中的最小空闲连接
min-idle: 0
# 连接超时时间(毫秒)默认是2000ms
timeout: 2000ms
cache:
redis:
## Entry expiration in milliseconds. By default the entries never expire.
time-to-live: 1d
#写入redis时是否使用键前缀。
use-key-prefix: true
复制代码
编写实体类
import lombok.Data;
import java.io.Serializable;
/**
* @Author: MaoLin
* @Date: 2019/3/24 14:36
* @Version 1.0
*/
@Data
//lombok依赖,可省略get set方法
public class User implements Serializable {
private int userId;
private String userName;
private String userPassword;
public User(int userId, String userName, String userPassword) {
this.userId = userId;
this.userName = userName;
this.userPassword = userPassword;
}
}
复制代码
service简单操作
import com.ml.demo.entity.User;
import org.springframework.stereotype.Service;
/**
* @Author: MaoLin
* @Date: 2019/3/24 14:38
* @Version 1.0
*/
@Service
public class UserDao {
public User getUser(int userId) {
System.out.println("执行此方法,说明没有缓存,如果没有走到这里,就说明缓存成功了");
User user = new User(userId, "没有缓存_"+userId, "password_"+userId);
return user;
}
public User getUser2(int userId) {
System.out.println("执行此方法,说明没有缓存,如果没有走到这里,就说明缓存成功了");
User user = new User(userId, "name_nocache"+userId, "nocache");
return user;
}
}
复制代码
控制层
- 在方法上添加相应的方法即可操作缓存了,SpringCache 对象可以对redis自行操作,减少了很多工作啊,还是那个
开箱即用
的Spring
import com.ml.demo.dao.UserDao;
import com.ml.demo.entity.User;
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.cache.annotation.CachePut;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import javax.annotation.Resource;
/**
* @Author: MaoLin
* @Date: 2019/3/26 17:03
* @Version 1.0
*/
@RestController
public class testController {
@Resource
private UserDao userDao;
/**
* 查询出一条数据并且添加到缓存
*
* @param userId
* @return
*/
@RequestMapping("/getUser")
@Cacheable("userCache")
public User getPrud(@RequestParam(required = true) String userId) {
System.out.println("如果没有缓存,就会调用下面方法,如果有缓存,则直接输出,不会输出此段话");
return userDao.getUser(Integer.parseInt(userId));
}
/**
* 删除一个缓存
*
* @param userId
* @return
*/
@RequestMapping(value = "/deleteUser")
@CacheEvict("userCache")
public String deleteUser(@RequestParam(required = true) String userId) {
return "删除成功";
}
/**
* 添加一条保存的数据到缓存,缓存的key是当前user的id
*
* @param user
* @return
*/
@RequestMapping("/saveUser")
@CachePut(value = "userCache", key = "#result.userId +''")
public User saveUser(User user) {
return user;
}
/**
* 返回结果userPassword中含有nocache字符串就不缓存
*
* @param userId
* @return
*/
@RequestMapping("/getUser2")
@CachePut(value = "userCache", unless = "#result.userPassword.contains('nocache')")
public User getUser2(@RequestParam(required = true) String userId) {
System.out.println("如果走到这里说明,说明缓存没有生效!");
User user = new User(Integer.parseInt(userId), "name_nocache" + userId, "nocache");
return user;
}
@RequestMapping("/getUser3")
@Cacheable(value = "userCache", key = "#root.targetClass.getName() + #root.methodName + #userId")
public User getUser3(@RequestParam(required = true) String userId) {
System.out.println("如果第二次没有走到这里说明缓存被添加了");
return userDao.getUser(Integer.parseInt(userId));
}
}
复制代码
接下来最重要的工作:跑起来
运行结果
-
存入数据:
-
从缓存读取数据:
-
删除缓存:
-
再读取:
-
此时没有缓存,调用方法,并存入缓存
-
此为cache中的条件:含有nocache字符时不存入缓存。自己去探索就好。
5. 小结&参考资料
小结
为了实现缓存,在网上参考了很多博客、资料,但是都不尽人意,后来经过几天的学习,发现Spring提供了缓存对象,我结合redis,优雅地实现了缓存。学习代码是个艰辛的过程,我在学习这部分时看了好多书,找了好多博客资料,终于找到了合适的缓存方案,很开心,不过这还只是一小步啊,加油!!!
- Git源码,欢迎clone和fork
参考资料
- Redis简介 | 菜鸟教程
- SpringCache与redis集成,优雅的缓存解决方案
- 基于Redis的Spring cache 缓存介绍
- SpringBoot | 第十一章:Redis 的集成和简单使用
- 官方文档Spring Data Redis