1. 主从模型
流程: redis支持1主N从模型以提高数据容灾性,主从中的数据只能从master流向slave,故主写从读,读写分离:
- 在
slave目录下开发配置文件6381/6382.conf:配置端口号,工作目录,日志,RDB文件:- 主从节点不建议部署在同一台机器上。
- 将主从节点配置为windows服务并启动,以6381为例:
redis-server --service-install slave/6381.conf --service-name redis6381redis-server --service-start --service-name redis6381
6382 > slaveof 127.0.0.1 6381:使6382成为6381的从节点,该过程异步执行,立即返回OK:- 建立主从关系之前,redis会先清空从节点的所有数据。
- 后台使用
bgsave命令将数据备份到RDB文件,然后再恢复到slave以完成数同步。
6382 > slaveof no one:使6382脱离主从关系,此后不再接收master同步的数据,但之前同步的数据仍然保留。6381/6382 > info replication:分别查看主从节点的replication信息。6381 > set a 100:在主节点中写数据,返回OK。6381 > set a:在主节点中读数据,返回100。6382 > set a 100:在从节点中写数据,报错。6382 > get a:在从节点中读数据,返回100。- 配置文件方式:若不使用命令,可直接在slave配置文件中额外配置并重启redis节点以搭建主从关系:
masterauth 123:配置master认证密码,无密码时可省略。slaveof 127.0.0.1 6381:配置其所属master的IP和端口号。
源码: /springdata-redis/
- conf:
6381.conf
port 6381
dir slave
logfile 6381.log
dbfilename dump-6381.rdb
# 设置配置文件密码密码
# requirepass 123
- conf:
6382.conf
port 6382
dir slave
logfile 6382.log
dbfilename dump-6382.rdb
# 如果6381配置master认证密码,6382需要配置这个,无密码时可省略
# masterauth 123
# 配置其所属master的IP和端口号
slaveof 127.0.0.1 6381
2. 哨兵模型
概念: sentinel哨兵无法操作数据,仅用于提升主从模型的高可用问题,建议搭建多个sentinel作为一个集群使用:
- 开发配置文件
7007/7008/7009.conf:配置端口号,工作目录,日志,RDB文件。 - 将3个节点配置为windows服务并启动,以7007为例:
- cmd:
redis-server --service-install slave/7007.conf --service-name redis7007 - cmd:
redis-server --service-start --service-name redis7007
- cmd:
- 分别使7008和7009成为7007的从节点:
- cmd:
7008/7009 > slaveof 127.0.0.1 7007
- cmd:
- 开发sentinel配置文件
27007/27008/27009.conf:配置端口号,工作目录,日志:protected-mode no:关闭保护模式,否则Jedis无法访问。sentinel monitor my-master 127.0.0.1 7007 2:监视以7007为master的主从结构:my-master:sentinel支持同时监视多套主从结构,所以需要设置名称以区分。2表示当累计超过2个主观下线标记时执行故障转移。
sentinel down-after-milliseconds my-master 5000:5秒内ping不通master或直接报错时标记主观下线。sentinel parallel-syncs my-master 1:故障转移时最多1个节点同步新的master数据。sentinel failover-timeout my-master 15000:故障转移超时时间为15秒。
- 将3个sentinel部署为window服务并启动,以27007为例:
- cmd:
redis-server --service-install sentinel/27007.conf --sentinel --service-name sentinel27007- sentinel需额外添加
--sentinel参数。
- sentinel需额外添加
- cmd:
redis-server --service-start --service-name sentinel27007
- cmd:
- 查看全部sentinel日志:生成哨兵ID,发现了全部slave,哨兵之间互相发现以保证高可用。
- 分别查看3个sentinel的监视信息:
- cmd:
27007/27008/27009 > info sentinel
- cmd:
- 手动下线7007,再次查看任一sentinel的监视信息:会发现master发生变更:
- cmd:
27007/27008/27009 > info sentinel
- cmd:
- 查看全部sentinel日志:假设27007为队长,27008/27009为队员:
z-res/sentinel日志解析.md
- 重新上线7007,再次查看任一sentinel的监视信息:发现三个sentinel都删除了对7007的主观下线,且其中一个sentinel执行了
+convert-to-slave将7007变更为当前master的slave:- cmd:
27007/27008/27009 > info sentinel
- cmd:
- tst:
c.y.s.JedisSentinelTest.jedisSentinel():构建sentinel连接池,获取连接,操作数据。
源码: /springdata-redis/
- conf:
7007.conf
# 以命令的方式设置 主节点或者从节点 就不再配置文件写了。7008,7009,同理
port 7007
dir ./sentinel
logfile 7007.log
dbfilename dump-7007.rdb
- conf:
27007.conf
# 27008,27009,同理
port 27007
dir ./sentinel
logfile 27007.log
protected-mode no
sentinel monitor my-master 127.0.0.1 7007 2
sentinel down-after-milliseconds my-master 5000
sentinel parallel-syncs my-master 1
sentinel failover-timeout my-master 15000
- res:
c.y.s.JedisSentinelTest
package com.yap.springdata2redis.jedis;
import org.junit.jupiter.api.Test;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPoolConfig;
import redis.clients.jedis.JedisSentinelPool;
import java.util.HashSet;
import java.util.Set;
/**
* @author yap
*/
class JedisSentinelTest {
private JedisPoolConfig jedisPoolConfig;
void init() {
jedisPoolConfig = new JedisPoolConfig();
jedisPoolConfig.setMaxTotal(1024);
jedisPoolConfig.setMaxWaitMillis(10000L);
jedisPoolConfig.setMaxIdle(200);
jedisPoolConfig.setMinIdle(0);
}
@Test
void jedisSentinel() {
init();
Set<String> sentinels = new HashSet<>();
sentinels.add("127.0.0.1:27007");
sentinels.add("127.0.0.1:27008");
sentinels.add("127.0.0.1:27009");
try (JedisSentinelPool jedisSentinelPool = new JedisSentinelPool("my-master", sentinels, jedisPoolConfig);
Jedis jedis = jedisSentinelPool.getResource()) {
if (!"PONG".equals(jedis.ping())) {
throw new RuntimeException("ping error...");
}
jedis.set("sentinel-key", "sentinel-val");
System.out.println(jedis.get("sentinel-key"));
} catch (Exception e) {
e.printStackTrace();
}
}
}
sentinel日志解析
-
队长日志:
+sdown master ..7007:对7007标记主观下线:我觉得7007挂了。+odown master ..7007 quorum 2/2:对7007标记客观下线:有2两个sentinel觉得7007挂了,那它就是挂了。+try-failover:尝试故障转移,只有一个sentinel执行故障转移操作。+vote-for-leader xxx 1:投1票给xxx作为sentinel队长。+elected-leader:当选队长。+selected-slave:选择一个slave,准备将其晋升为新的master。+failover-state-send-slaveof-noone:对该slave节点发送slaveof-noone命令。+failover-state-wait-promotion:对该slave节点执行slaveof-noone命令。+promoted-slave:slave晋升为master。+slave-reconf-sent/inprog/done:重写slave配置。-odown master 7007:删除对7007的客观下线标记,以便于它将来回归。+failover-end:故障转移完毕。+switch-master 7007 7008:完成master从7007到7008的切换。+slave slave 7009:使7009成为7008的从节点。+slave slave 7007:使7007成为7008的从节点,前提是7007复活。+sdown slave:对7007(此时它已降级为slave子节点)重新标记主观下线标记。
-
队员日志:
-
config-update-from:接收和同步sentinel队长的更新信息。
-
3. 分区集群
概念: 分区集群主要为了分担redis服务器压力,集群仅能使用一个库db0:
- redis默认采取虚拟槽方式进行分区以,主节点之间可以互知槽范围,尽量均匀分配槽以免数据倾斜,从节点不带槽。
z-res/数据分区方式.md
- 保持数据不倾斜:
- 在客户端提前获知所有节点的槽范围,访问时直接访问到对应的节点以解决效率问题。
- 集群中每个节点的配置内容保持统一且定期检查一致性。
数据分区方式.md
节点取余:
- 在客户端进行分区,先对数据key值进行哈希操作,然后再进行取余操作。
- 缺点:节点伸缩时数据需要迁移,建议翻倍扩容以减轻迁移量。
一致性哈希:
- 预设一个token环,数值范围是0~2^32。
- 在token环上均匀设置N个节点。
- 对数据的key值进行哈希操作,一定分布在两个节点范围内。
- 按照顺时针方向找到离这个哈希值最近的节点。
- 添加新节点时只会影响到两个节点,而不会影响其他的节点。
- 缺点:数据分步可能不均匀。
虚拟槽分区: redis-cluster默认使用:
- 预设虚拟槽0-16383范围,每个槽映射一个数据子集,一般比节点数大。
- 数据的key值进行对16383取余,结果一定分布在两个节点范围内。
3.1 原生安装
流程: 主要为了学习底层流程,工作中不建议使用:
- 开发配置文件
cluster/7001~7006.conf:配置端口号,工作目录,日志文件,RDB文件以及:cluster-enabled yes:开启集群模式。cluster-config-file nodes-7001.conf:集群相关信息的配置文件。cluster-node-timeout 15000:集群创建超时时间。cluster-require-full-coverage no:默认yes,表示当集群中有一个节点故障则整体不对外服务,建议关闭。
- 将6个节点配置为windows服务并启动,以7001为例:
- cmd:
redis-server --service-install cluster/7001.conf --service-name redis7001 - cmd:
redis-server --service-start --service-name redis7001
- cmd:
- 在任一节点如7001查看集群信息:也可以直接查看
nodes-7001.conf文件:- cmd:
7001 > cluster nodes:查看7001的集群节点连接情况。 - cmd:
7001 > cluster info:查看7001的集群信息。
- cmd:
- 用任一节点去meet其他5个节点,此时6个节点完成互通:
- cmd:
7001 > cluster meet 127.0.0.1 7002~7006:7001见面7002~7006。
- cmd:
- 为3个主节点分配槽,否则节点不可用,建议使用bat脚本循环为三个主节点分配槽以减少代码量:
- bat脚本格式:
for /L %循环变量 in (起始值,变化值,终止值) do 命令 - cmd:
for /L %i in (0,1,5461) do redis-cli -h 127.0.0.1 -p 7001 cluster addslots %i - cmd:
for /L %i in (5462,1,10922) do redis-cli -h 127.0.0.1 -p 7002 cluster addslots %i - cmd:
for /L %i in (10923,1,16383) do redis-cli -h 127.0.0.1 -p 7003 cluster addslots %i
- bat脚本格式:
- 在任一节点如7001查看槽信息和主从配置信息:
- cmd:
7001 > cluster slots
- cmd:
- 配置主从关系:7004从于7001,7005从于7002,7006从于7003:
- cmd:
7004/7005/7006 > cluster replicate 7001/7002/7003的nodeId
- cmd:
- 在任一节点如7001查看集群节点信息:重点关注主从关系是否搭建成功:
- cmd:
7001 > cluster nodes
- cmd:
- 集群方式连接任意两个节点并在集群中操作数据:
- cmd:
redis-cli -c -p 7001:-c表示集群方式连接以自动重定向到key对应槽位所在的节点并执行命令。
- cmd:
7001[-c] > set a 100 - cmd:
redis-cli -c -p 7002 - cmd:
7002[-c] > get a
- cmd:
- tst:
c.y.s.JedisClusterTest.jedisCluster():构建cluster连接池,获取连接,操作数据。 - tst:
c.y.s.jedis.JedisClusterTest.operateOnAllNodes():若需keys *类的命令能作用于全部节点:- 获取集群中的全部节点,包括从节点:
jedisCluster.getClusterNodes()
- 遍历并在每个主节点上进行
keys *操作:jedis.info("replication"):返回节点replication信息。’
- 获取集群中的全部节点,包括从节点:
源码: /springdata-redis/
- conf:
7001.conf
# 7001-7006 修改后5个节点个数字
port 7001
dir ./cluster
logfile 7001.log
dbfilename "dump-7001.rdb"
cluster-enabled yes
cluster-config-file nodes-7001.conf
cluster-node-timeout 15000
cluster-require-full-coverage no
- tst :
c.y.s.JedisClusterTest
package com.yap.springdata2redis.jedis;
import org.junit.jupiter.api.Test;
import redis.clients.jedis.*;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.TimeUnit;
/**
* @author yap
*/
class JedisClusterTest {
private Set<HostAndPort> nodes;
void init() {
nodes = new HashSet<>();
nodes.add(new HostAndPort("127.0.0.1", 7011));
nodes.add(new HostAndPort("127.0.0.1", 7012));
nodes.add(new HostAndPort("127.0.0.1", 7013));
nodes.add(new HostAndPort("127.0.0.1", 7014));
nodes.add(new HostAndPort("127.0.0.1", 7015));
nodes.add(new HostAndPort("127.0.0.1", 7016));
}
@Test
void jedisCluster() {
init();
try (JedisCluster jedisCluster = new JedisCluster(nodes, 1000, new JedisPoolConfig())) {
jedisCluster.set("jedis-cluster-name", "jedis-cluster-value");
System.out.println(jedisCluster.get("jedis-cluster-name"));
} catch (Exception e) {
e.printStackTrace();
}
}
@Test
void operateOnAllNodes() {
init();
try (JedisCluster jedisCluster = new JedisCluster(nodes, 10000, new JedisPoolConfig())) {
Map<String, JedisPool> clusterNodes = jedisCluster.getClusterNodes();
for (Map.Entry<String, JedisPool> entry : clusterNodes.entrySet()) {
Jedis jedis = entry.getValue().getResource();
// operate only on the master node
if (jedis.info("replication").contains("role:master")) {
System.out.println(jedis.keys("*"));
}
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
3.2 ruby安装
流程:
- 将redis根目录整体拷贝6份,分别取名
redis-7011~7016,视为6个redis实例。 - 在6个redis实例目录中开发配置文件
7011~7016.conf:配置端口,工作目录,日志,RDB文件,集群相关配置等: - 将6个节点配置为windows服务并启动,以7011为例:
- cmd:
redis-server --service-install 7011.conf --service-name redis7011 - cmd:
redis-server --service-start --service-name redis7011
- cmd:
- 安装ruby:傻瓜式安装,配置项可勾选后两项,表示添加环境变量以及关联相关文件:
z-res/rubyinstaller-2.2.4-x64.exe
- 下载 ruby驱动,选择3.2.1版本,并粘贴到ruby根目录中:
z-res/redis-3.2.1.gem
- 在ruby根目录中安装驱动:
- cmd:
ruby > gem install redis。
- cmd:
- 将redis源码的src目录中的集群脚本
redis-trib.rb分别拷贝到6个redis实例目录中:z-res/redis-win-3.2.100.zip
- 在任一包含
redis-trib.rb脚本的目录中搭建集群,数字1表示1主1从,0表示没有从节点:- cmd:
7011 > ruby redis-trib.rb create --replicas 1 127.0.0.1:7011 127.0.0.1:7012 127.0.0.1:7013 127.0.0.1:7014 127.0.0.1:7015 127.0.0.1:7016 Can I set the above configuration? (type 'yes' to accept):输入yes回车。
- cmd:
- 在任一节点如7011查看集群信息,槽信息和主从关系:
- cmd:
7011 > cluster nodes:
- cmd:
- 在任一包含
redis-trib.rb脚本的目录如7011中查看任一节点的数据分布,槽分布和主从信息:- cmd:
7011 > ruby redis-trib.rb info 127.0.0.1:7011
- cmd:
- 在任一包含
redis-trib.rb脚本的目录如7011中进行任一节点的数据均衡操作,线上慎用:- cmd:
7011> ruby redis-trib.rb rebalance 127.0.0.1:7011
- cmd:
- 集群方式连接任意两个节点并在集群中操作数据:
- cmd:
7011[-c] > set a 100 - cmd:
7012[-c] > get a
- cmd:
3.3 集群伸缩
流程: 集群伸缩指得就是槽和数据在节点之间的迁移,数据量越大迁移过程越较慢,但不会影响程序正常运行:
- 集群扩容:将redis根目录整体拷贝2份
redis-7017~7018,预设1主1从,配置和其他节点一致,并分别粘贴一个redis-trib.rb:- 将2个新节点部署成windows服务并启动,以7017为例:
- cmd:
redis-server --service-install 7017.conf --service-name redis7017 - cmd:
redis-server --service-start --service-name redis7017
- cmd:
- 将2个新节点加入到集群中:
- cmd:
7011 > cluster meet 127.0.0.1 7017/7018
- cmd:
- 对2个新节点配置主从关系:将7018配置为7017的从节点:
- cmd:
7018 > cluster replicate 7017的nodeId
- cmd:
- 对新的主节点入7017进行分配槽:
- cmd:
ruby redis-trib.rb reshard 127.0.0.1:7017 how many slots do you want to move?:给新ID总共分配16384/4=4096个槽。what is the receiving node ID:输入7017节点ID,该节点用于接收槽数据。please enter all the source node IDs:输入all,表示所有节点都为新节点分配槽数据,并生成计划。do you want to proceed with the porposed reshard plan:输入yes执行计划。
- cmd:
- 查看集群节点信息:7017的三段槽分别来自于其他所有节点:
- cmd:
7011 > cluster nodes
- cmd:
- 将2个新节点部署成windows服务并启动,以7017为例:
- 集群收缩:假设要删除7017/7018:
- 将7017的1366+1365+1365个槽尽量平均迁移到7011/7012/7013,以迁移到7011为例:
- cmd:
ruby redis-trib.rb reshard --from 7017的id --to 7011的id --slots 1366 127.0.0.1:7011
- cmd:
- 从集群中删除7017/7018节点,为了避免触发故障转移,先删除从节点,后删除主节点,以7018为例:
- cmd:
ruby redis-trib.rb del-node 127.0.0.1:7018 7018的id
- cmd:
- 查看集群节点信息:7018和7017已被移除集群:
- cmd:
7011 > cluster nodes
- cmd:
- 将7017的1366+1365+1365个槽尽量平均迁移到7011/7012/7013,以迁移到7011为例:
3.4 故障转移
流程: cluster集群内部实现了故障转移机制,无需使用sentinel:
- 带槽主节点master-b通过ping/pong机制发现与master-a通信超时或直接返回失败,对其标记主观下线
pfail。 - 带槽主节点master-c通过ping/pong机制发现与master-a通信超时或直接返回失败,对其标记主观下线
pfail。 - 当半数以上带槽主节点都对master-a标记了
pfail时,主观下线升级为客观下线,并向集群广播。 - 所有master-a的slave准备选举,但与master-a很久不联系的slave没有选举资格:
- 与master最后一次通信时间超过
cluster-node-timeout * cluster-slave-validity-factor时丧失资格。
- 与master最后一次通信时间超过
- 所有其他主节点对master-a的所有slave投票,与master-a最后通信时间越靠前的优先级越高。
- slave选举成功,执行
slave no one抛弃slave身份。 - 执行
clusterDelSlots命令将master-a的槽清空。 - 执行
clusterAddSlots命令将master-a的槽重新分配给新的主节点,并向集群广播。 - tst:
c.y.s.jedis.JedisClusterTest.failover():- 连入集群并利用循环每隔1秒钟向集群发送setex命令。
- 下线集群中的某master,如7011,查看节点选举和故障转移情况。
- 恢复集群中的某master,如7011,查看节点选举和故障转移情况。 源码: /springdata-redis/
- tst :
c.y.s.JedisClusterTest
@Test
void failover() {
init();
for (int i = 0; true; i++) {
try (JedisCluster jedisCluster = new JedisCluster(nodes, 1000, new JedisPoolConfig())) {
jedisCluster.setex("k" + i, 5, "v" + i);
System.out.println(jedisCluster.get("k" + i));
TimeUnit.SECONDS.sleep(1L);
} catch (Exception e) {
e.printStackTrace();
}
}
}