哨兵集群的一些缺点:
而Redis3.0版本开始就提供了RedisCluster的集群模式
整个服务数据是分片存储在多个master节点上的,每个master中存储的数据是不一样的,各个master节点下又可以加多个从节点,这样就组成了一个一个的小集群,多个小集群就组成了整个Cluster集群。
每个节点负责一定范围的哈希槽,默认总共有16384个slots槽位
这种方式搭建的集群读写都只能在master节点上行进行,slave节点一般只是做数据备份,不能读。
如果某个master节点宕机后,在哪个小集群中会进行选举,重新选举出一个master节点来。
一个小集群全挂了,不能对外提供服务了,默认情况下,整个cluster集群就都不能用了,但是可以通过一个配置项cluster-require-full-coverage
改为no
,这样其他小集群就还能对外提供服务。
我们首先需要关闭防火墙,或者是开发redis提供服务的端口和gossip的端口(提供服务端口+10000)
比如我当前一台服务器要部署两个redis服务,使用6379和6380端口,那么我就需要开放6379 6380 16379 16380端口
# 临时关闭防火墙
systemctl stop firewalld
# 禁止开机启动
systemctl disable firewalld
接下来的一个环境搭建案例是使用三台服务器,其中运行六个redis实例
对各个实例的redis.conf文件中的基本配置项做一些相应的修改
# 后台运行
daemonize yes
# 端口
port 6379
# 把pid进程号写入pidfile配置的文件
pidfile /var/run/redis_6379.pid
# 数据文件存放位置,rdb和aof文件使用的文件目录就是这个配置项
dir /usr/local/redis-cluster/6379/
# 绑定ip可以选择注释掉
# bind 127.0.0.1
# 关闭保护模式
protected-mode no
# aof持久化 可以选择开不开启
appendonly yes
# 指定redis连接访问密码,一般生产环境都会配置一个密码
requirepass hs
# 配置了上面的密码,那么也就是要指定一下集群访问的密码,和上面要一致,不然集群各个节点访问不通
masterauth hs
# 接下来三个是cluster集群相关的配置
# 启动集群模式
cluster-enabled yes
# 集群节点信息文件,这里6379建议和port对应上
# 这个文件保存了整个Cluster集群各个节点信息,当服务器关机后我们启动redis实例后 不需要在执行一遍--cluster create创建集群的命令。只需要启动redis服务即可,它会根据这个文件中的信息自动搭建好cluster集群
cluster-config-file nodes-6379.conf
# 节点超时时间
cluster-node-timeout 10000
六个redis实例的配置文件都准备好后就都先启动
redis-server redis.conf
此时六个redis-server都是毫无关联的,接下来将它们配置成为一个cluster集群
# -a 指定访问redis的密码
# -- cluster-replicas 表示每个redis小集群中,master下有几个slave,这里配置为1 就表示各个mastere只有一个slave
# 最后就是6个redis实例的ip和端口
redis-cli -a hs --cluster create --cluster-replicas 1 192.168.75.50:6379 192.168.75.50:6380 ......
验证集群
连接任意一个客户端即可
# -a 访问密码 -c 表示集群模式 -h -p为连接主机ip和端口
# redis-cli -c -h -p
redis-cli -a hs -c -h 192.168.75.50 6379
# 查看集群信息
cluster info
# 查看节点列表
cluster nodes
关闭
关闭集群则需要逐个进行关闭
redis-cli -a hs -c -h 192.168.75.50 -p 6379 shutdown
查看redis集群帮助
我们首先按照上面Cluster集群搭建部分的内容,先启动两个redis实例192.168.0.61:8007(主)
和192.168.0.61:8008(从)
然后执行下面的命令
# -a redis连接密码
# 第一个ip为当前新启动的redis实例ip
# 后面一个ip为当前cluster集群中任意一个正常运行的节点ip
/usr/local/redis-5.0.3/src/redis-cli -a hs --cluster add-node 192.168.0.61:8007 192.168.0.61:8001
查看节点状态
/usr/local/redis-5.0.3/src/redis-cli -a hs -c -h 192.168.0.61 -p 8001
192.168.0.61:8001> cluster nodes
新加入的节点都是master,并且不会分配任何slot槽位,我们要手动为新节点分配hash槽
使用redis-cli --cluster reshard
命令为新加入的节点分配槽位,需要使用集群中任意一个master节点对其进行重新分片工作
/usr/local/redis-5.0.3/src/redis-cli -a hs --cluster reshard 192.168.0.61:8001
接下来的命令交互如下:
… …
How many slots do you want to move (from 1 to 16384)? 600
(ps:需要多少个槽移动到新的节点上,自己设置,比如600个hash槽)
… …
What is the receiving node ID? 2728a594a0498e98e4b83a537e19f9a0a3790f38
(ps:把这600个hash槽移动到哪个节点上去,需要指定节点id)
… …
Please enter all the source node IDs.
Type ‘all’ to use all the nodes as source nodes for the hash slots.
Type ‘done’ once you entered all the source nodes IDs.
Source node 1:all
(ps:输入all为从所有主节点(8001,8002,8003)中分别抽取相应的槽数指定到新节点中,抽取的总槽数为600个)
… …
Do you want to proceed with the proposed reshard plan (yes/no)? yes
(ps:输入yes确认开始执行分片任务)
接下来再查看最新的集群节点信息
/usr/local/redis-5.0.3/src/redis-cli -a hs -c -h 192.168.0.61 -p 8001
192.168.0.61:8001> cluster nodes
此时还只是添加了一台master节点到集群中,我们接下来在添加192.168.0.61:8008
节点来作为192.168.0.61:8007
节点的从节点
# 使用集群中任意一个节点来进行添加新节点
/usr/local/redis-5.0.3/src/redis-cli -a hs --cluster add-node 192.168.0.61:8008 192.168.0.61:8001
新加入的节点是master,并且不会分配slot槽位
接下来需要登录刚刚添加的新节点,使用replicate
命令来指定当前节点要作为哪一个节点的slave节点,
# 先登录从节点,然后在replicate命令中指定主节点的id
/usr/local/redis-5.0.3/src/redis-cli -a hs -c -h 192.168.0.61 -p 8008
192.168.0.61:8008> cluster replicate 2728a594a0498e98e4b83a537e19f9a0a3790f38 #后面这串id为8007的节点id
现在8008端口的redis实例就变为了slave了。
接下来将上面新增加的两个节点删除
删除8008从节点
用redis-cli --cluster del-node
删除从节点8008,指定删除节点ip和端口,以及节点id
/usr/local/redis-5.0.3/src/redis-cli -a hs --cluster del-node 192.168.0.61:8008 a1cfe35722d151cf70585cee21275565393c0956
再次查看集群状态,如下图所示,8008这个slave节点已经移除,并且该节点的redis服务也已被停止
删除8807主节点
主节点的里面是有分配了hash槽的,所以我们这里必须先把8007里的hash槽放入到其他的可用主节点中去,然后再进行移除节点操作,不然会出现数据丢失问题
目前只能把master的数据迁移到一个节点上,暂时做不了平均分配功能
# 任选一个主节点进行重新分片
/usr/local/redis-5.0.3/src/redis-cli -a hs --cluster reshard 192.168.0.61:8007
接下来的命令交互如下:
… …
How many slots do you want to move (from 1 to 16384)? 600
(ps:需要多少个槽移动到新的节点上)
… …
What is the receiving node ID? 2728a594a0498e98e4b83a537e19f9a0a3790f38
(ps:把这600个hash槽移动到哪个节点上去,这里使用8001的主节点id)
… …
Please enter all the source node IDs.
Type ‘all’ to use all the nodes as source nodes for the hash slots.
Type ‘done’ once you entered all the source nodes IDs.
Source node 1:2728a594a0498e98e4b83a537e19f9a0a3790f38
(ps:这里是需要数据源,也就是我们的8007节点id。这里这次就不写all了)
… …
Source node 2:done
(ps:这里直接输入done 开始生成迁移计划)
… …
Do you want to proceed with the proposed reshard plan (yes/no)? yes
(ps:这里输入yes开始迁移)
至此,我们已经成功的把8007主节点的数据迁移到8001上去了,我们可以看一下现在的集群状态如下图,你会发现8007下面已经没有任何hash槽了,证明迁移成功!
最后我们直接使用del-node命令删除8007主节点即可
# 指定要删除的节点实例ip 端口 节点id
/usr/local/redis-5.0.3/src/redis-cli -a hs --cluster del-node 192.168.0.61:8007 2728a594a0498e98e4b83a537e19f9a0a3790f38
现在就是回到了最当开始六个实例的时候了
Jedis方式
引入依赖
<dependency>
<groupId>redis.clientsgroupId>
<artifactId>jedisartifactId>
<version>2.9.0version>
dependency>
java代码如下
public class JedisClusterTest {
public static void main(String[] args) throws IOException {
JedisPoolConfig config = new JedisPoolConfig();
config.setMaxTotal(20);
config.setMaxIdle(10);
config.setMinIdle(5);
// 指定集群所有节点
Set<HostAndPort> jedisClusterNode = new HashSet<HostAndPort>();
jedisClusterNode.add(new HostAndPort("192.168.0.61", 8001));
jedisClusterNode.add(new HostAndPort("192.168.0.62", 8002));
jedisClusterNode.add(new HostAndPort("192.168.0.63", 8003));
jedisClusterNode.add(new HostAndPort("192.168.0.61", 8004));
jedisClusterNode.add(new HostAndPort("192.168.0.62", 8005));
jedisClusterNode.add(new HostAndPort("192.168.0.63", 8006));
JedisCluster jedisCluster = null;
try {
//connectionTimeout:指的是连接一个url的连接等待时间
//soTimeout:指的是连接上一个url,获取response的返回等待时间
jedisCluster = new JedisCluster(jedisClusterNode, 6000, 5000, 10, "hs", config);
System.out.println(jedisCluster.set("cluster", "hs"));
System.out.println(jedisCluster.get("cluster"));
} catch (Exception e) {
e.printStackTrace();
} finally {
if (jedisCluster != null)
jedisCluster.close();
}
}
}
SpringBoot相关配置
引入依赖
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-data-redisartifactId>
dependency>
<dependency>
<groupId>org.apache.commonsgroupId>
<artifactId>commons-pool2artifactId>
dependency>
yaml配置
server:
port: 8080
spring:
redis:
database: 0
timeout: 3000
password: hs
cluster:
nodes: 192.168.0.61:8001,192.168.0.62:8002,192.168.0.63:8003,192.168.0.61:8004,192.168.0.62:8005,192.168.0.63:8006
lettuce:
pool:
max-idle: 50
min-idle: 10
max-active: 100
max-wait: 1000
java代码访问还是直接通过redisTemplate对象来访问
@RestController
public class IndexController {
private static final Logger logger = LoggerFactory.getLogger(IndexController.class);
@Autowired
private StringRedisTemplate stringRedisTemplate;
@RequestMapping("/test_cluster")
public void testCluster() throws InterruptedException {
stringRedisTemplate.opsForValue().set("hs", "666");
System.out.println(stringRedisTemplate.opsForValue().get("hs"));
}
}
RedisCluster将所有数据划分为16384个slots槽位,每个master节点负责一部分槽位
当redis客户端连接redis服务端时,会将各个节点负责的槽位信息获取到并缓存在本地。当客户端执行一条set命令时,客户端会根据hash计算出当前key的槽位,然后在得到对应的redis-server的ip,这样就直接定位到目标节点了
底层默认会使用crc16算法得到一个整数,然后在对16384取余,最终得到槽位结果
HASH_SLOT = CRC16(key) mod 16384
redis客户端执行一条set命令,该节点会发现这个key的槽位不归自己管理,然后它会向客户端发送一个跳转指令并携带目标节点地址
客户端收到指令后除了跳转到目标节点上去之外,还会同步更新纠正本地的槽位映射表缓存,后续所有key将使用更新后的槽位映射表。
这里之所以客户端会更新槽位映射表的原因是,假如服务端对槽位进行了重新分配,但是客户端还不知道,还是安装之前旧的缓存中的映射表去找节点,这时服务器就会知道这个槽位已经不归我管了,客户端此时再更新一下最新的槽位映射表即可
Redis Cluster集群架构使用的是gossip协议进行通信
维护集群元数据(集群节点信息,主从角色,节点数量,各节点共享的数据等)有两种方式:集中式、gossip
集中式
优点是对于节点元数据更新和读取的实时性更好,更新操作后会立刻更新集中式的存储中,其他节点就能立刻感知到
缺点是元数据的更新压力全集中在一个地方,可能会导致元数据的存储压力
很多中间件都会利用zookeeper集中式存储元数据
gossip
gossip包含多种消息,包括ping、pong、meet、fail等等
可能某个master与slave之间的因为网络抖动,一段时间不能互通了,那么可能slave就会认为这个master宕机了,就会触发主节点重新选举。
我们可以通过配置cluster-node-timeout
参数,设定一个超时时间,单位是毫秒,如果超过这个超时时间后才会去触发主节点重新选举
当一个主节点宕机后,并且经过上面cluster-node-timeout
参数设定的超时时间后,这个master节点下的所有slave节点就会想整个集群中的其他节点发送一个命令,但是只有其他小集群中的master节点才会响应,并且只会响应给一个slave节点。
因为一个master节点会收到宕机小集群下的所有slave节点的消息,它只会响应给最先请求到节点。
当某个slave得到的响应超过了所有master节点数量的一半后,那么它就会成为新的master节点。一段时间后宕机的老的master启动后会变为这个节点的slave节点。
如果多个slave节点得到的响应都没有超过一半那么就会重新再来一遍,但其实Redis底层通过slave延时发送机制来避免这个问题。
详情如下:
当slave发现自己的master变为FAIL状态时,便尝试进行Failover,以期成为新的master。由于挂掉的master可能会有多个slave,从而存在多个slave竞争成为master节点的过程, 其过程如下:
slave发现自己的master变为FAIL
将自己记录的集群currentEpoch加1,并广播FAILOVER_AUTH_REQUEST 信息
其他节点收到该信息,只有master响应,判断请求者的合法性,并发送FAILOVER_AUTH_ACK,对每一个epoch只发送一次ack
尝试failover的slave收集master返回的FAILOVER_AUTH_ACK
.slave收到超过半数master的ack后变成新Master
(这里解释了集群为什么至少需要三个主节点,如果只有两个,当其中一个挂了,只剩一个主节点是不能选举成功的,因为得到的结果永远都是1,不会大于1)
slave广播Pong消息通知其他集群节点。
从节点并不是在主节点一进入 FAIL 状态就马上尝试发起选举,而是有一定延迟,一定的延迟确保我们等待FAIL状态在集群中传播,slave如果立即尝试选举,其它masters或许尚未意识到FAIL状态,可能会拒绝投票
延迟计算公式:
DELAY = 500ms + random(0 ~ 500ms) + SLAVE_RANK * 1000ms
SLAVE_RANK
表示此slave已经从master复制数据的总量的rank。Rank越小代表已复制的数据越新。这种方式下,持有最新数据的slave将会首先发起选举(理论上)。
当发生网路分区,某个master节点不能和slave节点通信了,这时master节点没有宕机,但是slave会认为master宕机了,然后又触发了主节点重新选举。
然后就会出现一个小集群中存在了两个master节点。这时客户端就会分开向两个master节点中写数据,当网络分区恢复后,老master会作为新master节点是从节点,这时就会触发一次主从复制机制,老master的数据就会全清理掉,那么这一段时间中客户端往老master节点上写的数据就是丢失
可以通过下面配置来解决脑裂问题
# 写数据成功最少同步的slave数量,这个数量可以模仿大于半数机制配置,比如集群总共三个节点可以配置1,加上leader就是2,超过了半数
min-replicas-to-write 1
这个配置一定程度上会 影响集群的可用性,假如slave节点宕机了,导致配置的这个数量达不到,那么客户端就会写入不成功。
根据具体服务具体分析,看是否要开启这个配置
当redis.conf的配置cluster-require-full-coverage
为no
时,表示当负责一个插槽的主库下线且没有相应的从库进行故障恢复时,集群仍然可用,如果为yes
则集群不可用。
如果少于了三个master,那么主节点重新选举是不会成功的,因为slave得到其他master的ack应答永远不会大于一半。
比如集群中就两个master,现在是需要大于1,但是得到的ack应答数永远都是1,不会大于1.
推荐节点数为单数的原因是节省服务器资源
因为三个master节点的情况下,只能允许最多一个master节点宕机,而如果四个master节点的情况下,也最多只能允许一个master节点宕机。
当然这只是不考虑需要增加并发访问量而增加redis节点的情况,当然也可以把master节点数加为偶数,只是推荐单数。
我们知道可以通过mset key value [key value...]
来批量添加多个数据。
但使用cluster集群如果多个key的哈希槽计算结果一样才会成功,否则执行不成功
但是我们可以在key加一个相同的前缀,这个前缀使用{}包起来,这样redis就只会计算{}包起来值的hash槽,这样多个key就一样了
mset {user1}:1:name zhuge {user1}:1:age 18
当一个master服务器被某sentinel视为下线状态后,该sentinel会与其他sentinel协商选出sentinel的leader进行故障转移工作。每个发现master服务器进入下线的sentinel都可以要求其他sentinel选自己为sentinel的leader,选举是先到先得。同时每个sentinel每次选举都会自增配置纪元(选举周期),每个纪元中只会选择一个sentinel的leader。如果所有超过一半的sentinel选举某sentinel作为leader。之后该sentinel进行故障转移操作,从存活的slave中选举出新的master,这个选举过程跟集群的master选举很类似。
哨兵集群只有一个哨兵节点,redis的主从也能正常运行以及选举master,如果master挂了,那唯一的那个哨兵节点就是哨兵leader了,可以正常选举新master。
不过为了高可用一般都推荐至少部署三个哨兵节点。为什么推荐奇数个哨兵节点原理跟集群奇数个master节点类似。