Spring Boot 集成 Redis 哨兵模式

Redis环境

参考之前文章《CentOS 7 + Redis 实践哨兵模式一主两从

Spring Boot集成

创建项目

通过Spring Initializr 创建一个集成项目框架

项目名称
版本与插件

pom.xml

 <?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.7.6</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>com.kindey</groupId>
    <artifactId>redissen</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>redissen</name>
    <description>redissen</description>
    <properties>
        <java.version>8</java.version>
    </properties>
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-pool2</artifactId>
            <version>2.10.0</version>
        </dependency>
        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-lang3</artifactId>
        </dependency>
        <dependency>
            <groupId>cn.hutool</groupId>
            <artifactId>hutool-all</artifactId>
            <version>5.7.19</version>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
                <configuration>
                    <excludes>
                        <exclude>
                            <groupId>org.projectlombok</groupId>
                            <artifactId>lombok</artifactId>
                        </exclude>
                    </excludes>
                </configuration>
            </plugin>
        </plugins>
    </build>

</project>

application.yml

spring:
  data:
    redis:
      sentinel:
        database: 0
        password: Redis!23
        master: mymaster
        nodes: 192.168.88.96:26379,192.168.88.98:26379,192.168.88.156:26379
      lettuce:
        pool:
          max-idle: 10
          max-active: 20
          min-idle: 0
          max-wait: 10000

RedisConfig

package com.kindey.redissen.conf;

import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.pool2.impl.GenericObjectPoolConfig;
import cn.hutool.core.lang.Validator;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.connection.RedisNode;
import org.springframework.data.redis.connection.RedisSentinelConfiguration;
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.serializer.RedisSerializer;
import org.springframework.util.CollectionUtils;

import java.time.Duration;
import java.util.Arrays;
import java.util.List;

/**
 * 作用描述
 *
 * @author Kindey.S [kindey123@163.com]
 * @version 1.0
 * @date 2023/9/6
 */
@Configuration
@Slf4j
public class RedisConfig {
    @Value("${spring.data.redis.sentinel.database}")
    int database;
    @Value("${spring.data.redis.sentinel.password}")
    String password;
    @Value("${spring.data.redis.sentinel.master}")
    String master;
    @Value("${spring.data.redis.sentinel.nodes}")
    List<String> nodes;

    @Value("${spring.data.redis.lettuce.pool.max-idle}")
    int maxIdle;
    @Value("${spring.data.redis.lettuce.pool.max-active}")
    int maxActive;
    @Value("${spring.data.redis.lettuce.pool.min-idle}")
    int minIdle;
    @Value("${spring.data.redis.lettuce.pool.max-wait}")
    long maxWait;

    @Bean
    public RedisConnectionFactory lettuceConnectionFactory() {
        RedisSentinelConfiguration sentinelConfig = new RedisSentinelConfiguration();
        if (StringUtils.isEmpty(master)) {
            master = "mymaster";
        }
        sentinelConfig.setMaster(master);
        if (CollectionUtils.isEmpty(nodes)) {
            RedisNode redisNode = new RedisNode("127.0.0.1", 26379);
            sentinelConfig.addSentinel(redisNode);
        }
        for (String node : nodes) {
            String[] hostInfo = node.split(":");
            if (CollectionUtils.isEmpty(Arrays.asList(hostInfo))) {
                throw new RuntimeException("哨兵地址不能为空");
            } else if (hostInfo.length == 1) {
                if(!Validator.isIpv4(hostInfo[0])){
                    throw new RuntimeException("无效的哨兵IP:"+node);
                } else {
                    RedisNode redisNode = new RedisNode(hostInfo[0], 26379);
                    sentinelConfig.addSentinel(redisNode);
                }
            } else if (hostInfo.length == 2){
                RedisNode redisNode = new RedisNode(hostInfo[0], Integer.parseInt(hostInfo[1]));
                sentinelConfig.addSentinel(redisNode);
            } else {
                throw new RuntimeException("错误的哨兵连接地址:" + node);
            }
        }
        sentinelConfig.setDatabase(database);
        if (StringUtils.isNotEmpty(password)) {
            sentinelConfig.setPassword(password);
        }

        GenericObjectPoolConfig genericObjectPoolConfig = new GenericObjectPoolConfig();
        if (maxIdle == 0) {
            maxIdle = 10;
        }
        genericObjectPoolConfig.setMaxIdle(maxIdle);
        genericObjectPoolConfig.setMinIdle(minIdle);
        if (maxActive == 0) {
            maxActive = 20;
        }
        genericObjectPoolConfig.setMaxTotal(maxActive);
        if (maxWait == 0) {
            maxWait = 10 * 1000;
        }
        genericObjectPoolConfig.setMaxWaitMillis(maxWait);

        //redis客户端配置
        LettucePoolingClientConfiguration.LettucePoolingClientConfigurationBuilder builder =
                LettucePoolingClientConfiguration.builder().commandTimeout(Duration.ofMillis(maxWait));

        builder.shutdownTimeout(Duration.ofMillis(maxWait));
        builder.poolConfig(genericObjectPoolConfig);
        LettuceClientConfiguration clientConfig = builder.build();


        return new LettuceConnectionFactory(sentinelConfig, clientConfig);
    }

    @Bean
    public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
        // 将template 泛型设置为 <String, Object>
        RedisTemplate<String, Object> template = new RedisTemplate();
        // 连接工厂,不必修改
        template.setConnectionFactory(redisConnectionFactory);
        // key、hash的key 采用 String序列化方式
        template.setKeySerializer(RedisSerializer.string());
        template.setHashKeySerializer(RedisSerializer.string());
        // value、hash的value 采用 Jackson 序列化方式
        template.setValueSerializer(RedisSerializer.json());
        template.setHashValueSerializer(RedisSerializer.json());
        template.afterPropertiesSet();

        return template;
    }
}

TestService

package com.kindey.redissen.service;

import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Service;

/**
 * 作用描述
 *
 * @author Kindey.S [kindey123@163.com]
 * @version 1.0
 * @date 2023/9/6
 */
@Service
@Slf4j
public class TestService {
    @Autowired
    private RedisTemplate redisTemplate;

    @Scheduled(cron = "*/3 * * * * ?")
    public void getRedisData() {
        redisTemplate.opsForValue().set("bb","22");
        Object aa = redisTemplate.opsForValue().get("aa");
        log.info("aa:{}", null == aa ? null : aa.toString());
    }
}

测试结果

启动项目,可以看到,定时任务每3S访问一次redis。

2023-09-07 09:26:41.390  INFO 12284 --- [           main] w.s.c.ServletWebServerApplicationContext : Root WebApplicationContext: initialization completed in 1577 ms
2023-09-07 09:26:42.247  INFO 12284 --- [           main] o.s.b.w.embedded.tomcat.TomcatWebServer  : Tomcat started on port(s): 8080 (http) with context path ''
2023-09-07 09:26:42.263  INFO 12284 --- [           main] com.kindey.redissen.RedissenApplication  : Started RedissenApplication in 3.199 seconds (JVM running for 4.29)
2023-09-07 09:26:45.829  INFO 12284 --- [   scheduling-1] com.kindey.redissen.service.TestService  : aa:11
2023-09-07 09:26:48.013  INFO 12284 --- [   scheduling-1] com.kindey.redissen.service.TestService  : aa:11
2023-09-07 09:26:51.010  INFO 12284 --- [   scheduling-1] com.kindey.redissen.service.TestService  : aa:11

模拟master挂了,2023-09-07 09:27:57.947断连,2023-09-07 09:28:07.973恢复,中间间隔10s,原因是配置设置的命令超时时间为10s,如果想缩短此时间可以对配置信息spring.data.redis.lettuce.pool.max-wait进行调整,同时此时间还会受到sentinel的配置项sentinel down-after-milliseconds影响。

2023-09-07 09:27:57.947  INFO 12284 --- [xecutorLoop-1-3] i.l.core.protocol.ConnectionWatchdog     : Reconnecting, last destination was /192.168.88.96:6379
2023-09-07 09:28:07.021 ERROR 12284 --- [   scheduling-1] o.s.s.s.TaskUtils$LoggingErrorHandler    : Unexpected error occurred in scheduled task

org.springframework.dao.QueryTimeoutException: Redis command timed out; nested exception is io.lettuce.core.RedisCommandTimeoutException: Command timed out after 10 second(s)
    at org.springframework.data.redis.connection.lettuce.LettuceExceptionConverter.convert(LettuceExceptionConverter.java:70) ~[spring-data-redis-2.7.6.jar:2.7.6]
    at org.springframework.data.redis.connection.lettuce.LettuceExceptionConverter.convert(LettuceExceptionConverter.java:41) ~[spring-data-redis-2.7.6.jar:2.7.6]
    at org.springframework.data.redis.PassThroughExceptionTranslationStrategy.translate(PassThroughExceptionTranslationStrategy.java:44) ~[spring-data-redis-2.7.6.jar:2.7.6]
    at org.springframework.data.redis.FallbackExceptionTranslationStrategy.translate(FallbackExceptionTranslationStrategy.java:42) ~[spring-data-redis-2.7.6.jar:2.7.6]
    at org.springframework.data.redis.connection.lettuce.LettuceConnection.convertLettuceAccessException(LettuceConnection.java:278) ~[spring-data-redis-2.7.6.jar:2.7.6]
    at org.springframework.data.redis.connection.lettuce.LettuceConnection.await(LettuceConnection.java:1086) ~[spring-data-redis-2.7.6.jar:2.7.6]
    at org.springframework.data.redis.connection.lettuce.LettuceConnection.lambda$doInvoke$4(LettuceConnection.java:939) ~[spring-data-redis-2.7.6.jar:2.7.6]
    at org.springframework.data.redis.connection.lettuce.LettuceInvoker$Synchronizer.invoke(LettuceInvoker.java:673) ~[spring-data-redis-2.7.6.jar:2.7.6]
    at org.springframework.data.redis.connection.lettuce.LettuceInvoker$DefaultSingleInvocationSpec.get(LettuceInvoker.java:589) ~[spring-data-redis-2.7.6.jar:2.7.6]
    at org.springframework.data.redis.connection.lettuce.LettuceStringCommands.set(LettuceStringCommands.java:123) ~[spring-data-redis-2.7.6.jar:2.7.6]
    at org.springframework.data.redis.connection.DefaultedRedisConnection.set(DefaultedRedisConnection.java:314) ~[spring-data-redis-2.7.6.jar:2.7.6]
    at org.springframework.data.redis.core.DefaultValueOperations$7.inRedis(DefaultValueOperations.java:309) ~[spring-data-redis-2.7.6.jar:2.7.6]
    at org.springframework.data.redis.core.AbstractOperations$ValueDeserializingRedisCallback.doInRedis(AbstractOperations.java:61) ~[spring-data-redis-2.7.6.jar:2.7.6]
    at org.springframework.data.redis.core.RedisTemplate.execute(RedisTemplate.java:224) ~[spring-data-redis-2.7.6.jar:2.7.6]
    at org.springframework.data.redis.core.RedisTemplate.execute(RedisTemplate.java:191) ~[spring-data-redis-2.7.6.jar:2.7.6]
    at org.springframework.data.redis.core.AbstractOperations.execute(AbstractOperations.java:97) ~[spring-data-redis-2.7.6.jar:2.7.6]
    at org.springframework.data.redis.core.DefaultValueOperations.set(DefaultValueOperations.java:305) ~[spring-data-redis-2.7.6.jar:2.7.6]
    at com.kindey.redissen.service.TestService.getRedisData(TestService.java:24) ~[classes/:na]
    at sun.reflect.GeneratedMethodAccessor4.invoke(Unknown Source) ~[na:na]
    at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) ~[na:1.8.0_191]
    at java.lang.reflect.Method.invoke(Method.java:498) ~[na:1.8.0_191]
    at org.springframework.scheduling.support.ScheduledMethodRunnable.run(ScheduledMethodRunnable.java:84) ~[spring-context-5.3.24.jar:5.3.24]
    at org.springframework.scheduling.support.DelegatingErrorHandlingRunnable.run(DelegatingErrorHandlingRunnable.java:54) ~[spring-context-5.3.24.jar:5.3.24]
    at org.springframework.scheduling.concurrent.ReschedulingRunnable.run(ReschedulingRunnable.java:95) [spring-context-5.3.24.jar:5.3.24]
    at java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:511) [na:1.8.0_191]
    at java.util.concurrent.FutureTask.run$$$capture(FutureTask.java:266) [na:1.8.0_191]
    at java.util.concurrent.FutureTask.run(FutureTask.java) [na:1.8.0_191]
    at java.util.concurrent.ScheduledThreadPoolExecutor$ScheduledFutureTask.access$201(ScheduledThreadPoolExecutor.java:180) [na:1.8.0_191]
    at java.util.concurrent.ScheduledThreadPoolExecutor$ScheduledFutureTask.run(ScheduledThreadPoolExecutor.java:293) [na:1.8.0_191]
    at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149) [na:1.8.0_191]
    at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624) [na:1.8.0_191]
    at java.lang.Thread.run(Thread.java:748) [na:1.8.0_191]
Caused by: io.lettuce.core.RedisCommandTimeoutException: Command timed out after 10 second(s)
    at io.lettuce.core.internal.ExceptionFactory.createTimeoutException(ExceptionFactory.java:59) ~[lettuce-core-6.1.10.RELEASE.jar:6.1.10.RELEASE]
    at io.lettuce.core.protocol.CommandExpiryWriter.lambda$potentiallyExpire$0(CommandExpiryWriter.java:176) ~[lettuce-core-6.1.10.RELEASE.jar:6.1.10.RELEASE]
    at io.netty.util.concurrent.PromiseTask.runTask(PromiseTask.java:98) ~[netty-common-4.1.85.Final.jar:4.1.85.Final]
    at io.netty.util.concurrent.ScheduledFutureTask.run(ScheduledFutureTask.java:153) ~[netty-common-4.1.85.Final.jar:4.1.85.Final]
    at io.netty.util.concurrent.AbstractEventExecutor.runTask$$$capture(AbstractEventExecutor.java:174) ~[netty-common-4.1.85.Final.jar:4.1.85.Final]
    at io.netty.util.concurrent.AbstractEventExecutor.runTask(AbstractEventExecutor.java) ~[netty-common-4.1.85.Final.jar:4.1.85.Final]
    at io.netty.util.concurrent.DefaultEventExecutor.run(DefaultEventExecutor.java:66) ~[netty-common-4.1.85.Final.jar:4.1.85.Final]
    at io.netty.util.concurrent.SingleThreadEventExecutor$4.run(SingleThreadEventExecutor.java:997) ~[netty-common-4.1.85.Final.jar:4.1.85.Final]
    at io.netty.util.internal.ThreadExecutorMap$2.run(ThreadExecutorMap.java:74) ~[netty-common-4.1.85.Final.jar:4.1.85.Final]
    at io.netty.util.concurrent.FastThreadLocalRunnable.run(FastThreadLocalRunnable.java:30) ~[netty-common-4.1.85.Final.jar:4.1.85.Final]
    ... 1 common frames omitted

2023-09-07 09:28:07.956  WARN 12284 --- [ioEventLoop-6-3] io.lettuce.core.RedisClient              : Cannot connect Redis Sentinel at redis://192.168.88.96:26379?timeout=10s: java.util.concurrent.CompletionException: io.netty.channel.ConnectTimeoutException: connection timed out: /192.168.88.96:26379
2023-09-07 09:28:07.973  INFO 12284 --- [ioEventLoop-6-5] i.l.core.protocol.ReconnectionHandler    : Reconnected to 192.168.88.156:6379
2023-09-07 09:28:09.009  INFO 12284 --- [   scheduling-1] com.kindey.redissen.service.TestService  : aa:11
2023-09-07 09:28:12.004  INFO 12284 --- [   scheduling-1] com.kindey.redissen.service.TestService  : aa:11
2023-09-07 09:28:15.004  INFO 12284 --- [   scheduling-1] com.kindey.redissen.service.TestService  : aa:11
2023-09-07 09:28:18.006  INFO 12284 --- [   scheduling-1] com.kindey.redissen.service.TestService  : aa:11

项目代码

redissen

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,547评论 6 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,399评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,428评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,599评论 1 274
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,612评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,577评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,941评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,603评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,852评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,605评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,693评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,375评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,955评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,936评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,172评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,970评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,414评论 2 342

推荐阅读更多精彩内容