spring-boot-redis


记录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

默认使用JDK序列化策略

StringRedisTemplate

StringRedisTemplate介绍

1、StringRedisTemplate继承RedisTemplate,是springboot中针对操作字符串类型数据推出的redis客户端工具 。

2、只能用于操作String类型。

3、StringRedisTemplate操作字符串数据结构的对象:

ValueOperations = stringRedisTemplate.opsForValue() 。

4、使用String序列化策略。

StringRedisSerializer

image-20221002225716460

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:最近一次执行的命令


文章作者: WangQingLei
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 WangQingLei !
 上一篇
linux-grep linux-grep
grep用于分析一行信息,如果当中包含我们需要的信息,就会将该行拿出来。
2021-08-03
下一篇 
springEL springEL
为了更加灵活,Spring 还提供了表达式语言Spring EL 。通过Spring EL 可以拥有更为强大的运算规则来更好地装配Bean。
2021-07-28
  目录