集群搭建(redis7)

news2024/10/7 6:40:12

一、主从复制(replica)(不推荐)

介绍

  • 主从复制
    • mmaster以写为主,slave以读为主
    • 当master数据变化时,自动将新的数据异步同步到其他slave数据库
  • 读写分离
  • down机恢复
  • 数据备份
  • 水平扩容支撑高并发

基本操作

  • 配从不配主
    • 权限细节
      • master如果配置了 requirepass 参数,需要密码登录
      • slave 需要配置 masterauth来设置检验密码,否则的话master会拒绝slave的访问请求

基本操作命令

info replication   查看复制节点的主从关系和配置信息
    
replicaof/slaveof 主库IP 主库端口   replicaof/slaveof这两个一样,一般写入进redis.conf配置文件内,在运行期间修改slave节点的信息,如果该数据库已经某个数据库的从数据库,那么会停止和原主数据库的同步关系转而和新的主数据库同步

replicaof/slaveof no one      使当前数据库停止与其他数据库的同步,升级为主数据库

配置一个master,两个slave

当前环境是在同一个ip下不同端口配置

image.png
外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传
image.png
image.png

主master6001.conf
#1 Redis默认不是以守护进程的方式运行,可以通过该配置项修改,使用yes启用守护进程
daemonize yes
#2 注释只能本地连接
#bind 127.0.0.1
#3 默认开启保护模式,如果没有设置密码或者没有 bind 配置,我只允许在本机连接我,其它机器无法连接。
protected-mode no
port 6001
dir /redis-learn/redis-7.0.9/conf/replica/6001
pidfile redis_6001.pid
logfile  "6001.log"
requirepass xgm@2023
dbfilename dump6001.rdb
#开启aof持久增量存储
appendonly yes
appendfilename "appendonly6001.aof"


从slave16002.conf(拜大哥主机)
#1 Redis默认不是以守护进程的方式运行,可以通过该配置项修改,使用yes启用守护进程
daemonize yes
#2 注释只能本地连接
#bind 127.0.0.1
#3 默认开启保护模式,如果没有设置密码或者没有 bind 配置,我只允许在本机连接我,其它机器无法连接。
protected-mode no
port 6002
dir /redis-learn/redis-7.0.9/conf/replica/6002
pidfile redis_6001.pid
logfile  "6002.log"
requirepass xgm@2023
dbfilename dump6001.rdb
#开启aof持久增量存储
appendonly yes
appendfilename "appendonly6002.aof"
#主从复制,主机ip端口
replicaof 172.16.64.21  6001
#主机密码
masterauth xgm@2023

从slave26003.conf(拜大哥主机)
#1 Redis默认不是以守护进程的方式运行,可以通过该配置项修改,使用yes启用守护进程
daemonize yes
#2 注释只能本地连接
#bind 127.0.0.1
#3 默认开启保护模式,如果没有设置密码或者没有 bind 配置,我只允许在本机连接我,其它机器无法连接。
protected-mode no
port 6003
dir /redis-learn/redis-7.0.9/conf/replica/6003
pidfile redis_6003.pid
logfile  "6003.log"
requirepass xgm@2023
dbfilename dump6003.rdb
#开启aof持久增量存储
appendonly yes
appendfilename "appendonly6003.aof"
#主从复制,主机ip端口
replicaof 172.16.64.21  6001
#主机密码
masterauth xgm@2023


注意防火墙配置

启动: systemctl start firewalld
关闭: systemctl stop firewalld
查看状态: systemctl status firewalld
开机禁用 : systemctl disable firewalld
开机启用 : systemctl enable firewalld

添加 :firewall-cmd --zone=public --add-port=80/tcp --permanent (–permanent永久生效,没有此参数重启后失效)
重新载入: firewall-cmd --reload
查看: firewall-cmd --zone= public --query-port=80/tcp
删除: firewall-cmd --zone= public --remove-port=80/tcp --permanent

启动

./redis-server …/conf/replica/redis-masterr-6001.conf
./redis-server …/conf/replica/redis-slaver-6002.conf
./redis-server …/conf/replica/redis-slaver-6003.conf

查看主从关系

info replication

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

日志查看

主机
image.png
从机
image.png

复制原理

slave启动,同步初请

  • slave启动成功连接到master后会发送一个sync命令
  • slave首次全新连接master,一次完全同步(全量复制)将被自动执行,slave自身原有数据会被master数据覆盖清除

首次连接,全量复制

  • master节点收到sync命令后会在后台开始保存快照(即RDB持久化,主从复制会触发RDB),同时收集所有接收到的用于修改数据集命令缓存起来,master节点执行RDB持久化后,master将rdb快照文件和缓存的命令发送到所有slave,已完成一次完全同步
  • 而slave服务在接收到数据库文件数据后,将其存盘并加载到内存中,从而完成复制初始化

心跳持续,保持通信

  • repl-ping-replica-period 10
  • master发出PING包的周期,默认是10秒

进入平稳,增量复制

  • master 继续将新的所有收集到的修改命令自动一次传给slave,完成同步

从机下线,重连续传

  • master 会检查backlog里面的offset,master和slave都会保存一个复制的offset怀有一个masterId
  • offset 是保存在backlog 中的。master只会把已经复制的offset后面的数据赋值给slave,类似断电续传

缺点

复制延时,信号衰减

由于所有的写操作都是先在Master上操作,然后同步更新到Slave上,所以从Master同步到Slave机器有一定的延迟,当系统很繁忙的时候,延迟问题会更加严重,Slave机器数量的增加也会使这个问题更加严重。

master挂了

  1. 默认情况下不会在slave节点自动重选一个master
  2. 需要人工干预

image.png

二、哨兵(sentinel)(不推荐)

介绍

  • 哨兵巡查监控后台master主机是否故障,如果故障了根据投票数自动将某一个从库转换为新主库,继续对外服务,俗称无人值守运维,不存放数据只是吹哨人

作用

  • 监控redis运行状态,包括master和slave
  • 当master down机,能自动将slave切换成新master

哨兵的四个功能

主从监控
监控主从redis库运行是否正常
消息通知
哨兵可以将故障转移的结果发送到客户端
故障转移
如果master异常,则会进行主从切换,将其中一个slave作为新master
配置中心
客户端通过连接哨兵来获得当前Redis服务的主节点地址

搭建

image.png

redis配置成奇数,好投票
哨兵本身也要配置集群,不然也是单点故障

哨兵6004 sentinel.conf

bind 0.0.0.0
logfile "/redis-learn/redis-7.0.9/conf/sentinel/6004/6004.log"
pidfile /redis-learn/redis-7.0.9/conf/sentinel/6004/redis-sentinel-6004.pid
dir /redis-learn/redis-7.0.9/conf/sentinel/6004/
protected-mode no
daemonize no
port 6004
#设置要监控的redis master,2表示最少有几个哨兵认可客观下线,同意故障迁移的法定票数
sentinel monitor mymaster 172.16.64.21 6001 2
#sentinel访问master密码
sentinel auth-pass mymaster xgm@2023

哨兵6005 sentinel.conf

bind 0.0.0.0
logfile "/redis-learn/redis-7.0.9/conf/sentinel/6005/6005.log"
pidfile /redis-learn/redis-7.0.9/conf/sentinel/6005/redis-sentinel-6005.pid
dir /redis-learn/redis-7.0.9/conf/sentinel/6005/
protected-mode no
daemonize no
port 6005
#设置要监控的redis master,2表示最少有几个哨兵认可客观下线,同意故障迁移的法定票数
sentinel monitor mymaster 172.16.64.21 6001 2
#sentinel访问master密码
sentinel auth-pass mymaster xgm@2023

哨兵6006 sentinel.conf

bind 0.0.0.0
logfile "/redis-learn/redis-7.0.9/conf/sentinel/6006/6006.log"
pidfile /redis-learn/redis-7.0.9/conf/sentinel/6006/redis-sentinel-6006.pid
dir /redis-learn/redis-7.0.9/conf/sentinel/6006/
protected-mode no
daemonize no
port 6006
#设置要监控的redis master,2表示最少有几个哨兵认可客观下线,同意故障迁移的法定票数
sentinel monitor mymaster 172.16.64.21 6001 2
#sentinel访问master密码
sentinel auth-pass mymaster xgm@2023

image.png

主从复制一主2从

当前主从复制和上面搭建的一样,除了主机也要配置masterauth xgm@2023,因为6001可能挂机后变成从机

修改6001.conf

masterauth xgm@2023

启动主从复制集群

./redis-server …/conf/replica/redis-master-6001.conf
./redis-server …/conf/replica/redis-slaver-6002.conf
./redis-server …/conf/replica/redis-slaver-6003.conf

启动三个哨兵

./redis-sentinel …/conf/sentinel/sentinel6004.conf --sentinel &
./redis-sentinel …/conf/sentinel/sentinel6005.conf --sentinel &
./redis-sentinel …/conf/sentinel/sentinel6006.conf --sentinel &

故障迁移演示

查看集群关系,此时6001是master
外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传
关闭master 6001
image.png
image.png
结论

6002变为mster,数据不会丢失

重启6001
外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传
结论

6001不能切换为主master,还是之前的6002为master

哨兵选举原理

当一个主从配置中的master失效之后,sentinel可以选举出一个新的master,用于接替原master的工作,主从配置中其他redis服务器自动指向新的master同步数据。一般建议sentinel采用奇数台,防止某一台sentinel无法连接到master导致误切换、

image.png
image.png

SDOWN主观下线

  • SDOWN 是单个sentinel 自己主观上检测到的关于master的状态,从sentinel的角度来看,如果发送了PING心跳后,在一定时间内没有收到合法的回复,就达到了SDOWN的条件
  • sentinel配置文件中的down-after-milliseconds 设置了判断主观下线的时间长度

image.png

ODOWN客观下线

ODOWN需要一定数量的sentinel,多个哨兵达成一致意见才能认为一个master客观上已经宕机

image.png

选举出领导者哨兵
  • 当主节点被判断客观下线以后,各个哨兵节点会进行协商,县选举出一个领导者哨兵节点并由该领导者节点进行failover(故障迁移)
  • Raft算法 选出领导者节点

image.png

  • 由领导者节点开始推动故障切换并选出一个新master

    • 新主登基
      • 某个slave 备选成为新 master
    • 群臣俯首
      • 一朝天子一朝臣,重新认老大
    • 旧主拜服
      • 老master回来也得怂
  • 以上的failover都是sentinel自己独立完成,完全无需人工干预

使用建议
  • 哨兵节点的数量应为多个,哨兵本身应该集群,保证高可用
  • 哨兵节点的数量应该是奇数个
  • 各个哨兵节点的配置应该一致
  • 如果哨兵节点部署在Docker等容器里,要注意端口的正确映射
  • 哨兵集群+主从复制,并不能保证数据零丢失

springboot使用

踩坑:

注意
redis.conf配置文件中的replicaof 具体ip而不是127.0.0.1,不然哨兵连接报错
sentinel.conf中的mymaster主机IP也要具体指定

读写分离配置

pom文件
<?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.17</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>com.redis</groupId>
    <artifactId>redis-sentinel</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>redis-sentinel</name>
    <description>redis-sentinel</description>
    <properties>
        <java.version>1.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.springframework.boot</groupId>
            <artifactId>spring-boot-devtools</artifactId>
            <scope>runtime</scope>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>com.mysql</groupId>
            <artifactId>mysql-connector-j</artifactId>
            <scope>runtime</scope>
        </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>
        </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>

配置文件
#连接数据源
spring.datasource.druid.username=root
spring.datasource.druid.password=xgm@2023..
spring.datasource.druid.url=jdbc:mysql://172.16.204.51:3306/redis?serverTimezone=GMT%2B8
spring.datasource.druid.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.druid.initial-size=5

#redis哨兵模式
spring.redis.sentinel.master=mymaster
#哨兵集群
spring.redis.sentinel.nodes=172.16.64.21:6004,172.16.64.21:6005,172.16.64.21:6006
spring.redis.database=0
spring.redis.password=xgm@2023
spring.redis.timeout=3000ms
#默认的lettuce ,lettuce线程安全,Jedis是同步的,不支持异步,Jedis客户端实例不是线程安全的,需要每个线程一个Jedis实例,所以一般通过连接池来使用Jedis.
# 连接池最大连接数(使用负值表示没有限制)
spring.redis.lettuce.pool.max-active=100
# 连接池中的最大空闲连接
spring.redis.lettuce.pool.max-idle=100
# 连接池最大阻塞等待时间(使用负值表示没有限制)
spring.redis.lettuce.pool.max-wait=1000ms
# 连接池中的最小空闲连接
spring.redis.lettuce.pool.min-idle=1
spring.redis.lettuce.shutdown-timeout=1000ms


#日志
logging.pattern.console='%date{yyyy-MM-dd HH:mm:ss.SSS} | %highlight(%5level) [%green(%16.16thread)] %clr(%-50.50logger{49}){cyan} %4line -| %highlight(%msg%n)'
logging.level.root=info
logging.level.io.lettuce.core=debug
logging.level.org.springframework.data.redis=debug
读写分离配置
package com.redis.redissentinel.conf;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import io.lettuce.core.ReadFrom;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.data.redis.RedisProperties;
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.RedisPassword;
import org.springframework.data.redis.connection.RedisSentinelConfiguration;
import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory;
import org.springframework.data.redis.connection.lettuce.LettucePoolingClientConfiguration;
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;
import java.text.SimpleDateFormat;
import java.util.Arrays;
import java.util.HashSet;
import java.util.TimeZone;
import org.springframework.data.redis.core.RedisTemplate;
/**
 * @author ygr
 * @date 2022-02-15 16:30
 */
@Slf4j
@Configuration
public class RedisConfig {

    public ObjectMapper objectMapper() {
        ObjectMapper objectMapper = new ObjectMapper();
        objectMapper.setTimeZone(TimeZone.getTimeZone("GMT+8"));
        objectMapper.configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false);
        objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
        objectMapper.setDateFormat(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"));
        objectMapper.configure(JsonParser.Feature.ALLOW_SINGLE_QUOTES, true);
        return objectMapper;
    }

    @Bean
    @ConditionalOnMissingBean
    public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {
        // 创建RedisTemplate<String, Object>对象
        RedisTemplate<String, Object> template = new RedisTemplate<>();
        template.setConnectionFactory(factory);

        // 定义Jackson2JsonRedisSerializer序列化对象
        Jackson2JsonRedisSerializer<Object> jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer<>(Object.class);
        jackson2JsonRedisSerializer.setObjectMapper(objectMapper());

        StringRedisSerializer stringSerial = new StringRedisSerializer();
        // redis key 序列化方式使用stringSerial
        template.setKeySerializer(stringSerial);
        // redis value 序列化方式使用jackson
        template.setValueSerializer(jackson2JsonRedisSerializer);
        // redis hash key 序列化方式使用stringSerial
        template.setHashKeySerializer(stringSerial);
        // redis hash value 序列化方式使用jackson
        template.setHashValueSerializer(jackson2JsonRedisSerializer);

        template.afterPropertiesSet();
        return template;
    }
    @Bean
    public RedisConnectionFactory lettuceConnectionFactory(RedisProperties redisProperties) {
        RedisSentinelConfiguration sentinelConfig = new RedisSentinelConfiguration()
                .master(redisProperties.getSentinel().getMaster());
        redisProperties.getSentinel().getNodes().forEach(s -> {
            String[] arr = s.split(":");
            sentinelConfig.sentinel(arr[0],Integer.parseInt(arr[1]));
        });
        LettucePoolingClientConfiguration lettuceClientConfiguration = LettucePoolingClientConfiguration.builder()
                // 读写分离,若主节点能抗住读写并发,则不需要设置,全都走主节点即可
                //ANY 从任何节点读取,NEAREST 从最近节点读取,MASTER_PREFERRED / UPSTREAM_PREFERRED优先读取主节点,如果主节点不可用,则读取从节点,MASTER / UPSTREAM仅读取主节点
                .readFrom(ReadFrom.ANY_REPLICA)
                .build();
        sentinelConfig.setPassword(RedisPassword.of(redisProperties.getPassword()));
        sentinelConfig.setDatabase(redisProperties.getDatabase());
        return new LettuceConnectionFactory(sentinelConfig, lettuceClientConfiguration);
    }

}
测试
package com.redis.redissentinel;

import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.data.redis.core.RedisTemplate;

import javax.annotation.Resource;
import java.util.concurrent.TimeUnit;

@Slf4j
@SpringBootTest
class RedisSentinelApplicationTests {

    @Resource
    private RedisTemplate<String, Object> redisTemplate;


    @Test
    void witeTest() {
        for (int i = 0; i < 3; i++) {
            try {
                redisTemplate.opsForValue().set("k" + i, "v" + i);
                log.info("set value success: {}", i);

                Object val = redisTemplate.opsForValue().get("k" + i);
                log.info("get value success: {}", val);
                TimeUnit.SECONDS.sleep(1);
            } catch (Exception e) {
                log.error("error: {}", e.getMessage());
            }
        }
        log.info("finished...");
    }

    @Test
    void readTest() {

        Object k1 = redisTemplate.opsForValue().get("k1");
        log.info("读取节点k1的值:{}",k1);

    }

}

image.png

image.png

踩坑指南

1 redis哨兵相关配置指定具体的ip,不要写127.0.0.1
1>可以写进/etc/hosts ;比如 172.16.64.21 redis6001
2>如果是多个服务器地址,相互在hots指定
2 配置安全线程池必须在pom文件引入commons-pool2这个包
3 在redisconfig配置中必须设置sentinelConfig.setPassword(RedisPassword.of(redisProperties.getPassword()));
不然会报错
NOAUTH HELLO must be called with the client already authenticated, otherwise the HELLO AUTH

三、cluster(重点/推荐)

官网介绍

https://redis.io/docs/reference/cluster-spec/

image.png

架构图

image.png

1 cluster主节点挂了,slave顶替上来,自己挂了不影响其他主节点的使用
2 redis集群支持多个master,每个master又可以挂在多个slave
3 cluster自带sentinel故障转移机制,内置了高可用支持,无需再去使用哨兵功能
4 客户端与redis的节点连接,不再需要连接集群中所有的节点,只需要任意连接集群中的一个可用节点即可
5 槽位slot负责分配到各个物理服务节点,由对应集群来负责维护节点、插槽和数据之间的关系

架构设计原理

image.png

16384个slot卡槽,16384主节点,但建议最大主节点不要超过1000

redis集群槽位图

image.png

redis集群优点

1 方便扩缩容和数据分派查找;
扩缩容节点的移动并不会停止服务,改变节点哈希槽的数量都不会造成集群不可用的状态

slot槽位映射

哈希槽取余(不推荐)

hash(key)% 机器数;

适用场景
小厂/公司

不会变动机器数量

优点
简单粗暴,直接有效;起到负载均衡+分而治之的作用
缺点

进行扩容和缩容比较麻烦,映射关系重新计算,宕机会导致hash取余全部数据重新洗牌,原来的值存在服务器但获取不到,也就是数据丢失(虽然存在服务器,但找不到)

一致性哈希算法分区(不推荐)

image.png

ip节点映射和 落键规则
image.png
hash(key)% 2^32-1;

适用场景
中小公司
优点
具备容错性和扩张性
   加入和删除节点只影响哈希环中顺时针方向相邻的节点,对其他节点无影响
缺点

数据倾斜,头重脚轻,分布不均匀

哈希槽分区(16384卡槽,推荐)

0~2^14-1 ;16384
哈希槽实质就是一个数组,数组[0,16383]形成的hash slot
CRC16(key) % 16384

适用场景
大厂
优点
均匀分布,负载均衡
缺点
源码
    public static void main(String[] args) {
        //哈希槽,大小SlotHash.SLOT_COUNT=16384,HashMap
        // end = CRC16.crc16(key.array(), key.position(), key.limit() - key.position()) % 16384;
        int a = SlotHash.getSlot("A");//6373
        int b = SlotHash.getSlot("B");//10374
        int w = SlotHash.getSlot("W");//10770
        int end = SlotHash.getSlot("艾弗森大苏打的幅度萨芬热微软微软微软464666sswwwvvvbbssssss");//5161
        System.out.println(a);
        System.out.println(b);
        System.out.println(w);
        System.out.println(end);
    }
//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by FernFlower decompiler)
//

package io.lettuce.core.cluster;

import io.lettuce.core.codec.CRC16;
import io.lettuce.core.codec.RedisCodec;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;

public class SlotHash {
    public static final byte SUBKEY_START = 123;
    public static final byte SUBKEY_END = 125;
    public static final int SLOT_COUNT = 16384;

    private SlotHash() {
    }

    public static final int getSlot(String key) {
        return getSlot(key.getBytes());
    }

    public static int getSlot(byte[] key) {
        return getSlot(ByteBuffer.wrap(key));
    }

    public static int getSlot(ByteBuffer key) {
        int limit = key.limit();
        int position = key.position();
        int start = indexOf(key, (byte)123);
        int end;
        if (start != -1) {
            end = indexOf(key, start + 1, (byte)125);
            if (end != -1 && end != start + 1) {
                key.position(start + 1).limit(end);
            }
        }

        try {
            if (key.hasArray()) {
                end = CRC16.crc16(key.array(), key.position(), key.limit() - key.position()) % 16384;
                return end;
            }

            end = CRC16.crc16(key) % 16384;
        } finally {
            key.position(position).limit(limit);
        }

        return end;
    }

    private static int indexOf(ByteBuffer haystack, byte needle) {
        return indexOf(haystack, haystack.position(), needle);
    }

    private static int indexOf(ByteBuffer haystack, int start, byte needle) {
        for(int i = start; i < haystack.remaining(); ++i) {
            if (haystack.get(i) == needle) {
                return i;
            }
        }

        return -1;
    }

    static <K, V> Map<Integer, List<K>> partition(RedisCodec<K, V> codec, Iterable<K> keys) {
        Map<Integer, List<K>> partitioned = new HashMap();
        Iterator var3 = keys.iterator();

        while(var3.hasNext()) {
            K key = var3.next();
            int slot = getSlot(codec.encodeKey(key));
            if (!partitioned.containsKey(slot)) {
                partitioned.put(slot, new ArrayList());
            }

            Collection<K> list = (Collection)partitioned.get(slot);
            list.add(key);
        }

        return partitioned;
    }

    static <K> Map<K, Integer> getSlots(Map<Integer, ? extends Iterable<K>> partitioned) {
        Map<K, Integer> result = new HashMap();
        Iterator var2 = partitioned.entrySet().iterator();

        while(var2.hasNext()) {
            Map.Entry<Integer, ? extends Iterable<K>> entry = (Map.Entry)var2.next();
            Iterator var4 = ((Iterable)entry.getValue()).iterator();

            while(var4.hasNext()) {
                K key = var4.next();
                result.put(key, entry.getKey());
            }
        }

        return result;
    }
}

//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by FernFlower decompiler)
//

package io.lettuce.core.codec;

import java.nio.ByteBuffer;

public class CRC16 {
    private static final int[] LOOKUP_TABLE = new int[]{0, 4129, 8258, 12387, 16516, 20645, 24774, 28903, 33032, 37161, 41290, 45419, 49548, 53677, 57806, 61935, 4657, 528, 12915, 8786, 21173, 17044, 29431, 25302, 37689, 33560, 45947, 41818, 54205, 50076, 62463, 58334, 9314, 13379, 1056, 5121, 25830, 29895, 17572, 21637, 42346, 46411, 34088, 38153, 58862, 62927, 50604, 54669, 13907, 9842, 5649, 1584, 30423, 26358, 22165, 18100, 46939, 42874, 38681, 34616, 63455, 59390, 55197, 51132, 18628, 22757, 26758, 30887, 2112, 6241, 10242, 14371, 51660, 55789, 59790, 63919, 35144, 39273, 43274, 47403, 23285, 19156, 31415, 27286, 6769, 2640, 14899, 10770, 56317, 52188, 64447, 60318, 39801, 35672, 47931, 43802, 27814, 31879, 19684, 23749, 11298, 15363, 3168, 7233, 60846, 64911, 52716, 56781, 44330, 48395, 36200, 40265, 32407, 28342, 24277, 20212, 15891, 11826, 7761, 3696, 65439, 61374, 57309, 53244, 48923, 44858, 40793, 36728, 37256, 33193, 45514, 41451, 53516, 49453, 61774, 57711, 4224, 161, 12482, 8419, 20484, 16421, 28742, 24679, 33721, 37784, 41979, 46042, 49981, 54044, 58239, 62302, 689, 4752, 8947, 13010, 16949, 21012, 25207, 29270, 46570, 42443, 38312, 34185, 62830, 58703, 54572, 50445, 13538, 9411, 5280, 1153, 29798, 25671, 21540, 17413, 42971, 47098, 34713, 38840, 59231, 63358, 50973, 55100, 9939, 14066, 1681, 5808, 26199, 30326, 17941, 22068, 55628, 51565, 63758, 59695, 39368, 35305, 47498, 43435, 22596, 18533, 30726, 26663, 6336, 2273, 14466, 10403, 52093, 56156, 60223, 64286, 35833, 39896, 43963, 48026, 19061, 23124, 27191, 31254, 2801, 6864, 10931, 14994, 64814, 60687, 56684, 52557, 48554, 44427, 40424, 36297, 31782, 27655, 23652, 19525, 15522, 11395, 7392, 3265, 61215, 65342, 53085, 57212, 44955, 49082, 36825, 40952, 28183, 32310, 20053, 24180, 11923, 16050, 3793, 7920};

    private CRC16() {
    }

    public static int crc16(byte[] bytes) {
        return crc16(bytes, 0, bytes.length);
    }

    public static int crc16(byte[] bytes, int off, int len) {
        int crc = 0;
        int end = off + len;

        for(int i = off; i < end; ++i) {
            crc = doCrc(bytes[i], crc);
        }

        return crc & '\uffff';
    }

    public static int crc16(ByteBuffer bytes) {
        int crc;
        for(crc = 0; bytes.hasRemaining(); crc = doCrc(bytes.get(), crc)) {
        }

        return crc & '\uffff';
    }

    private static int doCrc(byte b, int crc) {
        return crc << 8 ^ LOOKUP_TABLE[(crc >>> 8 ^ b & 255) & 255];
    }
}

面试题为啥是16384个槽位

1 官网规定的16384
2 CRC16算法产生hash值有16bit=2^16=65536,太大压缩位图较难,信息传递减弱,网络拥堵

image.png
image.png

集群配置(三主三从)

配置文件

6007、6008、6009、6012、6014、6015 6个端口服务配置分别如下:

image.png

bind 0.0.0.0
daemonize yes
protected-mode no
port 6007
logfile "cluster6007.log"
pidfile /redis-learn/redis-7.0.9/conf/cluster/cluster6007.pid
dir /redis-learn/redis-7.0.9/conf/cluster/cluster6007
dbfilename dump6007.rdb
appendonly yes
appendfilename "appendonly6007.aof"
requirepass 123456
masterauth 123456
 
cluster-enabled yes
cluster-config-file nodes-6007.conf
cluster-node-timeout 5000

bind 0.0.0.0
daemonize yes
protected-mode no
port 6008
logfile "cluster6008.log"
pidfile /redis-learn/redis-7.0.9/conf/cluster/cluster6008.pid
dir /redis-learn/redis-7.0.9/conf/cluster/cluster6008
dbfilename dump6008.rdb
appendonly yes
appendfilename "appendonly6008.aof"
requirepass 123456
masterauth 123456
 
cluster-enabled yes
cluster-config-file nodes-6008.conf
cluster-node-timeout 5000

bind 0.0.0.0
daemonize yes
protected-mode no
port 6009
logfile "cluster6009.log"
pidfile /redis-learn/redis-7.0.9/conf/cluster/cluster6009.pid
dir /redis-learn/redis-7.0.9/conf/cluster/cluster6009
dbfilename dump6009.rdb
appendonly yes
appendfilename "appendonly6009.aof"
requirepass 123456
masterauth 123456
 
cluster-enabled yes
cluster-config-file nodes-6009.conf
cluster-node-timeout 5000

bind 0.0.0.0
daemonize yes
protected-mode no
port 6012
logfile "cluster6012.log"
pidfile /redis-learn/redis-7.0.9/conf/cluster/cluster6012.pid
dir /redis-learn/redis-7.0.9/conf/cluster/cluster6012
dbfilename dump6012.rdb
appendonly yes
appendfilename "appendonly6012.aof"
requirepass 123456
masterauth 123456
 
cluster-enabled yes
cluster-config-file nodes-6012.conf
cluster-node-timeout 5000

bind 0.0.0.0
daemonize yes
protected-mode no
port 6014
logfile "cluster6014.log"
pidfile /redis-learn/redis-7.0.9/conf/cluster/cluster6014.pid
dir /redis-learn/redis-7.0.9/conf/cluster/cluster6014
dbfilename dump6014.rdb
appendonly yes
appendfilename "appendonly6014.aof"
requirepass 123456
masterauth 123456
 
cluster-enabled yes
cluster-config-file nodes-6014.conf
cluster-node-timeout 5000

bind 0.0.0.0
daemonize yes
protected-mode no
port 6015
logfile "cluster6012.log"
pidfile /redis-learn/redis-7.0.9/conf/cluster/cluster6015.pid
dir /redis-learn/redis-7.0.9/conf/cluster/cluster6015
dbfilename dump6015.rdb
appendonly yes
appendfilename "appendonly6015.aof"
requirepass 123456
masterauth 123456
 
cluster-enabled yes
cluster-config-file nodes-6015.conf
cluster-node-timeout 5000

启动

./redis-server …/conf/cluster/cluster6007.conf
./redis-server …/conf/cluster/cluster6008.conf
./redis-server …/conf/cluster/cluster6009.conf
./redis-server …/conf/cluster/cluster6010.conf
./redis-server …/conf/cluster/cluster6011.conf
./redis-server …/conf/cluster/cluster6012.conf

通过redis-cli命令为6台机器构建集群关系

#-cluster-replicas 1 表示为每个master创建一个slave节点
redis-cli -a 123456 --cluster create --cluster-replicas 1 172.16.64.21:6007 172.16.64.21:6008 172.16.64.21:6009 172.16.64.21:6012 172.16.64.21:6014 172.16.64.21:6015

image.png
连接任意一个节点查看集群状态

#-c表示集群 不加的话不是按照集群启动的,对于在别的机器上的key,会报错
./redis-cli -a 123456 -p 6007 -c

查看

#查看节点信息
cluster nodes
#查看集群信息
cluster info
#查看当前节点主从关系
info replication

image.png

测试

新增key查看是否成功

image.png

主从容错切换迁移

找出一个主从测试即可,目前6007是6015的主节点
image.png

关闭6007查看6015会不会切换成master
image.png

启动6007看6015会不会让位
image.png

Redis集群不保证强一致性,意味着在特定的条件下,Redis集群可能会丢掉一些被系统收到的写入请求命令因为本质还是发送心跳包,需要一些时间判断是否down机,如果down机,对应的slave直接成为master如果想要原先的master继续做master的话

CLUSTER FAILOVER # 让谁上位 就在谁的端口号下执行这个命令

主从扩容

增加6016 6017两台服务

bind 0.0.0.0
daemonize yes
protected-mode no
port 6016
logfile "cluster6016.log"
pidfile /redis-learn/redis-7.0.9/conf/cluster/cluster6016.pid
dir /redis-learn/redis-7.0.9/conf/cluster/cluster6016
dbfilename dump6016.rdb
appendonly yes
appendfilename "appendonly6016.aof"
requirepass 123456
masterauth 123456
 
cluster-enabled yes
cluster-config-file nodes-6016.conf
cluster-node-timeout 5000

bind 0.0.0.0
daemonize yes
protected-mode no
port 6017
logfile "cluster6017.log"
pidfile /redis-learn/redis-7.0.9/conf/cluster/cluster6017.pid
dir /redis-learn/redis-7.0.9/conf/cluster/cluster6017
dbfilename dump6017.rdb
appendonly yes
appendfilename "appendonly6017.aof"
requirepass 123456
masterauth 123456
 
cluster-enabled yes
cluster-config-file nodes-6017.conf
cluster-node-timeout 5000

启动,此时这两个实例都是master

./redis-server …/conf/cluster/cluster6016.conf
./redis-server …/conf/cluster/cluster6017.conf

将新增6016、6017加入原来集群中

#cluster add-node 新节点ip:port 原来节点ip:port
./redis-cli -a 123456 --cluster add-node 172.16.64.21:6016 172.16.64.21:6007

image.png

检查集群情况,6016

./redis-cli -a 123456 --cluster check 172.16.64.21:6016

image.png

给6016分派卡槽,从其他的服务中均一点

./redis-cli -a 123456 --cluster reshard 172.16.64.21:6016

image.png

上述解释

  1. all:集群中的所有主节点都会成为源节点,redis-trib从各个源节点中各取出一部分哈希槽,凑够4096个,然后移动到6016节点上
  2. done :要从特点的哪个节点中取出 4096 个哈希槽
再次检查集群情况

./redis-cli -a 123456 --cluster check 172.16.64.21:6016

image.png

为主节点6016分配从节点6017 –cluster-master-id 后跟的是6016的id

redis-cli -a 123456 --cluster add-node 172.16.64.21:6017 172.16.64.21:6016 --cluster-slave --cluster-master-id 6ffe226dec047ac3a2e1f1be054d1ffd91007a79

image.png

主从缩容

让6016、6017下线

#卡槽有数据不能删除,需要还给集群才能删除,当前6017卡槽为0
./redis-cli -a 123456 --cluster del-node 172.16.64.21:6017 6017id号

image.png

将6016的槽号情况,重新分配,先全部都给6007

./redis-cli -a 123456 --cluster reshard 172.16.64.21:6016

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

查看集群情况

image.png

集群删除6016

#卡槽有数据不能删除,需要还给集群才能删除,当前6017卡槽为0
./redis-cli -a 123456 --cluster del-node 172.16.64.21:6016 6016id号

image.png

完整提供服务配置

image.png

springboot集成集群

lettuce和jedis区别
jedis:Jedis Client 是Redis 官网推荐的一个面向 Java 客户端,库文件实现了对各类API进行封装调用
lettuce: Lettuce是一个Redis的Java驱动包,Lettuce翻译为生菜,没错,就是吃的那种生菜,所以它的Logo就是生菜
区别:spingboot2.0后默认是用lettuce连接redis服务器,jedis反复连接线程资源创建和关闭,开销大,线程不安全
lettuce底层使用netty,很多线程连接redis值需创建一个lettuce连接,可以减少线程连接开销,线程安全

springboot接入redis cluster需要和哨兵一样做读写分离吗?

需要,现在主节点是6008,6009,6015 从节点6007,6012,6014

image.png

image.png

pom文件

<?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.17</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>com.redis</groupId>
    <artifactId>redis-cluster</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>redis-cluster</name>
    <description>redis-cluster</description>
    <properties>
        <java.version>1.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.springframework.boot</groupId>
            <artifactId>spring-boot-devtools</artifactId>
            <scope>runtime</scope>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>com.mysql</groupId>
            <artifactId>mysql-connector-j</artifactId>
            <scope>runtime</scope>
        </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>
        </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>

配置文件

#连接数据源
spring.datasource.druid.username=root
spring.datasource.druid.password=xgm@2023..
spring.datasource.druid.url=jdbc:mysql://172.16.204.51:3306/redis?serverTimezone=GMT%2B8
spring.datasource.druid.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.druid.initial-size=5

#redis cluster
#支持集群拓扑动态感应刷新,自适应拓扑刷新是否使用所有可用的更新,默认false关闭
spring.redis.lettuce.cluster.refresh.adaptive=true
#定时刷新
spring.redis.lettuce.cluster.refresh.period=2000
#集群信息
spring.redis.cluster.nodes=172.16.64.21:6007,172.16.64.21:6008,172.16.64.21:6009,172.16.64.21:6012,172.16.64.21:6014,172.16.64.21:6015
spring.redis.password=123456
spring.redis.timeout=60
#默认的lettuce ,lettuce线程安全,Jedis是同步的,不支持异步,Jedis客户端实例不是线程安全的,需要每个线程一个Jedis实例,所以一般通过连接池来使用Jedis.
# 连接池最大连接数(使用负值表示没有限制)
spring.redis.lettuce.pool.max-active=50
# 连接池中的最大空闲连接
spring.redis.lettuce.pool.max-idle=50
# 连接池最大阻塞等待时间(使用负值表示没有限制)
spring.redis.lettuce.pool.max-wait=1000
# 连接池中的最小空闲连接
spring.redis.lettuce.pool.min-idle=5
spring.redis.lettuce.shutdown-timeout=1000
#eviction线程调度时间间隔
spring.redis.lettuce.pool.time-between-eviction-runs=2000
#最大的要重定向的次数(由于集群中数据存储在多个节点所以,在访问数据时需要通过节点进行转发)
spring.redis.cluster.max-redirects=3
#最大的连接重试次数
spring.redis.cluster.max-attempts=3

#日志
logging.pattern.console='%date{yyyy-MM-dd HH:mm:ss.SSS} | %highlight(%5level) [%green(%16.16thread)] %clr(%-50.50logger{49}){cyan} %4line -| %highlight(%msg%n)'
logging.level.root=info
logging.level.io.lettuce.core=debug
logging.level.org.springframework.data.redis=debug

配置类

package com.redis.redissentinel.conf;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import io.lettuce.core.ReadFrom;
import io.lettuce.core.TimeoutOptions;
import io.lettuce.core.cluster.ClusterClientOptions;
import io.lettuce.core.cluster.ClusterTopologyRefreshOptions;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.pool2.impl.GenericObjectPoolConfig;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.data.redis.RedisProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.*;
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.serializer.GenericJackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;
import java.text.SimpleDateFormat;
import java.time.Duration;
import java.util.*;

import org.springframework.data.redis.core.RedisTemplate;

import javax.lang.model.element.NestingKind;

/**
 * @author ygr
 * @date 2022-02-15 16:30
 */
@Slf4j
@Configuration
public class RedisConfig {
    @Value("${spring.redis.lettuce.pool.max-idle}")
    String maxIdle;
    @Value("${spring.redis.lettuce.pool.min-idle}")
    String minIdle;
    @Value("${spring.redis.lettuce.pool.max-active}")
    String maxActive;
    @Value("${spring.redis.lettuce.pool.max-wait}")
    String maxWait;

    @Value("${spring.redis.lettuce.pool.time-between-eviction-runs}")
    String timeBetweenEvictionRunsMillis;
    @Value("${spring.redis.cluster.nodes}")
    String clusterNodes;
    @Value("${spring.redis.password}")
    String password;
    @Value("${spring.redis.cluster.max-redirects}")
    String maxRedirects;
    @Value("${spring.redis.lettuce.cluster.refresh.period}")
    String period;
    @Value("${spring.redis.timeout}")
    String timeout;

    @Bean
    public LettuceConnectionFactory lettuceConnectionFactory() {
        GenericObjectPoolConfig genericObjectPoolConfig = new GenericObjectPoolConfig();
        genericObjectPoolConfig.setMaxIdle(Integer.parseInt(maxIdle));
        genericObjectPoolConfig.setMinIdle(Integer.parseInt(minIdle));
        genericObjectPoolConfig.setMaxTotal(Integer.parseInt(maxActive));
        genericObjectPoolConfig.setMaxWait(Duration.ofMillis(Long.parseLong(maxWait)));
        genericObjectPoolConfig.setTimeBetweenEvictionRuns(Duration.ofMillis(Long.parseLong(timeBetweenEvictionRunsMillis)));
        String[] nodes = clusterNodes.split(",");
        List<RedisNode> listNodes = new ArrayList();
        for (String node : nodes) {
            String[] ipAndPort = node.split(":");
            RedisNode redisNode = new RedisNode(ipAndPort[0], Integer.parseInt(ipAndPort[1]));
            listNodes.add(redisNode);
        }
        RedisClusterConfiguration redisClusterConfiguration = new RedisClusterConfiguration();
        redisClusterConfiguration.setClusterNodes(listNodes);
        redisClusterConfiguration.setPassword(password);
        redisClusterConfiguration.setMaxRedirects(Integer.parseInt(maxRedirects));
        // 配置集群自动刷新拓扑
        ClusterTopologyRefreshOptions topologyRefreshOptions = ClusterTopologyRefreshOptions.builder()
                .enablePeriodicRefresh(Duration.ofSeconds(Long.parseLong(period))) //按照周期刷新拓扑
                .enableAllAdaptiveRefreshTriggers() //根据事件刷新拓扑
                .build();

        ClusterClientOptions clusterClientOptions = ClusterClientOptions.builder()
                //redis命令超时时间,超时后才会使用新的拓扑信息重新建立连接
                .timeoutOptions(TimeoutOptions.enabled(Duration.ofSeconds(Long.parseLong(period))))
                .topologyRefreshOptions(topologyRefreshOptions)
                .build();
        LettuceClientConfiguration clientConfig = LettucePoolingClientConfiguration.builder()
                .commandTimeout(Duration.ofSeconds(Long.parseLong(timeout)))
                .poolConfig(genericObjectPoolConfig)
                .readFrom(ReadFrom.REPLICA_PREFERRED) // 优先从副本读取
                .clientOptions(clusterClientOptions)
                .build();
        LettuceConnectionFactory factory = new LettuceConnectionFactory(redisClusterConfiguration, clientConfig);
        return factory;

    }



    public ObjectMapper objectMapper() {
        ObjectMapper objectMapper = new ObjectMapper();
        objectMapper.setTimeZone(TimeZone.getTimeZone("GMT+8"));
        objectMapper.configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false);
        objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
        objectMapper.setDateFormat(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"));
        objectMapper.configure(JsonParser.Feature.ALLOW_SINGLE_QUOTES, true);
        return objectMapper;
    }

    @Bean
    @ConditionalOnMissingBean
    public RedisTemplate<String, Object> redisTemplate(LettuceConnectionFactory  factory) {
        factory.setShareNativeConnection(false);
        LettuceClientConfiguration clientConfiguration = factory.getClientConfiguration();
        // 创建RedisTemplate<String, Object>对象
        RedisTemplate<String, Object> template = new RedisTemplate<>();
        template.setConnectionFactory(factory);

        // 定义Jackson2JsonRedisSerializer序列化对象
        Jackson2JsonRedisSerializer<Object> jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer<>(Object.class);
        jackson2JsonRedisSerializer.setObjectMapper(objectMapper());

        StringRedisSerializer stringSerial = new StringRedisSerializer();
        // redis key 序列化方式使用stringSerial
        template.setKeySerializer(stringSerial);
        // redis value 序列化方式使用jackson
        template.setValueSerializer(jackson2JsonRedisSerializer);
        // redis hash key 序列化方式使用stringSerial
        template.setHashKeySerializer(stringSerial);
        // redis hash value 序列化方式使用jackson
        template.setHashValueSerializer(jackson2JsonRedisSerializer);

        template.afterPropertiesSet();
        return template;
    }

}


//---------------------------------推荐下面此方式-------------------------------------------------//



package com.redis.redissentinel.conf;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import io.lettuce.core.ReadFrom;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.data.redis.RedisProperties;
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.RedisPassword;
import org.springframework.data.redis.connection.RedisSentinelConfiguration;
import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory;
import org.springframework.data.redis.connection.lettuce.LettucePoolingClientConfiguration;
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;
import java.text.SimpleDateFormat;
import java.util.Arrays;
import java.util.HashSet;
import java.util.TimeZone;
import org.springframework.data.redis.core.RedisTemplate;
/**
 * @author ygr
 * @date 2022-02-15 16:30
 */
@Slf4j
@Configuration
public class RedisConfig {

    public ObjectMapper objectMapper() {
        ObjectMapper objectMapper = new ObjectMapper();
        objectMapper.setTimeZone(TimeZone.getTimeZone("GMT+8"));
        objectMapper.configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false);
        objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
        objectMapper.setDateFormat(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"));
        objectMapper.configure(JsonParser.Feature.ALLOW_SINGLE_QUOTES, true);
        return objectMapper;
    }

    @Bean
    @ConditionalOnMissingBean
    public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {
        // 创建RedisTemplate<String, Object>对象
        RedisTemplate<String, Object> template = new RedisTemplate<>();
        template.setConnectionFactory(factory);

        // 定义Jackson2JsonRedisSerializer序列化对象
        Jackson2JsonRedisSerializer<Object> jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer<>(Object.class);
        jackson2JsonRedisSerializer.setObjectMapper(objectMapper());

        StringRedisSerializer stringSerial = new StringRedisSerializer();
        // redis key 序列化方式使用stringSerial
        template.setKeySerializer(stringSerial);
        // redis value 序列化方式使用jackson
        template.setValueSerializer(jackson2JsonRedisSerializer);
        // redis hash key 序列化方式使用stringSerial
        template.setHashKeySerializer(stringSerial);
        // redis hash value 序列化方式使用jackson
        template.setHashValueSerializer(jackson2JsonRedisSerializer);

        template.afterPropertiesSet();
        return template;
    }
    @Bean
    public RedisConnectionFactory lettuceConnectionFactory(RedisProperties redisProperties) {
        RedisSentinelConfiguration sentinelConfig = new RedisSentinelConfiguration()
                .master(redisProperties.getSentinel().getMaster());
        redisProperties.getSentinel().getNodes().forEach(s -> {
            String[] arr = s.split(":");
            sentinelConfig.sentinel(arr[0],Integer.parseInt(arr[1]));
        });
        LettucePoolingClientConfiguration lettuceClientConfiguration = LettucePoolingClientConfiguration.builder()
                // 读写分离,若主节点能抗住读写并发,则不需要设置,全都走主节点即可
                //ANY 从任何节点读取,NEAREST 从最近节点读取,MASTER_PREFERRED / UPSTREAM_PREFERRED优先读取主节点,如果主节点不可用,则读取从节点,MASTER / UPSTREAM仅读取主节点
                .readFrom(ReadFrom.ANY_REPLICA)
                .build();
        sentinelConfig.setPassword(RedisPassword.of(redisProperties.getPassword()));
        sentinelConfig.setDatabase(redisProperties.getDatabase());
        return new LettuceConnectionFactory(sentinelConfig, lettuceClientConfiguration);
    }

}

测试

package com.redis.redissentinel;

import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.data.redis.core.RedisTemplate;

import javax.annotation.Resource;
import java.util.concurrent.TimeUnit;

@Slf4j
@SpringBootTest
class RedisSentinelApplicationTests {

    @Resource
    private RedisTemplate<String, Object> redisTemplate;


    @Test
    void witeTest() {
        for (int i = 0; i < 3; i++) {
            try {
                redisTemplate.opsForValue().set("k" + i, "v" + i);
                log.info("set value success: {}", i);

                Object val = redisTemplate.opsForValue().get("k" + i);
                log.info("get value success: {}", val);
                TimeUnit.SECONDS.sleep(1);
            } catch (Exception e) {
                log.error("error: {}", e.getMessage());
            }
        }
        log.info("finished...");
    }

    @Test
    void readTest() {
        Object k1 = redisTemplate.opsForValue().get("k1");
        log.info("读取节点k1的值:{}",k1);
    }

}

当前集群节点信息
image.png

image.png

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1217244.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

CMA认证和CNAS认可的联系和区别?哪个更权威?

一、CMA认证是什么?   CMA认证是指中国计量认证&#xff0c;省级以上的计量行政部门根据中国计量法的规定&#xff0c;对申请CMA测试资质的第三方检测机构进行评估&#xff0c;对检测水平和检测可靠性达到国家标准的实验室授予计量认证合格证书(CMA资质)。 二、CNAS认可是什…

制造业数据标准化的优势分析

之前我们介绍过>>数据驱动工作效率提升的5个层次——以PreMaint设备数字化平台为例&#xff0c;这次我们将深入分析数据标准化在制造业中的优势。 从持续的供应链中断和疯狂的通货膨胀&#xff0c;到日益昂贵和难以采购的原材料&#xff0c;制造企业正面对越来越多的挑战…

rsync远程同步(rsync+inotify)

目录 一、概述 1、关于rsync 2、rsync的特点&#xff1a; 3、备份方式&#xff1a; 4、同步方式&#xff1a; 二、rsync相关命令 1、rsync常用命令的选项&#xff1a; 2、启动和关闭rsync服务&#xff1a; 3、关闭 rsync 服务 三、 免交互&#xff1a; 1、免密同步&a…

Java_实现图书管理系统

目录 前言 框架核心思想 框架的实现 书类和书架类的实现 功能接口实现 功能的声明 父类用户和子类管理员&#xff0c;子类普通用户 Main方法 前言 java图书管理系统的详细解析;从思考到实现,一步步带你学会图书管理系统. 框架核心思想 下图只是一个图书管理系统的初步…

历年国自然标书申请 面上项目614份 2001-2019年 面上标书

这里列举几例 清华任丰原 哈尔滨 杨宝峰 # 关注微信&#xff1a;生信小博士&#xff0c;10元领取 关注微信之后&#xff0c; 点开付费合集即可领取

Strus2 系列漏洞

目录 1、S2-001 2、S2-005 3、S2-007 4、S2-008 5、S2-009 6、S2-012 7、S2-013/S2-014 8、S2-015 9、S2-016 10、S2-019 11、s2-032 12、S2-045 13、S2-048 14、S2-052 15、S2-053 16、S2-057 17、S2-devMode Strusts 中使用 OGNL 为表达式语言。OGNL(Object…

OpenAI 开发者大会2023 解读

概述 宏观上还是分成两点&#xff1a;新的底层模型GPT-4 Turbo、新的应用生态GPTs。其余的名词都是服务于上面两个&#xff0c;很多名词是面向开发者的&#xff08;非普通用户&#xff0c;主要是在页面上使用&#xff09;容易导致混淆&#xff0c;比如什么JSON 格式、函数调…

使用uniapp写小程序,真机调试的时候不显示log

项目场景&#xff1a; 当小程序文件太大的情况下使用真机调试&#xff0c;但是真机调试的调试器没有任何反应 问题描述 使用uniapp写小程序&#xff0c;真机调试的时候不显示log 原因分析&#xff1a; 提示&#xff1a;因为真机调试的时候没有压缩文件&#xff0c;所以调试的…

EtherCAT从站EEPROM分类附加信息详解:SM(同步管理器)

0 工具准备 1.EtherCAT从站EEPROM数据&#xff08;本文使用DE3E-556步进电机驱动器&#xff09;1 分类附加信息——SM&#xff08;同步管理器&#xff09; 1.1 分类附加信息规范 在EEPROM字64开始的区域存储的是分类附加信息&#xff0c;这里存储了包括设备信息、SM配置、FM…

有向无权图的最短路径

在运筹学领域的经典模型中&#xff0c;最大流问题、多商品网络流问题和最短路径问题等都依附在图上对问题进行描述&#xff0c;同样&#xff0c;当我们梳理问题的数学模型&#xff0c;或理解相关问题的求解算法时&#xff0c;也要依靠它。因此&#xff0c;我将总结和图相关的问…

Android NDK JNI 开发native层崩溃日志栈分析

问题&#xff1a; 在Android的JNI开发中&#xff0c;你是否看到如下一堆崩溃日志&#xff0c;不知如何下手分析问题&#xff0c;崩溃在哪一行&#xff1f; 11-16 17:20:44.844 23077 23077 W test_jni_h: jni_preload: Starting for processln 11-16 17:20:44.844 23077 2307…

AWD比赛中的一些防护思路技巧

## 思路1&#xff1a; 1、改服务器密码 &#xff08;1&#xff09;linux&#xff1a;passwd &#xff08;2&#xff09;如果是root删除可登录用户&#xff1a;cat /etc/passwd | grep bash userdel -r 用户名 &#xff08;3&#xff09;mysql&#xff1a;update mysql.user set…

基于SpringBoot+Vue的二手物品交易平台

基于SpringBootVue的二手物品交易平台的设计与实现~ 开发语言&#xff1a;Java数据库&#xff1a;MySQL技术&#xff1a;SpringBootMyBatisVue工具&#xff1a;IDEA/Ecilpse、Navicat、Maven 系统展示 主页 详情 管理员界面 摘要 本项目是基于Spring Boot 和 Vue 技术栈构建…

latex简单使用

​​文章目录 公式详解 普通公式公式居中带标号公式上标下标根号分式括号运算符列表 无序列表有序列表插入图片 单图多图排版表格脚注与定理子标题目录与附录 目录附录参考文献字体设置 字体样式 加粗斜体字母大写等线自定义字体字体大小 第一种设置第二种设置第三种设置 页面…

系列六、JVM的内存结构【栈】

一、产生背景 由于跨平台性的设计&#xff0c;Java的指令都是根据栈来设计的&#xff0c;不同平台的CPU架构不同&#xff0c;所以不能设计为基于寄存器的。 二、概述 栈也叫栈内存&#xff0c;主管Java程序的运行&#xff0c;是在线程创建时创建&#xff0c;线程销毁时销毁&…

【PyQt小知识 - 4】:QGroupBox分组框控件 - 边框和标题设置

QGroupBox QGroupBox 是 PyQt 中的一个小部件&#xff0c;用于创建一个带有标题的组框。 可以使用 QGroupBox 将相关控件分组并添加一个标题。 以下是一个使用 QGroupBox 的示例代码&#xff08;示例一&#xff09;&#xff1a; from PyQt5.QtWidgets import * import sysa…

ERP管理系统:企业升级的秘密武器

ERP管理系统&#xff1a;企业升级的秘密武器 在当今快速发展的商业环境中&#xff0c;企业要想保持竞争力&#xff0c;就必须不断进行自我升级。而在这个过程中&#xff0c;ERP管理系统以其强大的功能和优化流程的能力&#xff0c;逐渐成为了企业升级的秘密武器。 一、ERP管理…

Unity开发之C#基础-异常处理(Try Catch)

前言 其实本来这章应该将栈和队列的 但是后来想想 栈和队列在实际应用很少跟多的是大家了解一下栈和队列的基本常识比如先进先出的是谁后进先出的是谁这种 csdn有很多介绍栈和队列的文章 我觉得都比我理解深刻所以大家可以去搜索参照一下 今天我们继续往下讲解 如何自己主动的…

【Java 进阶篇】JQuery 遍历 —— For 循环的奇妙之旅

在前端开发的世界里&#xff0c;遍历是一个常见而重要的操作。它让我们能够浏览并操纵文档中的元素&#xff0c;为用户提供更加丰富和交互性的体验。而在 JQuery 中&#xff0c;遍历的方式多种多样&#xff0c;其中 for 循环是一种简单而灵活的选择。在本篇博客中&#xff0c;我…

11-Vue基础之组件通信(二)

个人名片&#xff1a; &#x1f60a;作者简介&#xff1a;一名大二在校生 &#x1f921; 个人主页&#xff1a;坠入暮云间x &#x1f43c;座右铭&#xff1a;懒惰受到的惩罚不仅仅是自己的失败&#xff0c;还有别人的成功。 &#x1f385;**学习目标: 坚持每一次的学习打卡 文章…