Cluster架构
Redis哨兵与Cluster集群模式对比
1. 哨兵模式
Redis3.0之前一般是通过哨兵工具来监测master节点的状态,若master节点宕机,则哨兵集群会进行主从切换,从其他slave节点中选举出新的master节点。
相较于高可用集群模式而言,哨兵模式有如下不足:
- 哨兵模式的配置相对较复杂,且性能与高可用方面较一般
- 主从切换的过程中会出现访问瞬断的问题
- 哨兵模式中只有一个master节点对外提供读写服务,不能支持很高的并发;并且单节点的内存不宜过大,否则持久化文件过大影响主从同步的效率
2. 高可用Cluster集群模式
Redis高可用Cluster模式是由多个主从节点群组成的分布式服务器群,具有复制,高可用和分片特性;
相较于哨兵模式,其配置简单并且在性能和高可用方面更有一筹,不需要哨兵也能实现主从切换、故障转移功能,并且每个节点支持水平扩展(官方最大扩展1000个节点)。
Redis Cluster工作原理
Redis Cluster将数据划分为16384个槽位,每个节点负责其中一部分槽位,这部分槽位信息保存在对应负责的节点中。
当Client端连接集群时会得到一份集群的槽位配置信息缓存在客户端本地,在client查找某个key时可以根据缓存的槽位配置直接定位到对应的集群目标节点,另外当client端和server端的槽位信息不一致时,通过槽位纠正机制实现槽位的校验调整。
- 槽位定位算法: HASH_SLOT = CRC16(key) % 16384
- 跳转重定位:当Client端向一个“错误”节点(指令key所在的哈希slot不是这个节点时)发出指令,这个节点发现指令key对应的槽位不属于自己负责的,则会向client端发送一个携带目标节点的跳转指令告诉client端去这个目标节点获取数据,client端接收到指令后会跳转到指定的目标节点并更新本地缓存的槽位信息。
- 集群节点间通信:Redis Cluster集群节点间采用gossip协议通信
- 网络抖动:Redis Cluster提供cluster-node-timeout配置,当某个节点持续cluster-node-timeout时间失联会被认为节点故障则会进行主从切换
- 批量操作的支持:redis集群只支持所有key落在同一slot的情况,如果有多个key一定要用mset命令在redis集群上操作,则可以在key的前面加上{XX},这样参数数据分片hash计算的只会是大括号里的值,这样能确保不同的key能落到同一个slot中。如:mset {user}:1:name Jeffrey {user}:1:age 18
- 脑裂问题:redis集群没有过半机制会有脑裂问题,网络分区导致脑裂后多个主节点对外提供写服务,一旦网络分区恢复,会将其中一个主节点变为从节点,只可能会导致大量数据丢失。Redis Cluster提供min‐replicas‐to‐write 1配置最大程度上规避脑裂问题(该配置表示写数据成功最少同步的slave数量,这个数量可以模仿大于半数机制配置,比如集群总共三个节点可以配置1,加上leader就是2,超过了半数)
Redis Cluster集群选举原理
当slave发现自己的master变为FAIL状态时,slave期望成为新的master会尝试进行Failover,由于挂掉的master
可能会有多个slave,从而存在多个slave竞争成为master节点的过程。 其过程如下:
- slave发现自己的master变为FAIL
- 将自己记录的集群currentEpoch加1,并广播FAILOVER_AUTH_REQUEST信息
- 其他节点收到该信息,只有master响应,master会判断请求合法性,并发送FAILOVER_AUTH_ACK,对每一个
epoch只发送一次ack - 尝试failover的slave收集master返回的FAILOVER_AUTH_ACK
- slave收到超过半数master的ack后变成新Master(这里解释了集群为什么至少需要三个主节点,如果只有两
个,当其中一个挂了,只剩一个主节点是不能选举成功的) - slave广播Pong消息通知其他集群节点
从节点并不是一发现主节点FAIL 状态就马上尝试发起选举,而是有一定延迟,一定的延迟确保FAIL状态在集群中传播,slave如果立即尝试选举,其它masters或许尚未意识到FAIL状态,可能会拒绝投票
•延迟计算公式:DELAY = 500ms + random(0 ~ 500ms) + SLAVE_RANK * 1000ms
•SLAVE_RANK表示此slave已经从master复制数据的总量的rank。Rank越小代表已复制的数据越新。该方式理论上持有最新数据的slave将会先发起选举
搭建部署
前提:已编译安装完毕Redis(安装目录为 /usr/local/redis-5.0.3)
Redis Cluster集群至少要有3个master节点,本次搭建部署三个主从小集群即三个一主一从6个节点作为演示;
一般情况下,这三个主从小集群分别部署到三个不同的服务器节点,每台机器一主一从,但由于资源有限,本次演示的6个Redis节点均部署到同一台虚拟机。
部署步骤:
- 在目录/usr/local/下新建目录 redis-cluster并进入该目录,在该目录下新建三个子目录作为三个不同的服务器节点node1, node2, node3,然后分别在这三个子目录下新建两个子目录作为主从节点的配置目录,其中node1下新建子目录8001, 8004,node2下新建子目录8002, 8005,node3下新建子目录8003, 8006,再创建6个节点数据存放目录用于保存集群节点配置文件以及持久化数据文件
mkdir -p /usr/local/redis-cluster/node1/8001
mkdir -p /usr/local/redis-cluster/node1/8004
mkdir -p /usr/local/redis-cluster/node2/8002
mkdir -p /usr/local/redis-cluster/node2/8005
mkdir -p /usr/local/redis-cluster/node3/8003
mkdir -p /usr/local/redis-cluster/node3/8006
mkdir -p /usr/local/redis-cluster/8001
mkdir -p /usr/local/redis-cluster/8002
mkdir -p /usr/local/redis-cluster/8003
mkdir -p /usr/local/redis-cluster/8004
mkdir -p /usr/local/redis-cluster/8005
mkdir -p /usr/local/redis-cluster/8006
cd /usr/local/redis-cluster && ll
- 将Redis安装目录下的redis.conf配置文件拷贝到node1中的8001目录下
cp /usr/local/redis-5.0.3/redis.conf ./node1/8001/redis.conf
- 分别对8001~8006下的redis.conf配置文件进行修改(下面以node1下的8001节点配置为例,其他节点配置类似,只需替换对应端口号即可)
- 修改8001节点配置
vi node1/8001/redis.conf
#设置端口
port 8001
#设置后台启动
daemonize yes
#将PID进程号写入pidfile文件
pidfile “/var/run/redis_8001.pid”
#指定数据存放目录
dir “/usr/local/redis-cluster/8001”
#启动集群模式
cluster-enabled yes
#设置集群节点配置文件
cluster-config-file nodes-8001.conf
#设置集群节点超时时间(ms)
cluster-node-timeout 10000
#注释bind
#bind 127.0.0.1
#关闭保护模式
protected-mode no
#开启AOF
appendonly yes
#密码设置如下
#设置redis访问密码
requirepass admin
#设置集群节点间访问密码
masterauth admin(与访问密码一致即可)
:wq
- 修改node1下的8004节点配置(将修改后的8001下的redis.conf复制到8004下,使用批量替换进行修改 :%s/源字符串/目标字符串/g)
cp node1/8001/redis.conf node1/8004/redis.conf
vi node1/8004/redis.conf
:%s/8001/8004/g
:wq
- 同上,依次修改node2下的8002,8005和node3下的8003,8006目录下的redis.conf
- 分别启动8001~8006节点并查看是否启动成功
/usr/local/redis-5.0.3/src/redis-server /usr/local/redis-cluster/node1/8001/redis.conf
/usr/local/redis-5.0.3/src/redis-server /usr/local/redis-cluster/node1/8004/redis.conf
/usr/local/redis-5.0.3/src/redis-server /usr/local/redis-cluster/node2/8002/redis.conf
/usr/local/redis-5.0.3/src/redis-server /usr/local/redis-cluster/node2/8005/redis.conf
/usr/local/redis-5.0.3/src/redis-server /usr/local/redis-cluster/node3/8003/redis.conf
/usr/local/redis-5.0.3/src/redis-server /usr/local/redis-cluster/node3/8006/redis.conf
ps -ef|grep redis
- 创建集群:通过redis-cli创建(redis5之前是通过ruby脚本的redis‐trib.rb实现)
注意:由于执行下面命令需要确认三台机器之间的redis实例要能相互访问,如果这三个主从小集群是分别部署在三个不同服务器节点上,则可以关闭所有机器防火墙或打开所有机器redis服务端口和集群节点gossip通信端口16379(默认是在redis端口号上加10000)
命令 --cluster-replicas 1 表示为集群中每个主节点创建一个从节点
说明:本次演示是在同一台虚拟机上部署,采取直接关闭防火墙的方式(临时关闭 systemctl stop firewalld,开机禁止启动 systemctl disable firewalld)
/usr/local/redis-5.0.3/src/redis-cli -a admin --cluster create --cluster-replicas 1 192.168.126.130:8001 192.168.126.130:8002 192.168.126.130:8003 192.168.126.130:8004 192.168.126.130:8005 192.168.126.130:8006
- 验证集群:redis-cli连接任意一个集群节点即可
#连接集群节点8001
/usr/local/redis-5.0.3/src/redis-cli -a admin -c -h 192.168.126.130 -p 8001
#查看集群信息
cluster info
#查看集群节点列表
cluster nodes
数据操作验证:
SpringBoot整合Redis Cluster集群
引入依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-pool2</artifactId>
</dependency>
修改application.yml
server:
port: 8080
#redis cluster config
spring:
application:
name: redis-demo
redis:
database: 0
timeout: 3000
password: admin
cluster:
nodes: 192.168.126.130:8001,192.168.126.130:8002,192.168.126.130:8003,192.168.126.130:8004,192.168.126.130:8005,192.168.126.130:8006
lettuce:
pool:
max-idle: 50
min-idle: 10
max-active: 100
max-wait: 1000
启动类App
package com.itjeffrey.redis.test;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;
import org.springframework.scheduling.annotation.EnableScheduling;
/**
* @From: Jeffrey
* @Date: 2022/11/12
*/
@EnableScheduling
@SpringBootApplication(exclude = {DataSourceAutoConfiguration.class})
public class App {
public static void main(String[] args) {
SpringApplication.run(App.class, args);
}
}
RedisConfig
package com.itjeffrey.redis.test.service;
import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.PropertyAccessor;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cache.annotation.CachingConfigurerSupport;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.cache.RedisCacheConfiguration;
import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.RedisSerializationContext;
import org.springframework.data.redis.serializer.RedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;
import java.time.Duration;
import java.util.HashMap;
import java.util.Map;
/**
* Redis配置
* @From: Jeffrey
*/
@Slf4j
@Configuration
@EnableCaching
public class RedisConfig extends CachingConfigurerSupport {
//string serializer
private RedisSerializer<String> redisSerializer = new StringRedisSerializer();
//object serializer
private Jackson2JsonRedisSerializer<Object> jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer<>(Object.class);
public RedisConfig(){
setObjectMapper(jackson2JsonRedisSerializer);
}
//LettuceConnectionFactory实例化过程中会自动从spring.cache.redis中读取配置信息
@Autowired
private LettuceConnectionFactory lettuceConnectionFactory;
/**
* config redisTemplate---manually add caches
* @return RedisTemplate
*/
@Bean
public RedisTemplate<String, Object> redisTemplate() {
RedisTemplate<String, Object> template = new RedisTemplate<>();
// 设置ObjectMapper(解决查询缓存转换异常问题)
setObjectMapper(jackson2JsonRedisSerializer);
// 设置连接工厂
template.setConnectionFactory(lettuceConnectionFactory);
// 配置key, value, hashValue序列化
template.setKeySerializer(redisSerializer);
template.setValueSerializer(jackson2JsonRedisSerializer);
template.setHashValueSerializer(jackson2JsonRedisSerializer);
return template;
}
/**
* 解决查询缓存转换异常问题
*/
private void setObjectMapper(Jackson2JsonRedisSerializer jackson2JsonRedisSerializer){
ObjectMapper om = new ObjectMapper();
om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
jackson2JsonRedisSerializer.setObjectMapper(om);
}
}
RedisUtil
package com.itjeffrey.redis.test.service;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.*;
import org.springframework.stereotype.Component;
import java.io.Serializable;
import java.util.List;
import java.util.Set;
import java.util.concurrent.TimeUnit;
/**
* Redis操作工具类
* @From: Jeffrey
*/
@Component
public class RedisUtil {
@Autowired
private RedisTemplate redisTemplate;
/**
* 写入缓存
*
* @param key
* @param value
* @return
*/
public boolean set(final String key, Object value) {
boolean result = false;
try {
ValueOperations<Serializable, Object> operations = redisTemplate.opsForValue();
operations.set(key, value);
result = true;
} catch (Exception e) {
e.printStackTrace();
}
return result;
}
/**
* 写入缓存设置时效时间
*
* @param key
* @param value
* @return
*/
public boolean set(final String key, Object value, Long expireTime, TimeUnit timeUnit) {
boolean result = false;
try {
ValueOperations<Serializable, Object> operations = redisTemplate.opsForValue();
operations.set(key, value);
redisTemplate.expire(key, expireTime, timeUnit);
result = true;
} catch (Exception e) {
e.printStackTrace();
}
return result;
}
/**
* 批量删除对应的value
*
* @param keys
*/
public void remove(final String... keys) {
for (String key : keys) {
remove(key);
}
}
/**
* 批量删除key
*
* @param pattern
*/
public void removePattern(final String pattern) {
Set<Serializable> keys = redisTemplate.keys(pattern);
if (keys.size() > 0) {
redisTemplate.delete(keys);
}
}
/**
* 删除对应的value
*
* @param key
*/
public void remove(final String key) {
if (exists(key)) {
redisTemplate.delete(key);
}
}
/**
* 判断缓存中是否有对应的value
*
* @param key
* @return
*/
public boolean exists(final String key) {
return redisTemplate.hasKey(key);
}
/**
* 读取缓存
*
* @param key
* @return
*/
public Object get(final String key) {
Object result = null;
ValueOperations<Serializable, Object> operations = redisTemplate.opsForValue();
result = operations.get(key);
return result;
}
/**
* 哈希 添加
*
* @param key
* @param hashKey
* @param value
*/
public void hmSet(String key, Object hashKey, Object value) {
HashOperations<String, Object, Object> hash = redisTemplate.opsForHash();
hash.put(key, hashKey, value);
}
/**
* 哈希获取数据
*
* @param key
* @param hashKey
* @return
*/
public Object hmGet(String key, Object hashKey) {
HashOperations<String, Object, Object> hash = redisTemplate.opsForHash();
return hash.get(key, hashKey);
}
/**
* 列表添加
*
* @param k
* @param v
*/
public void lPush(String k, Object v) {
ListOperations<String, Object> list = redisTemplate.opsForList();
list.rightPush(k, v);
}
/**
* 列表获取
*
* @param k
* @param l
* @param l1
* @return
*/
public List<Object> lRange(String k, long l, long l1) {
ListOperations<String, Object> list = redisTemplate.opsForList();
return list.range(k, l, l1);
}
/**
* 集合添加
*
* @param key
* @param value
*/
public void add(String key, Object value) {
SetOperations<String, Object> set = redisTemplate.opsForSet();
set.add(key, value);
}
/**
* 集合获取
*
* @param key
* @return
*/
public Set<Object> setMembers(String key) {
SetOperations<String, Object> set = redisTemplate.opsForSet();
return set.members(key);
}
/**
* 有序集合添加
*
* @param key
* @param value
* @param scoure
*/
public void zAdd(String key, Object value, double scoure) {
ZSetOperations<String, Object> zset = redisTemplate.opsForZSet();
zset.add(key, value, scoure);
}
/**
* 有序集合获取
*
* @param key
* @param scoure
* @param scoure1
* @return
*/
public Set<Object> rangeByScore(String key, double scoure, double scoure1) {
ZSetOperations<String, Object> zset = redisTemplate.opsForZSet();
return zset.rangeByScore(key, scoure, scoure1);
}
}
测试
package com.itjeffrey.redis.test.service;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Service;
/**
* @From: Jeffrey
* @Date: 2022/11/12
*/
@Service
public class TestService {
private int count;
@Autowired
private RedisUtil redisUtil;
@Scheduled(cron = "0 0/2 * * * ?")
public void test(){
String key = "user" + count;
redisUtil.set(key, "jeffrey-" + count);
System.out.println("set redis cache, key:" + key + " - value:" + redisUtil.get(key));
count++;
}
}
结果
控制台打印:
client连接8001节点并查看数据