红茶的个人站点

  • 首页
  • 专栏
  • 开发工具
  • 其它
  • 隐私政策
Awalon
Talk is cheap,show me the code.
  1. 首页
  2. 专栏
  3. Spring Boot 学习笔记
  4. 正文

从零开始 Spring Boot 19:Redis

2022年8月25日 230点热度 0人点赞 0条评论

spring boot

图源:简书 (jianshu.com)

Redis是一种很成熟的缓存技术,也被称作NOSQL。可以利用这类技术来缓存长时间计算的结果,以节约系统资源或者提升响应时间。

和以往一样,本篇文章的示例代码将基于从零开始 Spring Boot 18:微信登录 - 魔芋红茶's blog (icexmoon.cn)中的最终示例代码进行修改,可以从仓库learn_spring_boot/ch18 (github.com)获取对应的完整源码。

安装Redis

在Windows上安装Redis的方式可以参考:

  • 在 windows 上安装 Redis

但实际上Redis官方并不推荐上边这种做法,实际上微软团队也早就不维护上边的项目了。

Redis官方推荐通过WSL2安装,具体步骤可以参考:

  • Install Redis on Windows | Redis

不知道怎么安装WSL2的可以参考安装 WSL | Microsoft Docs。

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客户端有很多,我使用的是Another Redis Desktop Manager 。

下载和安装过程可以参考Another Redis DeskTop Manage一款免费的Redis可视化工具 - 腾讯云开发者社区-腾讯云 (tencent.com)。

配置

添加依赖:

        <!-- 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进行配置:

@Configuration
@EnableCaching
public class RedisConfig extends CachingConfigurerSupport {
​
    @Bean
    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();
        };
    }
​
    @Bean
    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的语法检查:

image-20220825093347821

执行编译也会报错。

就像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:

image-20220825094431077

@Resource就不存在上边的问题,因为在不指定属性时,@Resource是按照所修饰的属性名称来匹配的,在这里要装配的属性名是redisTemplate,而我们注入IOC的Bean名称也是redisTemplate,所以自然可以匹配成功进行装配。

可能有人会疑惑这里我们需要的是RedisTemplate<String, GetPagedBooksVO>,但实际上通过自动装配获取到的是一个RedisTemplate<String, Object>对象,这样会不会出问题?

实际上Java实现泛型的方式仅仅是在编译层面的语法检查,在运行期所有被定义为泛型的属性和对象都是以Object类型定义和存在的,所以在运行期并不会存在“因为类型参数不一致”而导致的运行错误,当然前提是你往泛型容器中存入的数据和读取的数据是同一类型,否则依然会出现类型转换错误。

关于泛型原理相关内容,可以阅读Java编程笔记13:泛型 - 魔芋红茶's blog (icexmoon.cn)。

从这点上看,对于自动装配对象是泛型的,且其类型参数与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;
    }
}

主要分为三个步骤:

  1. 引入配置中的redis相关配置信息。

  2. 注入SessionDAO,这个类型是shiro抽象出的session存储介质对象,所以这里具体是用RedisSessionDAO来注入。RedisSessionDAO具体使用的redis连接信息由RedisManager对象持有,所以还额外注入了一个RedisManager对象。

  3. 修改注入的SessionManager对象,通过SessionManager.setSessionDAO方法为其指定我们注入的SessionDAO作为session存储介质,而非默认的内存。

之后可以先登录并调用接口,再重启应用后重新调用接口,同样的token可以满足两次调用,这说明应用停止、重启后在Redis中保存的token没有失效。

关于Spring Boot中使用Redis的相关内容就介绍到这里了,比较简单,依然遵循本系列文章实例优先的风格。

最后,本篇文章的最终完整示例代码见learn_spring_boot/ch19 (github.com)。

谢谢阅读。

参考资料

  • @Autowired 和 @Resource 注解的区别及正确的使用姿势 - 简书 (jianshu.com)

本作品采用 知识共享署名 4.0 国际许可协议 进行许可
标签: redis spring boot
最后更新:2022年8月29日

魔芋红茶

加一点PHP,加一点Go,加一点Python......

点赞
< 上一篇
下一篇 >

文章评论

取消回复

*

code

COPYRIGHT © 2021 icexmoon.cn. ALL RIGHTS RESERVED.
本网站由提供CDN加速/云存储服务

Theme Kratos Made By Seaton Jiang

宁ICP备2021001508号

宁公网安备64040202000141号