一、主从复制(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下不同端口配置
主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
日志查看
主机
从机
复制原理
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挂了
- 默认情况下不会在slave节点自动重选一个master
- 需要人工干预
二、哨兵(sentinel)(不推荐)
介绍
- 哨兵巡查监控后台master主机是否故障,如果故障了根据投票数自动将某一个从库转换为新主库,继续对外服务,俗称无人值守运维,不存放数据只是吹哨人
作用
- 监控redis运行状态,包括master和slave
- 当master down机,能自动将slave切换成新master
哨兵的四个功能
主从监控
监控主从redis库运行是否正常
消息通知
哨兵可以将故障转移的结果发送到客户端
故障转移
如果master异常,则会进行主从切换,将其中一个slave作为新master
配置中心
客户端通过连接哨兵来获得当前Redis服务的主节点地址
搭建
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
主从复制一主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
结论
6002变为mster,数据不会丢失
重启6001
结论
6001不能切换为主master,还是之前的6002为master
哨兵选举原理
当一个主从配置中的master失效之后,sentinel可以选举出一个新的master,用于接替原master的工作,主从配置中其他redis服务器自动指向新的master同步数据。一般建议sentinel采用奇数台,防止某一台sentinel无法连接到master导致误切换、
SDOWN主观下线
- SDOWN 是单个sentinel 自己主观上检测到的关于master的状态,从sentinel的角度来看,如果发送了PING心跳后,在一定时间内没有收到合法的回复,就达到了SDOWN的条件
- sentinel配置文件中的down-after-milliseconds 设置了判断主观下线的时间长度
ODOWN客观下线
ODOWN需要一定数量的sentinel,多个哨兵达成一致意见才能认为一个master客观上已经宕机
选举出领导者哨兵
- 当主节点被判断客观下线以后,各个哨兵节点会进行协商,县选举出一个领导者哨兵节点并由该领导者节点进行failover(故障迁移)
- Raft算法 选出领导者节点
-
由领导者节点开始推动故障切换并选出一个新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);
}
}
踩坑指南
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/
架构图
1 cluster主节点挂了,slave顶替上来,自己挂了不影响其他主节点的使用
2 redis集群支持多个master,每个master又可以挂在多个slave
3 cluster自带sentinel故障转移机制,内置了高可用支持,无需再去使用哨兵功能
4 客户端与redis的节点连接,不再需要连接集群中所有的节点,只需要任意连接集群中的一个可用节点即可
5 槽位slot负责分配到各个物理服务节点,由对应集群来负责维护节点、插槽和数据之间的关系
架构设计原理
16384个slot卡槽,16384主节点,但建议最大主节点不要超过1000
redis集群槽位图
redis集群优点
1 方便扩缩容和数据分派查找;
扩缩容节点的移动并不会停止服务,改变节点哈希槽的数量都不会造成集群不可用的状态
slot槽位映射
哈希槽取余(不推荐)
hash(key)% 机器数;
适用场景
小厂/公司
不会变动机器数量
优点
简单粗暴,直接有效;起到负载均衡+分而治之的作用
缺点
进行扩容和缩容比较麻烦,映射关系重新计算,宕机会导致hash取余全部数据重新洗牌,原来的值存在服务器但获取不到,也就是数据丢失(虽然存在服务器,但找不到)
一致性哈希算法分区(不推荐)
ip节点映射和 落键规则
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,太大压缩位图较难,信息传递减弱,网络拥堵
集群配置(三主三从)
配置文件
6007、6008、6009、6012、6014、6015 6个端口服务配置分别如下:
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
连接任意一个节点查看集群状态
#-c表示集群 不加的话不是按照集群启动的,对于在别的机器上的key,会报错
./redis-cli -a 123456 -p 6007 -c
查看
#查看节点信息
cluster nodes
#查看集群信息
cluster info
#查看当前节点主从关系
info replication
测试
新增key查看是否成功
主从容错切换迁移
找出一个主从测试即可,目前6007是6015的主节点
关闭6007查看6015会不会切换成master
启动6007看6015会不会让位
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
检查集群情况,6016
./redis-cli -a 123456 --cluster check 172.16.64.21:6016
给6016分派卡槽,从其他的服务中均一点
./redis-cli -a 123456 --cluster reshard 172.16.64.21:6016
上述解释
- all:集群中的所有主节点都会成为源节点,redis-trib从各个源节点中各取出一部分哈希槽,凑够4096个,然后移动到6016节点上
- done :要从特点的哪个节点中取出 4096 个哈希槽
再次检查集群情况
./redis-cli -a 123456 --cluster check 172.16.64.21:6016
为主节点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
主从缩容
让6016、6017下线
#卡槽有数据不能删除,需要还给集群才能删除,当前6017卡槽为0
./redis-cli -a 123456 --cluster del-node 172.16.64.21:6017 6017id号
将6016的槽号情况,重新分配,先全部都给6007
./redis-cli -a 123456 --cluster reshard 172.16.64.21:6016
查看集群情况
集群删除6016
#卡槽有数据不能删除,需要还给集群才能删除,当前6017卡槽为0
./redis-cli -a 123456 --cluster del-node 172.16.64.21:6016 6016id号
完整提供服务配置
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
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);
}
}
当前集群节点信息