记录Springboot整合redis中遇到的问题。
Springboot整合redis
相关配置
POM文件
<!--添加父工程依赖-->
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.5.0</version>
<relativePath/>
</parent>
<!--添加redis相关依赖-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-pool2</artifactId>
</dependency>
application.yml
# 测试环境
spring:
#redis
redis:
database: 0
host: 127.0.0.1
port: 6379
password: foobared
timeout: 5000
# 连接池设置
lettuce:
pool:
max-idle: 8
max-wait: -1
min-idle: 1
max-active: 8
shutdowntimeout: 100
RedisConfig相关代码
package com.rrc.config;
import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.PropertyAccessor;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.rrc.listener.RedisReceiver;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.pool2.impl.GenericObjectPoolConfig;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.cache.interceptor.KeyGenerator;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisPassword;
import org.springframework.data.redis.connection.RedisStandaloneConfiguration;
import org.springframework.data.redis.connection.lettuce.LettuceClientConfiguration;
import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory;
import org.springframework.data.redis.connection.lettuce.LettucePoolingClientConfiguration;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.listener.ChannelTopic;
import org.springframework.data.redis.listener.RedisMessageListenerContainer;
import org.springframework.data.redis.listener.adapter.MessageListenerAdapter;
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.RedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;
import java.time.Duration;
@Slf4j
@Configuration
public class RedisConfig {
@Value("${spring.redis.database}")
private int database;
@Value("${spring.redis.host}")
private String host;
@Value("${spring.redis.password}")
private String password;
@Value("${spring.redis.port}")
private int port;
@Value("${spring.redis.timeout}")
private long timeout;
@Value("${spring.redis.lettuce.shutdown-timeout}")
private long shutDownTimeout;
@Value("${spring.redis.lettuce.pool.max-idle}")
private int maxIdle;
@Value("${spring.redis.lettuce.pool.min-idle}")
private int minIdle;
@Value("${spring.redis.lettuce.pool.max-active}")
private int maxActive;
@Value("${spring.redis.lettuce.pool.max-wait}")
private long maxWait;
//json序列化器
private Jackson2JsonRedisSerializer<Object> jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);
@Bean
public LettuceConnectionFactory lettuceConnectionFactory() {
GenericObjectPoolConfig genericObjectPoolConfig = new GenericObjectPoolConfig();
genericObjectPoolConfig.setMaxIdle(maxIdle);
genericObjectPoolConfig.setMinIdle(minIdle);
genericObjectPoolConfig.setMaxTotal(maxActive);
genericObjectPoolConfig.setMaxWaitMillis(maxWait);
genericObjectPoolConfig.setTimeBetweenEvictionRunsMillis(100);
RedisStandaloneConfiguration redisStandaloneConfiguration = new RedisStandaloneConfiguration();
redisStandaloneConfiguration.setDatabase(database);
redisStandaloneConfiguration.setHostName(host);
redisStandaloneConfiguration.setPort(port);
redisStandaloneConfiguration.setPassword(RedisPassword.of(password));
LettuceClientConfiguration clientConfig = LettucePoolingClientConfiguration.builder()
.commandTimeout(Duration.ofMillis(timeout))
.shutdownTimeout(Duration.ofMillis(shutDownTimeout))
.poolConfig(genericObjectPoolConfig)
.build();
LettuceConnectionFactory factory = new LettuceConnectionFactory(redisStandaloneConfiguration, clientConfig);
// factory.setShareNativeConnection(true);
// factory.setValidateConnection(false);
return factory;
}
//redisTemplate模板提供给其他类对redis数据库进行操作
@Bean(name = "redisTemplate")
public RedisTemplate<String, Object> redisTemplate(LettuceConnectionFactory redisConnectionFactory) {
//序列化配置
Jackson2JsonRedisSerializer<Object> objectJackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer<Object>(Object.class);
ObjectMapper objectMapper = new ObjectMapper();
objectMapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
objectJackson2JsonRedisSerializer.setObjectMapper(objectMapper);
RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
redisTemplate.setConnectionFactory(redisConnectionFactory);
redisTemplate.setKeySerializer(keySerializer());
redisTemplate.setHashKeySerializer(keySerializer());
redisTemplate.setValueSerializer(valueSerializer());
redisTemplate.setHashValueSerializer(valueSerializer());
log.debug("自定义RedisTemplate加载完成");
return redisTemplate;
}
//redis键序列化使用StringRedisSerializer
private RedisSerializer<String> keySerializer() {
return new StringRedisSerializer();
}
//redis值序列化使用json序列化器
private RedisSerializer<Object> valueSerializer() {
return new GenericJackson2JsonRedisSerializer();
}
//缓存键自动生成器
@Bean
public KeyGenerator myKeyGenerator() {
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();
};
}
/**
* redis消息监听器容器
* 点赞消息订阅处理器
*
* @param collectListenerAdapter 关注消息订阅处理器
* @return
*/
@Bean
RedisMessageListenerContainer container(LettuceConnectionFactory redisConnectionFactory,
MessageListenerAdapter collectListenerAdapter,
MessageListenerAdapter commentListenerAdapter) {
RedisMessageListenerContainer container = new RedisMessageListenerContainer();
container.setConnectionFactory(redisConnectionFactory);
// 以下为修改默认的序列化方式,网上大多数消息发布订阅都是String类型,但是实际中数据类型肯定不止String类型
Jackson2JsonRedisSerializer<Object> jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer<Object>(
Object.class);
ObjectMapper objectMapper = new ObjectMapper();
objectMapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
jackson2JsonRedisSerializer.setObjectMapper(objectMapper);
// 收藏主题并绑定消息订阅处理器
collectListenerAdapter.setSerializer(jackson2JsonRedisSerializer);
container.addMessageListener(collectListenerAdapter, new ChannelTopic("TOPIC_COLLECT"));
return container;
}
/**
* 收藏消息订阅处理器,并指定处理方法
*
* @param receiver
* @return
*/
@Bean
MessageListenerAdapter commentListenerAdapter(RedisReceiver receiver) {
MessageListenerAdapter commentListenerAdapter = new MessageListenerAdapter(receiver);
//消息的反序列化方式
commentListenerAdapter.setSerializer(jackson2JsonRedisSerializer);
return commentListenerAdapter;
}
}
发布订阅
消息发送
redisTemplate.convertAndSend(entry.getKey(), JSON.toJSONString(message));
Redis发布订阅频道信息获取
Redis原生命令获取发布订阅频道信息为:pubsub channels [pattern]
。但是我在开发的时候使用的客户端是redisTemplate。不支持上述命令。最后使用曲线救国的方式。因为我连接池使用的是jedis,而jedis支持该命令,所以写了个工具类进行转换。
public class RedisChannelUtil {
private RedisChannelUtil() {
}
/**
* @Author Wangql
* @Description 获取Redis中的发布订阅频道名称 redisTemplate不支持此命令 使用底层jedis来执行
* @Date 18:47 2021/7/1
* @Param [redisTemplate]
* @return java.util.List<java.lang.String>
**/
public static List<String> getPushRedisChannels(RedisTemplate redisTemplate) {
RedisConnectionFactory connection = redisTemplate.getConnectionFactory();
Jedis jedis = null;
try {
jedis = (Jedis) connection.getConnection().getNativeConnection();
// 使用的带前缀的模糊匹配
return jedis.pubsubChannels(PushConstant.REDIS_SERVICE_CHANNEL + "*");
} finally {
if (jedis != null) {
jedis.close();
}
}
}
}
注意点:
org.springframework.data.redis.listener.ChannelTopic:一个确定的字符串
org.springframework.data.redis.listener.PatternTopic:基于模式匹配
在使用ChannelTopic时可以使用pubsub channels [pattern]获取到对应的频道,但是在使用PatternTopic时使用pubsub channels [pattern]就存在获取不到相应频道的情况。使用时一定要注意!!!
踩坑记录
>>>>>>>>>>>>>>>>>>>>SpringBoot 服务启动完毕<<<<<<<<<<<<<<<<<<
2021-08-01 02:16:15.116 [main] INFO org.springframework.boot.availability.ApplicationAvailabilityBean 75 logStateChange - Application availability state ReadinessState changed to ACCEPTING_TRAFFIC
2021-08-01 02:16:15.813 [container-2] ERROR org.springframework.data.redis.listener.RedisMessageListenerContainer 651 handleSubscriptionException - Connection failure occurred. Restarting subscription task after 5000 ms
2021-08-01 02:16:20.823 [container-3] ERROR org.springframework.data.redis.listener.RedisMessageListenerContainer 651 handleSubscriptionException - Connection failure occurred. Restarting subscription task after 5000 ms
2021-08-01 02:16:25.847 [container-4] ERROR org.springframework.data.redis.listener.RedisMessageListenerContainer 651 handleSubscriptionException - Connection failure occurred. Restarting subscription task after 5000 ms
2021-08-01 02:16:30.860 [container-5] ERROR org.springframework.data.redis.listener.RedisMessageListenerContainer 651 handleSubscriptionException - Connection failure occurred. Restarting subscription task after 5000 ms
以上为报错信息看到handleSubscriptionException。刚开始以为自己的发布订阅模式配置有问题,最后发现为密码错误,但是启动Application一直会提示重连,不会有其他的提示信息。
但是写一个简单的单元测试后会发现提示密码错误
ERR invalid password
Caused by: io.lettuce.core.RedisConnectionException: Unable to connect to 127.0.0.1:6379
at io.lettuce.core.RedisConnectionException.create(RedisConnectionException.java:78)
at io.lettuce.core.RedisConnectionException.create(RedisConnectionException.java:56)
at io.lettuce.core.AbstractRedisClient.getConnection(AbstractRedisClient.java:330)
at io.lettuce.core.RedisClient.connect(RedisClient.java:216)
Caused by: io.lettuce.core.RedisCommandExecutionException: ERR invalid password
at io.lettuce.core.internal.ExceptionFactory.createExecutionException(ExceptionFactory.java:137)
RedisTemplate 和 StringRedisTemplate
RedisTemplate
Springboot2后在Lettuce的redis客户端基础上进一步封装,于是就形成了RedisTemplate
StringRedisTemplate
StringRedisTemplate介绍
1、StringRedisTemplate继承RedisTemplate,是springboot中针对操作字符串类型数据推出的redis客户端工具 。
2、只能用于操作String类型。
3、StringRedisTemplate操作字符串数据结构的对象:
ValueOperations = stringRedisTemplate.opsForValue() 。
4、使用String序列化策略。
RedisTemplate和StringRedisTemplate数据不互通的根本原因是两者的序列化方式不一致。
分布式锁:
https://www.sevenyuan.cn/2020/04/04/redis/2020-04-04-annotation-redis-lock/#more
redis客户端连接,最大连接数查询与设置
info clients可以查看当前的redis连接数
Redis>info clients
{
"client_recent_max_output_buffer": 0,
"blocked_clients": 0,
"connected_clients": 134, //说明:客户端连接数
"client_recent_max_input_buffer": 2
}
config get maxclients 可以查询redis允许的最大连接数
127.0.0.1:6379> config get maxclients
1) "maxclients"
2) "10000"
127.0.0.1:6379>
1. 在2.6之后版本,可以修改最大连接数配置,默认10000,可以在redis.conf配置文件中修改
# maxclients 10000
2.config set maxclients num 可以设置redis允许的最大连接数
127.0.0.1:6379> CONFIG set maxclients 10
OK
127.0.0.1:6379>
3.启动redis.service服务时加参数--maxclients 100000来设置最大连接数限制
redis-server --maxclients 100000 -f /etc/redis.conf
查看明细
127.0.0.1:6379> client list
id=22 addr=127.0.0.1:1311 fd=12 name= age=56813 idle=0 flags=N db=0 sub=0 psub=0 multi=-1 qbuf=26 qbuf-free=32742 obl=0 oll=0 omem=0 events=r cmd=client
每个字段含义:
addr:客户端的地址和端口
fd:套接字所使用的文件描述符
idle:以秒计算的空闲时长
flags:客户端 flag
db:该客户端正在使用的数据库 ID
sub:已订阅频道的数量
psub:已订阅模式的数量
multi:在事务中被执行的命令数量
qbuf:查询缓冲区的长度(字节为单位, 0 表示没有分配查询缓冲区)
qbuf-free:查询缓冲区剩余空间的长度(字节为单位, 0 表示没有剩余空间)
obl:输出缓冲区的长度(字节为单位, 0 表示没有分配输出缓冲区)
oll:输出列表包含的对象数量(当输出缓冲区没有剩余空间时,命令回复会以字符串对象的形式被入队到这个队列里)
omem:输出缓冲区和输出列表占用的内存总量
events:文件描述符事件
cmd:最近一次执行的命令