图源:
Redis是一种很成熟的缓存技术,也被称作NOSQL。可以利用这类技术来缓存长时间计算的结果,以节约系统资源或者提升响应时间。
和以往一样,本篇文章的示例代码将基于中的最终示例代码进行修改,可以从仓库获取对应的完整源码。
在Windows上安装Redis的方式可以参考:
但实际上Redis官方并不推荐上边这种做法,实际上微软团队也早就不维护上边的项目了。
Redis官方推荐通过WSL2安装,具体步骤可以参考:
不知道怎么安装WSL2的可以参考。
Redis自带一个命令行的客户端redis-cli
。
如果是Windows安装,就在安装目录下的
redis-cli.exe
。
启动后会自动连接本地安装的Redis
icexmoon@Awalon:~$ redis-cli
127.0.0.1:6379>
可以使用PING命令来检测连接是否正常:
127.0.0.1:6379> PING
PONG
使用exit
命令退出:
127.0.0.1:6379> exit
也可以连接指定Redis服务器:
icexmoon@Awalon:~$ redis-cli -h 192.168.1.120 -p 6379 -a redis
Warning: Using a password with '-a' or '-u' option on the command line interface may not be safe.
192.168.1.120:6379>
可以通过keys
命令查找指定的键值对:
192.168.1.120:6379> keys ccsp* 1) "ccsp.trtc.usersig:ccspsv"
keys
命令支持简单的通配符以及正则。
虽然命令行客户端可以满足我们的需要,但并不好用,幸运的是我们有很多图形化的Redis客户端可以选择。
图形化Redis客户端有很多,我使用的是。
下载和安装过程可以参考。
配置
添加依赖:
<!-- redis -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
添加相关配置:
#redis 公共配置
spring.redis.database=0
# Redis服务器连接端口
spring.redis.port=6379
# 连接池最大连接数(使用负值表示没有限制) 默认 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
# Redis服务器地址
spring.redis.host=localhost
# Redis服务器连接密码(默认为空)
spring.redis.password=
添加配置类对Redis进行配置:
public class RedisConfig extends CachingConfigurerSupport {
public KeyGenerator keyGenerator() {
return (target, method, params) -> {
StringBuilder sb = new StringBuilder();
sb.append(target.getClass().getName());
sb.append(method.getName());
for (Object obj : params) {
sb.append(obj.toString());
}
return sb.toString();
};
}
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
RedisTemplate<String, Object> template = new RedisTemplate<>();
template.setConnectionFactory(redisConnectionFactory);
// Json序列化配置
Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);
ObjectMapper objectMapper = new ObjectMapper();
objectMapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
// enableDefaultTyping 方法已经过时,使用新的方法activateDefaultTyping
// objectMapper.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
objectMapper.activateDefaultTyping(LaissezFaireSubTypeValidator.instance, ObjectMapper.DefaultTyping.NON_FINAL, JsonTypeInfo.As.WRAPPER_ARRAY);
jackson2JsonRedisSerializer.setObjectMapper(objectMapper);
// String序列化
StringRedisSerializer stringRedisSerializer = new StringRedisSerializer();
// key 序列化方式
template.setKeySerializer(stringRedisSerializer);
// hash的key序列化方式
template.setHashKeySerializer(stringRedisSerializer);
// value的序列化方式(json序列化)
template.setValueSerializer(jackson2JsonRedisSerializer);
// hash的value序列化方式(json序列化)
template.setHashValueSerializer(jackson2JsonRedisSerializer);
template.afterPropertiesSet();
return template;
}
}
因为实际上Redis中的键值对都是以字符串形式存在的,所以实际上我们要对不同类型的key和值序列化为字符串后再保存到Redis中,而这个配置类的用途就是为Redis指定默认的序列化方式。在这里配置好后使用Redis的时候就可以直接使用具体类型,无需再操心怎么序列化和反序列化了。
使用
假设在这个示例项目中,获取书籍分页信息是一个比较耗时的请求,可以使用Redis缓存数据来进行优化。
获取书籍分页信息的控制层方法是:
@PostMapping("/book/page")
@ApiOperation("获取分页的书籍列表")
public GetPagedBooksVO getPagedBooks(@Validated @RequestBody GetPagedBooksDTO dto) {
...
}
使用Redis缓存数据,需要先选择Redis中的数据类型,Redis中的数据类型有Map、List等,最简单的是Value,可以看做是字符串,通过对应的Key就可以对Value进行获取和写入。
篇幅关系这里对Redis数据类型不展开说明。
要使用Redis,需要先自动装配一个RedisTemplate
类型的对象:
public class BookController {
...
@Resource
private RedisTemplate<String, GetPagedBooksVO> redisTemplate;
...
}
RedisTemplate
是一个泛型类,为了能通过其API直接写入和获取我们所需要的具体类型,而不是Object
,这里需要指定类型参数。RedisTemplate
有两个类型参数,第一个是Redis中用于查找匹配存储数据的索引值,一般都是String
类型,第二个是具体存储的数据类型,这里我们需要缓存getPagedBooks
方法的查询结果,所以是GetPagedBooksVO
。
自动装配
需要注意的是,这里使用@Resource
注解来自动装配redisTemplate
属性,而非常见的@Autowired
注解。
事实上使用@Autowired
无法通过IDEA的语法检查:
执行编译也会报错。
就像IDEA提示的,@Autowired
注解是通过匹配类型来完成自动装配的,而我们在Redis相关配置类中注入的Bean是这样的:
@Configuration
@EnableCaching
public class RedisConfig extends CachingConfigurerSupport {
...
@Bean
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
...
}
}
通过@Bean
注解将方法的返回值注入IOC,此时注入的Bean的类型是RedisTemplate<String, Object>
,名称是方法名redisTemplate
。
所以之前试图用@Autowired
注解装配RedisTemplate<String, GetPagedBooksVO>
类型的属性会报错,因为在编译期泛型检查是有效的,RedisTemplate<String, GetPagedBooksVO>
和RedisTemplate<String, Object>
被认为是两种不同的类型。
@Qualifier
注解可以和@Autowired
结合使用,在类型匹配的基础上再使用Bean的名称匹配,但显然这里是不适用的,因为没有匹配类型的Bean:
@Resource
就不存在上边的问题,因为在不指定属性时,@Resource
是按照所修饰的属性名称来匹配的,在这里要装配的属性名是redisTemplate
,而我们注入IOC的Bean名称也是redisTemplate
,所以自然可以匹配成功进行装配。
可能有人会疑惑这里我们需要的是
RedisTemplate<String, GetPagedBooksVO>
,但实际上通过自动装配获取到的是一个RedisTemplate<String, Object>
对象,这样会不会出问题?实际上Java实现泛型的方式仅仅是在编译层面的语法检查,在运行期所有被定义为泛型的属性和对象都是以
Object
类型定义和存在的,所以在运行期并不会存在“因为类型参数不一致”而导致的运行错误,当然前提是你往泛型容器中存入的数据和读取的数据是同一类型,否则依然会出现类型转换错误。关于泛型原理相关内容,可以阅读。
从这点上看,对于自动装配对象是泛型的,且其类型参数与IOC中目标Bean类型参数不一致的,必须使用
@Resource
而非@Autowired
完成自动装配。当然这是我个人总结的,如果有什么不对的欢迎指正。
ops
就像前面说的,Redis中的数据类型是分为多种类型的,对应的,RedisTemplate
用于执行读写等相关操作的API也按照Redis数据类型抽象为多种类型,比如用于操作最简单的Value类型的就是ValueOperations
。
所以要想读写数据,我们就要先获取一个对应数据类型的“操作”,这可以通过opsXXX
相关方法完成:
@PostMapping("/book/page")
@ApiOperation("获取分页的书籍列表")
public GetPagedBooksVO getPagedBooks(@Validated @RequestBody GetPagedBooksDTO dto) {
final String REDIS_PREFIX = "books.getpagedbooks.";
String dtoJson = JSON.toJSONString(dto);
String redisKey = REDIS_PREFIX + MyStringUtil.md5(dtoJson);
GetPagedBooksVO cachedVO = redisTemplate.opsForValue().get(redisKey);
if (cachedVO != null) {
return cachedVO;
}
log.info("current:" + dto.getPageRequest().getCurrent());
log.info("paging:" + dto.getPageRequest().getPaging());
IPage<Book> pagedBooks = bookService.getPagedBooks(dto.getPageRequest().getPage());
GetPagedBooksVO vo = new GetPagedBooksVO();
vo.setBooks(pagedBooks.getRecords());
vo.setPageResponse(PageResponse.getPageResponse(pagedBooks));
redisTemplate.opsForValue().set(redisKey, vo, 5, TimeUnit.MINUTES);
return vo;
}
这里首先为要缓存的数据索引确定一个前缀,通常是项目名称+类名+方法名的方式来确保唯一性。
如果控制层的方法会接收参数,那我们就还需要将参数信息追加到Redis索引中,以确保为不同入参缓存不同的结果集。当然参数信息往往很长,而Redis索引长度有限,所以通常采用先将参数JSON,然后再MD5的方式获取入参“摘要”,然后作为Redis索引的一部分。
因为我们使用的Redis数据类型是Value
,所以这里通过redisTemplate.opsForValue()
方法获取一个Value
对应的ops,然后调用其get
方法获取可能存在的缓存数据。
如果数据不存在,就执行原本的逻辑,从数据库查询数据。如果存在缓存数据,就直接返回。
最后,如果不存在缓存数据,从数据库查询后还需要调用ops的set
方法缓存数据到Redis,这样下次同样入参的查询就不经过数据库了,可以提高接口调用效率。
需要注意的是,使用Redis缓存数据的时候一般需要指定缓存时间,否则会被视为长期有效,这样会浪费Redis的内存。这样做还有一个好处是可以利用Redis存储一些短期有效的数据,比如各种验证码。指定有效期的具体方式是通过set
的额外参数实现,其第三个参数可以指定有效时长,第四个参数可以指定时间单位。
shiro
除了使用Redis缓存数据优化接口以外,还可以用Redis作为shiro保存身份信息Session的存储介质。
shiro默认采用本地服务器的内存作为Session的存储介质,这样做有两个缺陷:
-
每次应用重启都会导致Session失效(内存中应用申请的空间都会被销毁),这也是为什么这个示例应用在修改代码重启后需要重新登录获取token。
-
无法在多台服务器之间共享Session,也就是说无法实现多台服务器的负载均衡部署。
所以商业化应用更多的是使用Redis作为shiro的存储介质。
要让shiro使用Redis需要先添加依赖:
<!-- shiro使用redis -->
<dependency>
<groupId>org.crazycake</groupId>
<artifactId>shiro-redis</artifactId>
<version>3.3.1</version>
</dependency>
修改shiro的配置类:
@Configuration
public class ShiroConfig {
@Value("${spring.redis.password}")
private String redisPassword;
@Value("${spring.redis.host}")
private String redisHost;
@Value("${spring.redis.port}")
private String redisPort;
...
@Bean(name = "sessionManager")
public SessionManager sessionManager() {
CustomSessionManager customSessionManager = new CustomSessionManager();
//设置session过期时间(单位毫秒)
//设置shiro session有效期为15天
customSessionManager.setGlobalSessionTimeout(60 * 60 * 24 * 15 * 1000);
//定期验证session
customSessionManager.setSessionValidationSchedulerEnabled(true);
customSessionManager.setSessionDAO(sessionDAO());
//删除无效session
customSessionManager.setDeleteInvalidSessions(true);
return customSessionManager;
}
...
@Bean
public RedisManager redisManager() {
// 它可对redis的ip、端口等进行配置
RedisManager redisManager = new RedisManager();
redisManager.setHost(redisHost + ":" + redisPort);
if (!ObjectUtils.isEmpty(redisPassword)) {
redisManager.setPassword(redisPassword);
}
return redisManager;
}
@Bean
public SessionDAO sessionDAO() {
RedisSessionDAO redisSessionDAO = new RedisSessionDAO();
redisSessionDAO.setRedisManager(redisManager());
return redisSessionDAO;
}
}
主要分为三个步骤:
-
引入配置中的redis相关配置信息。
-
注入
SessionDAO
,这个类型是shiro抽象出的session存储介质对象,所以这里具体是用RedisSessionDAO
来注入。RedisSessionDAO
具体使用的redis连接信息由RedisManager
对象持有,所以还额外注入了一个RedisManager
对象。 -
修改注入的
SessionManager
对象,通过SessionManager.setSessionDAO
方法为其指定我们注入的SessionDAO
作为session存储介质,而非默认的内存。
之后可以先登录并调用接口,再重启应用后重新调用接口,同样的token可以满足两次调用,这说明应用停止、重启后在Redis中保存的token没有失效。
关于Spring Boot中使用Redis的相关内容就介绍到这里了,比较简单,依然遵循本系列文章实例优先的风格。
最后,本篇文章的最终完整示例代码见。
谢谢阅读。
文章评论