集群搭建(redis7)

一、主从复制(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@ConditionalOnMissingBeanpublic 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 序列化方式使用stringSerialtemplate.setKeySerializer(stringSerial);// redis value 序列化方式使用jacksontemplate.setValueSerializer(jackson2JsonRedisSerializer);// redis hash key 序列化方式使用stringSerialtemplate.setHashKeySerializer(stringSerial);// redis hash value 序列化方式使用jacksontemplate.setHashValueSerializer(jackson2JsonRedisSerializer);template.afterPropertiesSet();return template;}@Beanpublic 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 {@Resourceprivate RedisTemplate<String, Object> redisTemplate;@Testvoid 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...");}@Testvoid 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");//6373int b = SlotHash.getSlot("B");//10374int w = SlotHash.getSlot("W");//10770int end = SlotHash.getSlot("艾弗森大苏打的幅度萨芬热微软微软微软464666sswwwvvvbbssssss");//5161System.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 123456cluster-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 123456cluster-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 123456cluster-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 123456cluster-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 123456cluster-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 123456cluster-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 123456cluster-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 123456cluster-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;@Beanpublic 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@ConditionalOnMissingBeanpublic 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 序列化方式使用stringSerialtemplate.setKeySerializer(stringSerial);// redis value 序列化方式使用jacksontemplate.setValueSerializer(jackson2JsonRedisSerializer);// redis hash key 序列化方式使用stringSerialtemplate.setHashKeySerializer(stringSerial);// redis hash value 序列化方式使用jacksontemplate.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@ConditionalOnMissingBeanpublic 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 序列化方式使用stringSerialtemplate.setKeySerializer(stringSerial);// redis value 序列化方式使用jacksontemplate.setValueSerializer(jackson2JsonRedisSerializer);// redis hash key 序列化方式使用stringSerialtemplate.setHashKeySerializer(stringSerial);// redis hash value 序列化方式使用jacksontemplate.setHashValueSerializer(jackson2JsonRedisSerializer);template.afterPropertiesSet();return template;}@Beanpublic 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 {@Resourceprivate RedisTemplate<String, Object> redisTemplate;@Testvoid 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...");}@Testvoid readTest() {Object k1 = redisTemplate.opsForValue().get("k1");log.info("读取节点k1的值:{}",k1);}}

当前集群节点信息
image.png

image.png

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

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

JVS低代码表单设计:数据联动详解(多级数据级、数据回显等)

在这信息化时代&#xff0c;表单作为数据的收集和展示工具&#xff0c;已经渗透到不同的角落。JVS低代码对表单的设计和操作进行了不断的优化和创新。其中&#xff0c;联动回显作为一项重要的功能&#xff0c;无论是多级数据级联控制、组件的联动控制&#xff0c;还是多表的数据…

通信网络安全防护定级备案流程介绍(附流程图)

通信网络安全防护定级备案是拥有增值电信业务经营许可证并且有开展电信业务的企业要做的一件事情。刚接触这块的家人们在填报操作的时候可能对具体通信网络安全防护定级备案流程还不是很清楚&#xff0c;所以就给大家画张具体的流程图吧&#xff0c;可以更加直观的了解。 通信…

JVM的运行时数据区

Java虚拟机&#xff08;JVM&#xff09;的运行时数据区是程序在运行过程中使用的内存区域&#xff0c;主要包括以下几个部分&#xff1a; 程序计数器虚拟机栈本地方法栈堆方法区运行时常量池直接内存 不同的虚拟机实现可能会略有差异。这些区域协同工作&#xff0c;支持Java…

ElasticSearch在Windows上的下载与安装

Elasticsearch是一个开源的分布式搜索和分析引擎&#xff0c;它可以帮助我们快速地搜索、分析和处理大量数据。Elasticsearch能够快速地处理结构化和非结构化数据&#xff0c;支持全文检索、地理位置搜索、自动补全、聚合分析等功能&#xff0c;能够承载各种类型的应用&#xf…

Sql Server 2017主从配置之:发布订阅

使用发布订阅模式搭建Sql Server 2017主从同步&#xff0c;类似事件通知机制&#xff0c;基本可以做到准实时同步&#xff0c;可以同时做到一对多的数据同步。 不过发布订阅模式&#xff0c;只能同时数据&#xff0c;不能同步表结构。在创建发布的时候&#xff0c;需要选择需要…

ospf路由选路及路由汇总

一、知识补充 1、ABR和ASBR 1.1 ABR ABR指的是边界路由&#xff0c;通常位于两个或多个区域之间&#xff0c;用于在不同的OSPF区域之间传递信息。当一个路由器同时连接到两个或多个区域时&#xff0c;它就成为了ABR&#xff0c;它需要维护每个区域的拓扑信息和路由表&#x…

【Kingbase FlySync】命令模式:部署双轨并行,并实现切换同步

【Kingbase FlySync】命令模式:安装部署同步软件&#xff0c;实现Oracle到KES实现同步 双轨并行方案说明一.准备工作二.环境说明三.目标实操(1).准备安装环境Orcle服务器(Oracle40)1.上传所有工具包2.操作系统配置a.增加flysync 用户、设置密码b.配置环境变量c.调整limits.conf…

git问题: git@10.18.*.*: Permission denied (publickey,password)

遇到的问题&#xff1a; openSSH版本太高&#xff0c;openssh高版本默认禁止ssh-rsa加密算法&#xff0c;直接换ed25519 执行以下命令&#xff1a; 在.ssh目录下执行&#xff1a;ssh-keygen -t ed25519 -C “youremail.com” ssh-add ~/.ssh/id_ed25519 将id_ed25519.pub添加…

解决证书加密问题:OpenSSL与urllib3的兼容性与优化

在使用客户端证书进行加密通信时&#xff0c;用户可能会遇到一些问题。特别是当客户端证书被加密并需要密码保护时&#xff0c;OpenSSL会要求用户输入密码。这对于包含多个调用的大型会话来说并不方便&#xff0c;因为密码无法在连接的多个调用之间进行缓存和重复使用。用户希望…

PPT幻灯片里的图片,批量提取

之前分享过如何将PPT文件导出成图片&#xff0c;今天继续分享PPT技巧&#xff0c;如何提取出PPT文件里面的图片。 首先&#xff0c;我们将PPT文件的后缀名&#xff0c;修改为rar&#xff0c;将文件改为压缩包文件 然后我们将压缩包文件进行解压 最好是以文件夹的形式解压出来…

PgSQL技术内幕-Bitmap Index Scan

PgSQL技术内幕-Bitmap Index Scan 1、简介 Bitmap索引扫描是对索引扫描的一个优化&#xff0c;通过建立位图的方式将原来的随机堆表访问转换成顺序堆表访问。主要分为两点&#xff1a;1&#xff09;管理每个Bitmap的hash slot没用完时&#xff0c;每个Bitmap代表每个heap页中满…

【蓝桥杯选拔赛真题23】C++计算24 第十二届蓝桥杯青少年创意编程大赛C++编程选拔赛真题解析

C/C++计算24 第十二届蓝桥杯青少年创意编程大赛C++选拔赛真题 一、题目要求 1、编程实现 “计算 24”是一个流传已久的数字游戏,小蓝最近对此痴迷不已 游戏规则是:从 1~10 之间的自然数任意拿出 4 个数(4 个数各不相同,顺序随机),进行加、减、乘三种运算(使用某种运算…

什么是BT种子!磁力链接又是如何工作的?

目录 一.什么是BT&#xff1f;1.BT简介&#xff1a;1.1.BT是目前最热门的下载方式之一1.2.BT服务器是通过一种传销的方式来实现文件共享的 2.小知识&#xff1a;2.1.你知道吗BT下载和常规下载到底有哪些不同2.2.BT下载的灵魂&#xff1a;种子2.3.当下载结束后&#xff0c;如果未…

matlab 坡度滤波算法地面分割

目录 一、算法原理1、实现流程2、参考文献二、代码实现三、结果展示四、测试数据一、算法原理 1、实现流程 1、格网示意图 2、计算格网行列数 公式中的特殊符号为向上取整,

初识分布式键值对存储etcd

欢迎大家到我的博客浏览。胤凯 (oyto.github.io)大家好&#xff0c;今天我带大家来学习一下 etcd。 一、什么是 etcd etcd 是一个开源的分布式键值存储系统&#xff0c;主要用于构建分布式系统中那点服务发现、配置管理、分布式锁等场景。它采用 Raft 一致性算法来确保所有节…

面试题-6

1.精灵图和base64的区别是什么&#xff1f; 精灵图:把多张小图整合到一张大图上,利用定位的一些属性把小图显示在页面上,当访问页面可以减少请求,提高加载速度 base64&#xff1a;传输8bit字节代码的编码方式,把原本二进制形式转为64个字符的单位&#xff0c;最后组成字符串 …

物联网赋能:WIFI HaLow在无线连接中的优势

在探讨无线网络连接时&#xff0c;我们不难发现&#xff0c;WIFI已经成为我们日常生活中不可或缺的一部分&#xff0c;承载了半数以上的互联网流量&#xff0c;并在家庭、学校、娱乐场所等各种场合广泛应用。然而&#xff0c;尽管WIFI4、WIFI5和WIFI6等协议无处不在&#xff0c…

记一次线上bug排查-----SpringCloud Gateway组件 请求头accept-encoding导致响应结果乱码

基于公司的业务需求&#xff0c;在SpringCloud Gateway组件的基础上&#xff0c;写了一个转发服务&#xff0c;测试开发阶段运行正常&#xff0c;并实现初步使用。但三个月后&#xff0c;PostMan请求接口&#xff0c;返回异常&#xff0c;经排查&#xff0c;从日志中获取到转发…

嵌入式QTGit面试题

自己在秋招过程中遇到的QT和嵌入式和Git相关的面试题&#xff0c;因为比较少就一起放了 QT connect第5个参数是什么&#xff1f; Qt::AutoConnection&#xff1a; 默认值&#xff0c;使用这个值则连接类型会在信号发送时决定。 如果接收者和发送者在同一个线程&#xff0c;则…

SVG圆形 <circle>的示例代码

本专栏是汇集了一些HTML常常被遗忘的知识&#xff0c;这里算是温故而知新&#xff0c;往往这些零碎的知识点&#xff0c;在你开发中能起到炸惊效果。我们每个人都没有过目不忘&#xff0c;过久不忘的本事&#xff0c;就让这一点点知识慢慢渗透你的脑海。 本专栏的风格是力求简洁…