前言

近日心血来潮想做一个开源项目,目标是做一款可以适配多端、功能完备的模板工程,包含后台管理系统和前台系统,开发者基于此项目进行裁剪和扩展来完成自己的功能开发。

本项目为前后端分离开发,后端基于Java21SpringBoot3开发,后端使用Spring SecurityJWTSpring Data JPA等技术栈,前端提供了vueangularreactuniapp微信小程序等多种脚手架工程。

本文主要介绍在SpringBoot3项目中如何整合Redis,JDK版本是Java21

项目地址:https://gitee.com/breezefaith/fast-alden

相关技术简介

Redis

Redis是一个开源的使用ANSI C语言编写、支持网络、可基于内存亦可持久化的日志型、Key-Value数据库,并提供多种语言的API。

和Memcached类似,它支持存储的value类型相对更多,包括string(字符串)、list(链表)、set(集合)、zset(sorted set --有序集合)和hash(哈希类型)。这些数据类型都支持push/pop、add/remove及取交集并集和差集及更丰富的操作,而且这些操作都是原子性的。在此基础上,redis支持各种不同方式的排序。与memcached一样,为了保证效率,数据都是缓存在内存中。区别的是redis会周期性的把更新的数据写入磁盘或者把修改操作写入追加的记录文件,并且在此基础上实现了master-slave(主从)同步。

redis的出现,很大程度补偿了memcached这类key/value存储的不足,在部分场合可以对关系数据库起到很好的补充作用。它提供了Java,C/C++,C#,PHP,JavaScript,Perl,Object-C,Python,Ruby,Erlang等客户端,使用很方便。

Redis支持主从同步。数据可以从主服务器向任意数量的从服务器上同步,从服务器可以是关联其他从服务器的主服务器。这使得Redis可执行单层树复制。存盘可以有意无意的对数据进行写操作。由于完全实现了发布/订阅机制,使得从数据库在任何地方同步树时,可订阅一个频道并接收主服务器完整的消息发布记录。同步对读取操作的可扩展性和数据冗余很有帮助。

官网:http://redis.io/

Redis 常用数据类型使用场景:

  • String,存短信验证码、缓存、计数器、分布式session
  • List,发布订阅等
  • Set,共同好友、点赞或点踩等
  • Hash,存储对象
  • Zset,排行榜
  • HyperLogLog,在线用户数、统计访问量等
  • GeoHash,同城的人、同城的店等
  • BitMap,签到打卡、活跃用户等

实现步骤

引入maven依赖

pom.xml中添加spring-boot-starter-data-redis以及相关依赖。

<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!-- 解决在实体类使用java.time包下的LocalDateTime、LocalDate等类时序列化/反序列化报错的问题 -->
<dependency>
<groupId>com.fasterxml.jackson.datatype</groupId>
<artifactId>jackson-datatype-jsr310</artifactId>
<version>2.13.0</version>
</dependency>
</dependencies>

项目中引入spring-boot-starter-data-redis后默认使用Lettuce作为Redis客户端库。与老牌的Jedis客户端相比,Lettuce功能更加强大,不仅解决了线程安全的问题,还支持异步和响应式编程,支持集群,Sentinel,管道和编码器等等功能。

如果想使用Jedis,还需要引入Jedis相关依赖。

<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
</dependency>

修改配置文件

修改SpringBoot项目配置文件,本项目使用的是application.yml文件。

spring:
data:
redis:
host: localhost # Redis服务器地址
port: 6379 # Redis服务器连接端口
password: 123456 # Redis服务器连接密码(默认为空)
database: 0 # Redis数据库索引(默认为0)
timeout: 60s # 连接空闲超过N(s秒、ms毫秒,不加单位时使用毫秒)后关闭,0为禁用,这里配置值和tcp-keepalive值一致
# Lettuce连接池配置
lettuce:
pool:
max-active: 10 # 允许最大连接数,默认8(负值表示没有限制),推荐值:大于cpu * 2,通常为(cpu * 2) + 2
max-idle: 8 # 最大空闲连接数,默认8,推荐值:cpu * 2
min-idle: 0 # 最小空闲连接数,默认0
max-wait: 5s # 连接用完时,新的请求等待时间(s秒、ms毫秒),超过该时间抛出异常,默认-1(负值表示没有限制)

定义Redis配置类

在Redis配置类中,我们声明了一个自定义的RedisTemplate<String, Object>和一个自定义的Redis序列化器RedisSerializer<Object>,不声明也可以使用Spring Boot提供的默认的Bean。


/**
* Redis相关Bean配置
*/
@Configuration
public class RedisConfig {
@Bean
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
RedisSerializer<Object> serializer = redisSerializer();
RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
redisTemplate.setConnectionFactory(redisConnectionFactory);
redisTemplate.setKeySerializer(new StringRedisSerializer());
redisTemplate.setValueSerializer(serializer);
redisTemplate.setHashKeySerializer(new StringRedisSerializer());
redisTemplate.setHashValueSerializer(serializer);
redisTemplate.afterPropertiesSet();
return redisTemplate;
} @Bean
public RedisSerializer<Object> redisSerializer() {
ObjectMapper objectMapper = new ObjectMapper();
objectMapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
//必须设置,否则无法将JSON转化为对象,会转化成Map类型
objectMapper.activateDefaultTyping(LaissezFaireSubTypeValidator.instance, ObjectMapper.DefaultTyping.NON_FINAL); // 自定义ObjectMapper的时间处理模块
JavaTimeModule javaTimeModule = new JavaTimeModule(); javaTimeModule.addSerializer(LocalDateTime.class, new LocalDateTimeSerializer(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")));
javaTimeModule.addDeserializer(LocalDateTime.class, new LocalDateTimeDeserializer(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"))); javaTimeModule.addSerializer(LocalDate.class, new LocalDateSerializer(DateTimeFormatter.ofPattern("yyyy-MM-dd")));
javaTimeModule.addDeserializer(LocalDate.class, new LocalDateDeserializer(DateTimeFormatter.ofPattern("yyyy-MM-dd"))); javaTimeModule.addSerializer(LocalTime.class, new LocalTimeSerializer(DateTimeFormatter.ofPattern("HH:mm:ss")));
javaTimeModule.addDeserializer(LocalTime.class, new LocalTimeDeserializer(DateTimeFormatter.ofPattern("HH:mm:ss"))); objectMapper.registerModule(javaTimeModule); // 禁用将日期序列化为时间戳的行为
objectMapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS); //创建JSON序列化器
return new Jackson2JsonRedisSerializer<>(objectMapper, Object.class);
}
}

上述代码中针对java.time包下的LocalDateLocalDateTimeLocalTime等类做了兼容,如果要缓存的实体数据中使用了LocalDateLocalDateTimeLocalTime但没有自定义ObjectMapper的时间处理模块,可能会遇到如下报错。

2024-01-11T21:33:25.233+08:00 ERROR 13212 --- [nio-8080-exec-1] o.a.c.c.C.[.[.[/].[dispatcherServlet]    : Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception

org.springframework.data.redis.serializer.SerializationException: Could not write JSON: Java 8 date/time type `java.time.LocalDateTime` not supported by default: add Module "com.fasterxml.jackson.datatype:jackson-datatype-jsr310" to enable handling (through reference chain: java.util.ArrayList[0]->com.fast.alden.data.model.SysApiResource["createdTime"])
at org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer.serialize(Jackson2JsonRedisSerializer.java:157) ~[spring-data-redis-3.2.0.jar:3.2.0]
at org.springframework.data.redis.core.AbstractOperations.rawValue(AbstractOperations.java:128) ~[spring-data-redis-3.2.0.jar:3.2.0]
at org.springframework.data.redis.core.DefaultValueOperations.set(DefaultValueOperations.java:236) ~[spring-data-redis-3.2.0.jar:3.2.0]

这是因为Jackson库在默认情况下不支持Java8java.time包下的LocalDateLocalDateTimeLocalTime等类型的序列化和反序列化。错误堆栈中也给出了解决方案,添加 com.fasterxml.jackson.datatype:jackson-datatype-jsr310依赖,但光添加依赖是不够的,还我们需要像上述代码一样自定义序列化和反序列化的行为。

定义Redis服务类,封装Redis常用操作

进行到此处时,其实我们已经可以通过获取RedisTemplate<String, Object>这个Bean来操作Redis了,但为了使用方便,我们定义了一个RedisService执行常用的Redis相关操作,实际上就是对RedisTemplate<String, Object>的进一步封装。

RedisService接口定义如下。

/**
* Redis操作服务类
*/
public interface RedisService { /**
* 保存属性
*
* @param time 超时时间(秒)
*/
void set(String key, Object value, long time); /**
* 保存属性
*/
void set(String key, Object value); /**
* 获取属性
*/
Object get(String key); /**
* 删除属性
*/
Boolean del(String key); /**
* 批量删除属性
*/
Long del(List<String> keys); /**
* 设置过期时间
*/
Boolean expire(String key, long time); /**
* 获取过期时间
*/
Long getExpire(String key); /**
* 判断是否有该属性
*/
Boolean hasKey(String key); /**
* 按delta递增
*/
Long incr(String key, long delta); /**
* 按delta递减
*/
Long decr(String key, long delta); /**
* 获取Hash结构中的属性
*/
Object hGet(String key, String hashKey); /**
* 向Hash结构中放入一个属性
*/
Boolean hSet(String key, String hashKey, Object value, long time); /**
* 向Hash结构中放入一个属性
*/
void hSet(String key, String hashKey, Object value); /**
* 直接获取整个Hash结构
*/
Map<Object, Object> hGetAll(String key); /**
* 直接设置整个Hash结构
*/
Boolean hSetAll(String key, Map<String, Object> map, long time); /**
* 直接设置整个Hash结构
*/
void hSetAll(String key, Map<String, ?> map); /**
* 删除Hash结构中的属性
*/
void hDel(String key, Object... hashKey); /**
* 判断Hash结构中是否有该属性
*/
Boolean hHasKey(String key, String hashKey); /**
* Hash结构中属性递增
*/
Long hIncr(String key, String hashKey, Long delta); /**
* Hash结构中属性递减
*/
Long hDecr(String key, String hashKey, Long delta); /**
* 获取Set结构
*/
Set<Object> sMembers(String key); /**
* 向Set结构中添加属性
*/
Long sAdd(String key, Object... values); /**
* 向Set结构中添加属性
*/
Long sAdd(String key, long time, Object... values); /**
* 是否为Set中的属性
*/
Boolean sIsMember(String key, Object value); /**
* 获取Set结构的长度
*/
Long sSize(String key); /**
* 删除Set结构中的属性
*/
Long sRemove(String key, Object... values); /**
* 获取List结构中的属性
*/
List<Object> lRange(String key, long start, long end); /**
* 获取List结构的长度
*/
Long lSize(String key); /**
* 根据索引获取List中的属性
*/
Object lIndex(String key, long index); /**
* 向List结构中添加属性
*/
Long lPush(String key, Object value); /**
* 向List结构中添加属性
*/
Long lPush(String key, Object value, long time); /**
* 向List结构中批量添加属性
*/
Long lPushAll(String key, Object... values); /**
* 向List结构中批量添加属性
*/
Long lPushAll(String key, Long time, Object... values); /**
* 从List结构中移除属性
*/
Long lRemove(String key, long count, Object value);
}

RedisService实现类定义如下。

/**
* Redis操作实现类
*/
@Service
public class RedisServiceImpl implements RedisService {
@Resource
private RedisTemplate<String, Object> redisTemplate; @Override
public void set(String key, Object value, long time) {
redisTemplate.opsForValue().set(key, value, time, TimeUnit.SECONDS);
} @Override
public void set(String key, Object value) {
redisTemplate.opsForValue().set(key, value);
} @Override
public Object get(String key) {
return redisTemplate.opsForValue().get(key);
} @Override
public Boolean del(String key) {
return redisTemplate.delete(key);
} @Override
public Long del(List<String> keys) {
return redisTemplate.delete(keys);
} @Override
public Boolean expire(String key, long time) {
return redisTemplate.expire(key, time, TimeUnit.SECONDS);
} @Override
public Long getExpire(String key) {
return redisTemplate.getExpire(key, TimeUnit.SECONDS);
} @Override
public Boolean hasKey(String key) {
return redisTemplate.hasKey(key);
} @Override
public Long incr(String key, long delta) {
return redisTemplate.opsForValue().increment(key, delta);
} @Override
public Long decr(String key, long delta) {
return redisTemplate.opsForValue().increment(key, -delta);
} @Override
public Object hGet(String key, String hashKey) {
return redisTemplate.opsForHash().get(key, hashKey);
} @Override
public Boolean hSet(String key, String hashKey, Object value, long time) {
redisTemplate.opsForHash().put(key, hashKey, value);
return expire(key, time);
} @Override
public void hSet(String key, String hashKey, Object value) {
redisTemplate.opsForHash().put(key, hashKey, value);
} @Override
public Map<Object, Object> hGetAll(String key) {
return redisTemplate.opsForHash().entries(key);
} @Override
public Boolean hSetAll(String key, Map<String, Object> map, long time) {
redisTemplate.opsForHash().putAll(key, map);
return expire(key, time);
} @Override
public void hSetAll(String key, Map<String, ?> map) {
redisTemplate.opsForHash().putAll(key, map);
} @Override
public void hDel(String key, Object... hashKey) {
redisTemplate.opsForHash().delete(key, hashKey);
} @Override
public Boolean hHasKey(String key, String hashKey) {
return redisTemplate.opsForHash().hasKey(key, hashKey);
} @Override
public Long hIncr(String key, String hashKey, Long delta) {
return redisTemplate.opsForHash().increment(key, hashKey, delta);
} @Override
public Long hDecr(String key, String hashKey, Long delta) {
return redisTemplate.opsForHash().increment(key, hashKey, -delta);
} @Override
public Set<Object> sMembers(String key) {
return redisTemplate.opsForSet().members(key);
} @Override
public Long sAdd(String key, Object... values) {
return redisTemplate.opsForSet().add(key, values);
} @Override
public Long sAdd(String key, long time, Object... values) {
Long count = redisTemplate.opsForSet().add(key, values);
expire(key, time);
return count;
} @Override
public Boolean sIsMember(String key, Object value) {
return redisTemplate.opsForSet().isMember(key, value);
} @Override
public Long sSize(String key) {
return redisTemplate.opsForSet().size(key);
} @Override
public Long sRemove(String key, Object... values) {
return redisTemplate.opsForSet().remove(key, values);
} @Override
public List<Object> lRange(String key, long start, long end) {
return redisTemplate.opsForList().range(key, start, end);
} @Override
public Long lSize(String key) {
return redisTemplate.opsForList().size(key);
} @Override
public Object lIndex(String key, long index) {
return redisTemplate.opsForList().index(key, index);
} @Override
public Long lPush(String key, Object value) {
return redisTemplate.opsForList().rightPush(key, value);
} @Override
public Long lPush(String key, Object value, long time) {
Long index = redisTemplate.opsForList().rightPush(key, value);
expire(key, time);
return index;
} @Override
public Long lPushAll(String key, Object... values) {
return redisTemplate.opsForList().rightPushAll(key, values);
} @Override
public Long lPushAll(String key, Long time, Object... values) {
Long count = redisTemplate.opsForList().rightPushAll(key, values);
expire(key, time);
return count;
} @Override
public Long lRemove(String key, long count, Object value) {
return redisTemplate.opsForList().remove(key, count, value);
}
}

使用Redis服务类

下面以简单的登录和注销为例介绍Redis服务类的简单使用,登录成功时向Redis中插入了一条当前用户的记录,如果要查询系统当前在线用户可以从Redis中查询;注销时从Redis中找到在线用户记录并删除。

@Service
public class AuthServiceImpl extends AuthService {
private final RedisService redisService; public AuthServiceImpl(RedisService redisService) {
this.redisService = redisService;
} public String login(LoginParam param) {
// 根据登录参数查找用户,具体代码请自行实现
SysUser user = new SysUser();
// 根据用户信息生成token,具体代码请自行实现
String token = "";
// 在Redis中增加一条在线用户记录
redisService.set("OnlineUser:" + user.getUsername() + ":" + token, user); return token;
} public void logout() {
// 获取当前用户,具体代码请自行实现
SysUser user = new SysUser();
// 获取当前用户token,具体代码请自行实现
String token = "";
// 清空登录信息,具体代码请自行实现 // 删除Redis中当前用户记录
redisService.del("OnlineUser:" + user.getUsername() + ":" + token);
}
}

总结

本文简单介绍了一下RedisRedis常见数据类型的使用场景,以及详细介绍了SpringBoot3整合Redis的详细过程,如有错误,还望批评指正。

在后续实践中我也是及时更新自己的学习心得和经验总结,希望与诸位看官一起进步。

Java21 + SpringBoot3整合Redis,使用Lettuce连接池,推荐连接池参数配置,封装Redis操作的更多相关文章

  1. mysql 远程连接不上,bind-address参数配置要求,以及怎么去使得mysql能够允许远程的客户端访问

    刚安装了MySQL服务器,使用远程管理工具总是连接不上,因为知道mysql的默认端口是3306,于是使用telnet连接这个端口,(从这里可以学到telnet是可以这样用的) telnet 192.1 ...

  2. HttpClient 4.3连接池参数配置及源码解读

    目前所在公司使用HttpClient 4.3.3版本发送Rest请求,调用接口.最近出现了调用查询接口服务慢的生产问题,在排查整个调用链可能存在的问题时(从客户端发起Http请求->ESB-&g ...

  3. HttpClient4.3 连接池参数配置及源码解读

    目前所在公司使用HttpClient 4.3.3版本发送Rest请求,调用接口.最近出现了调用查询接口服务慢的生产问题,在排查整个调用链可能存在的问题时(从客户端发起Http请求->ESB-&g ...

  4. 踩坑记录:Redis的lettuce连接池不生效

    踩坑记录:Redis的lettuce连接池不生效 一.lettuce客户端 lettuce客户端 Lettuce 和 Jedis 的都是连接Redis Server的客户端程序.Jedis在实现上是直 ...

  5. Springboot2.x+shiro+redis(Lettuce)整合填坑

    主要记录关键和有坑的地方 前提: 1.SpringBoot+shiro已经集成完毕,如果没有集成,先查阅之前的Springboot2.0 集成shiro权限管理 2.redis已经安装完成 3.red ...

  6. OpenResty 高阶实战之————Redis授权登录使用短连接(5000)和长连接(500W) 使用连接池AB压力测试结果

    一.短连接开始测试 ab -n 5000 -c 100 -k 127.0.0.1/test_redis_short #demo1 Concurrency Level: Time taken for t ...

  7. java原生程序redis连接(连接池/长连接和短连接)选择问题

    最近遇到的连接问题我准备从重构的几个程序(redis和mysql)长连接和短连接,以及连接池和单连接等问题用几篇博客来总结下. 这个问题的具体发生在java原生程序和redis的交互中.这个问题对我最 ...

  8. ServiceStack.Redis的源码分析(连接与连接池)

    前几天在生产环境上redis创建连接方面的故障,分析过程中对ServiceStack.Redis的连接创建和连接池机制有了进一步了解.问题分析结束后,通过此文系统的将学习到的知识点整理出来. 从连接池 ...

  9. 初探 Redis 客户端 Lettuce:真香!

    一.Lettuce 是啥? 一次技术讨论会上,大家说起 Redis 的 Java 客户端哪家强,我第一时间毫不犹豫地喊出 "Jedis, YES!" "Jedis 可是官 ...

  10. Redis 学习笔记3:Jedis 连接虚拟机下的Redis 服务

    Jedis 是 Redis 官方首选的 Java 客户端开发包. 虚拟机的IP地址是192.168.8.88. Jedis代码是放在windows上的,启动虚拟机上的Redis服务之后,用Jedis连 ...

随机推荐

  1. Base64编码:数据传输的安全使者

    Base64编码是一种将二进制数据转换为可传输的文本表示形式的方法,它在全球范围内被广泛应用于网络通信.数据存储和传输等领域.本文将从多个方面介绍Base64编码的原理.应用及其在现实场景中的优势,帮 ...

  2. 斯坦福 UE4 C++ ActionRoguelike游戏实例教程 12.认识GamePlayTag, 实现技能的互斥

    斯坦福课程 UE4 C++ ActionRoguelike游戏实例教程 0.绪论 概述 本篇文章对应Lecture 17 - GameplayTags, 67.67节.本文将会讲述UE4中Gamepl ...

  3. spring-cloud-alibaba项目打包

    在父依赖中加入 <build> <plugins> <plugin> <groupId>org.springframework.boot</gro ...

  4. GDAL使用PROJ坐标转换相关问题的总结

    目录 1. 概述 2. 详论 2.1. 数据 2.2. PROJ库 2.3. 参考 1. 概述 GDAL是使用PROJ进行坐标转换的,但是很容易出现转换不了的问题,这里总结一二,以供参考. 2. 详论 ...

  5. three.js中帧缓存的使用

    目录 1. 概述 2. 示例 2.1. 代码 2.2. 解析 3. 参考 1. 概述 在网上查阅了一下three.js关于帧缓存的使用,感觉很多都是关于three.js中后处理通道的使用的.后处理通道 ...

  6. MySQL进阶篇:详解SQL性能分析

    MySQL进阶篇:第三章_SQL性能分析 SQL执行频率 MySQL 客户端连接成功后,通过 show [session|global] status 命令可以提供服务器状态信息.通过如下指令,可以查 ...

  7. 输入的查询SQL语句,是如何执行的?

    摘要:输入一条语句,返回一个结果,却不知道这条语句在 MySQL 内部的执行过程. 本文分享自华为云社区<一条查询SQL是如何执行的>,作者: 共饮一杯无 . 执行如下SQL,我们看到的只 ...

  8. Solon 编译打包怎样保持参数名不变?

    1.Java 项目 Java maven 项目 <plugin> <groupId>org.apache.maven.plugins</groupId> <a ...

  9. Jmeter对各种接口类型的测试

    JMeter对各种类型接口的测试 默认做接口测试前,已经给出明确的接口文档(如,http://test.nnzhp.cn/wiki/index.php?doc-view-59):本地配好了JMeter ...

  10. AI 黑科技,老照片修复,模糊变高清

    大家好 最近闲逛,发现腾讯开源的老照片修复算法新出了V1.3的预训练模型,手痒试了一下. 我拿"自己"的旧照片试了一下,先看效果 GFPGAN FPGAN算法由腾讯PCG ARC实 ...