Redis Cluster
数据分布
单个数据库的容量已经无法满足容量需求,将数据按照一定规则分散到多台数据库服务器上,并且各个节点都是互相通讯的,客户端在存取数据时,根据要存取的数据也按照这个规则访问对应的数据库服务器,这样不仅扩大数据库容量,还能提高并发量,甚至能提升网络带宽,这就是分布式的基本原理
范围分区
顺序范围:根据数据的某个属性范围进行分区,属性可以是时间、索引字段(key)等,比如:今天的数据和明天的数据分布在不同节点上,索引范围是1到100、101到200分布在不同节点上
列表范围:根据数据库表中的多列内容相同情况下进行分区,比如:描述分类的字段,将不同类别的数据分布在不同节点上
优点:数据分布与业务相关,可顺序访问
缺点:数据分散容易倾斜
哈希分区
根据哈希函数计算出数据的哈希值,根据哈希值将数据分布在不同节点上
优点:数据分散度高不易倾斜
缺点:数据分布与业务无关,不可顺序访问
节点取余分区
计算出数据的哈希值后,将哈希值与节点个数进行取余,余数为几就将数据分布到编号为几的节点上
问题:若添加或删除一个节点,就需要对数据进行重新计算哈希并取余,数据迁移高达80%
解决:成倍伸缩节点,可减少数据迁移量到50%
一致性哈希分区
约定长度2^32^位的哈希环(可以理解为哈希值的范围,将范围看作一个环),将每个节点均匀分布到环上,计算出的哈希值一定落在环上,顺时针的去选择节点对该数据进行操作
解决的问题:若添加或删除一个节点时,会添加到环上,必然是在两个节点之间,所以只会影响到相邻的两个节点,不会影响其他节点
问题:任然存在少量的数据迁移,而且还会导致数据分布不均匀
解决:成倍伸缩节点,可以使数据分布均匀
虚拟槽(节点)分区
预设均匀的虚拟槽,每个槽映对应环上一个虚拟节点,再让用户决定虚拟节点到实际节点进行映射,达到分布情况由用户设置决定的目的,Redis采用CRC16哈希算法进行哈希值的计算,再对16383(即16383个槽)取模来决定节点归那个虚拟节点管,从而映射到实际节点上
优点:即时添加新节点,也不会立刻数据迁移,只有用户将虚拟槽分配给新节点才会发生数据迁移,从而减少数据迁移过程中丢数据的可能性
架构
原理
智能客户端
redis-cli -c
使用c参数可以开启集群模式下的客户端,当发生以下异常时会自动跳转
概念
- moved重定向:随机选取节点进行命令的执行,若命中直接返回结果,否则会返回一个moved异常告知客户端应该访问的节点,即槽已经迁移,客户端再进行跳转
- ask重定向:执行命令的节点正在进行槽的迁移,该节点会返回一个ask异常,告知客户端当前槽是在当前节点,但是数据已经迁移到了另一个节点,即槽在迁移过程中,数据已经迁移,客户端先给另一个节点发送
asking
命令,再执行要执行的命令
步骤
- 从集群中选一个可运行节点,使用
clutser slots
命令获取槽和节点之间的映射 - 将映射结果存储本地,为每个节点创建连接池
- 准备执行命令,即本地对key进行计算出对应的槽,从映射中获取槽对应的节点,再进行命令的执行
- 若连接出错有以下几种情况
- 普通的连接超时:重连即可
- ask异常:根据ask异常进行跳转即可
- moved异常:先根据moved异常进行跳转,同时使用
clutser slots
命令刷新槽和节点之间的映射,再执行第3步,若期间连续多次出现了moved异常,则可能集群有问题
批量操作
集群中无法使用mget
和mset
的批量操作的,除非这些key都在一个槽中,这显然是不现实的
方案 | 原理 | 优点 | 缺点 | 网络IO |
---|---|---|---|---|
串行get/set | 遍历所有的key并单个的执行get/set操作 | 编程简单,少量keys满足需求 | 大量keys请求延迟严重 | O(keys) |
串行IO | 将所有访问相同节点的key进行汇总,使用管道进行命令传输 | 编程较简单,少量node满足需求 | 大量node延迟严重 | O(nodes) |
并行IO | 多个线程并行的使用管道将汇总命令进行传输 | 由于是并行,延迟取决于最慢的节点 | 编程复杂,超时定位问题难 | O(max_slow(node)) |
hash_tag | 当一个key包含{}时,就会仅对{}包括的字符串做hash,就可以只访问一个节点 | 性能最高 | 读写增加tag维护成本,tag分布易出现数据倾斜 | O(1) |
故障转移
集群中各个节点进行互相监控
主观下线与客观下线
- 主观下线:单个节点对某个节点是否在线的看法
- 在定时的心跳检测过程中,两次心跳超过
cluster-node-timeout
设置的时间就会标记为主观下线
- 在定时的心跳检测过程中,两次心跳超过
- 客观下线:半数以上有槽的主节点对某个节点是否在线的看法
- 将其他节点发送的认为主观下线的消息存入一个故障链表中,节点可以知道有多少主节点主观下线
- 故障链表是存在有效期,有效期为
cluster-node-timeout
乘2,防止之前的主观下线消息长久存在于故障链表中 - 当达有效的主观下线到达半数以上时,就将节点更新为客观下线,并向集群广播节点下线,并做故障转移
故障恢复
-
对从节点进行资格检查
- 每个从节点检查与故障主节点的断线时间,超过
cluster-node-timeout
乘cluster-slave-validity-factor
(默认是10)的从节点会取消资格
- 每个从节点检查与故障主节点的断线时间,超过
-
准备选举时间,为了使偏移量最大的从节点更有机会成为主节点
- 对各个符合资格的从节点按照偏移量进行排序,偏移量越大的节点准备选举时间越小,因为越早被选举,后续投票阶段可获得更多票数
-
所有可用的主节点对参加竞选的从节点进行选举投票,当半数以上主节点的票数时,就看升级为主节点
-
替换主节点
- 在选中的从节点上执行
slaveof no one
命令,即取消复制成为主节点 - 执行
cluster del slot
命令撤销故障主节点负责的槽,执行cluster add slot
命令把这些槽分配给自己 - 向集群广播自己已经替换了故障从节点的消息
- 在选中的从节点上执行
集群伸缩
对于主节点来说,不管是添加还是删除节点,都需要进行槽和数据的迁移,对于从节点来说就不需要进行槽和数据的迁移了,迁移前应该先指定槽的迁移计划,尽量的让槽分布均匀和小量的数据迁移
- 对目标节点发送
cluster setslot 某个槽 importing 目标节点nodeid
命令,让目标节点准备导入该槽中的数据,目标节点nodeid为了确认就是找这个节点 - 对源节点发送
cluster set 某个槽 migrating 源节点nodeid
命令,让源节点准备导出该槽中数据,源节点nodeid为了确认就是找这个节点 - 在源节点循环执行
cluster setkeysinlot 某个槽 指定个数的key
命令,获取指定个数个key后,在执行migrate 目标节点IP 目标节点端口 key 0 超时时间
将数据迁移到目标节点上,直到所有key都迁移完 - 向集群内所有节点发送
cluster setslot 某个槽 node 目标节点nodeid
命令,告知集群内所有节点槽已经分配给目标节点
添加节点
- 以集群模式开启要添加的Redis节点,并且配置要与集群中的节点配置统一
- 在已启动的孤立的节点中执行
cluster meet
命令指定要连接的节点IP和端口,加入集群 - 若是主节点则需做槽和数据的迁移
删除节点
- 要下线节点若有槽需先做槽和数据的迁移
- 对所有节点执行
cluster forget 要忘记节点的nodeid
命令,通知所有节点删除该节点,需要在60s之内都通知到所有,否则无效
实现方式
在某个Redis节点上执行cluster nodes
命令可进行查看单个Redis节点的信息
在任意Redis节点上执行cluster info
命令可以查看整个集群的信息
在任意Redis节点上执行cluster slots
命令可以查看到整个集群中槽分配的情况,主从节点应该分配的槽是一样的,在同一组
原生命令
-
以集群模式开启多个Redis节点
配置项 含义 默认值 cluster-enabled
节点是否开启集群模式 yes cluster-node-timeout
节点主观下线超时时间,单位毫秒 15000 cluster-config-file
自动生成的集群节点配置文件名,也是 cluster info
命令的执行结果内容nodes.conf cluster-require-full-coverage
集群完整性参数,是否需要所有节点全都可用,集群才能对外服务 yes -
选择一个节点执行
cluster meet
命令指定要连接的节点IP和端口,连接一遍所有节点即可,因为Gossip协议会让其他节点互相感知 -
在每个节点上使用
cluster addslots
指定槽,该命令无法指定某个范围,可以通过shell脚本来指定范围port=$1 #第一个参数为Redis节点端口 start=$2 #起始槽 end=$3 #结束槽 for slot in `seq ${start} ${end}` #遍历起始到结束位置 do echo "slot:${slot}" redis-cli -p ${port} cluster addslots ${slot} #执行添加槽命令 done
-
在从节点上使用
cluster replicate
指定nodeid,来设置对应的主节点
官方工具
若是3.0版本redis需要在Redis源码包的src目录中找到名为redis-trib的ruby脚本,若是5.0及以上版本以及集成到redis-cli中,无需以下操作
- 安装Ruby环境,使用
yum install ruby
安装,默认安装的是2.0版本 - 安装Ruby的redis客户端,使用
gem install redis -v 3.0.7
安装 - 以集群模式开启多个Redis节点
- 执行redis-trib脚本
#3.0版本
./redis-trib.rb create --replicas 1 \ #创建集群每个主节点有一个从节点
127.0.0.1:6379 127.0.0.1:6380 127.0.0.1:6381 \ #主节点
127.0.0.1:6382 127.0.0.1:6383 127.0.0.1:6384 #从节点,一一对应
#5.0版本
redis-cli --cluster create --cluster-replicas 1 \ #创建集群每个主节点有一个从节点
127.0.0.1:6379 127.0.0.1:6380 127.0.0.1:6381 \ #主节点
127.0.0.1:6382 127.0.0.1:6383 127.0.0.1:6384 #从节点,一一对应
客户端使用
jedisCluster
Set<HostAndPort> nodeList = new HashSet<>(); //存放所有节点
nodeList.add(new HostAndPort("127.0.0.1", 6379));
nodeList.add(new HostAndPort("127.0.0.1", 6380));
nodeList.add(new HostAndPort("127.0.0.1", 6381));
nodeList.add(new HostAndPort("127.0.0.1", 6382));
nodeList.add(new HostAndPort("127.0.0.1", 6383));
nodeList.add(new HostAndPort("127.0.0.1", 6384));
JedisPoolConfig jedisPoolConfig = new JedisPoolConfig(); //若不进行配置,则使用的是默认配置
JedisCluster jedisCluster = new JedisCluster(nodeList, jedisPoolConfig);
//之后直接使用jedisCluster执行命令即可,无需关心连接池归还,内部已经封装
批量操作
Map<String, JedisPool> jedisPoolMap = jedisCluster.getClusterNodes(); //获取所有节点的连接池
for (Map.Entry<String, JedisPool> entry : jedisPoolMap.entrySet()) { //遍历所有连接池
Jedis jedis = null;
try {
jedis = entry.getValue().getResource(); //每个连接池中获取一个连接
//jedis.auth("redis"); //操作数据前设置的密码
if (jedis.info("replication").contains("master")) {
//若是主节点才进行操作
}
} finally {
if (jedis != null) {//需手动归还Jedis连接
jedis.close();
}
}
}
节点运维
数据迁移
#3.0版本
./redis-trib.rb reshard 127.0.0.1:6379
#指定集群中的任意一个节点即可
#首先会打印出集群信息,并提示需要迁移多个槽
#接着会提示需要将槽迁移到哪个节点上,需要填目标的nodeid
#之后会提示槽从哪些节点中迁出,若填写all则待迁移的槽在剩余节点中平均分配,也可从指定节点中迁出
./redis-trib.rb info 127.0.0.1:6379
#指定集群中的任意一个节点即可,查看集群中数据和槽的分布情况
./redis-trib.rb rebalance 127.0.0.1:6379
#指定集群中的任意一个节点即可,将未均匀分配槽和数据的集群均匀分配
#5.0版本
redis-cli --cluster reshard 127.0.0.1:6379
redis-cli --cluster info
redis-cli --cluster rebalance 127.0.0.1:6379
节点下线
#3.0版本
./redis-trib.rb del-node 127.0.0.1:6385 9eba8a6900357b84be7746dfb45b7e20f44430d7
#第一个是要删除的节点,第二个是要删除的节点的nodeid
#5.0版本
redis-cli --cluster del-node 127.0.0.1:6385 9d183bfc9bcbf135573c5891f23aae635b1e864a
节点上线
#3.0版本
./redis-trib.rb add-node 127.0.0.1:6385 127.0.0.1:6379
#第一个是要添加的新节点,第二个是已存在集群中的任意节点
./redis-trib.rb add-node --slave --master-id a2370763fcf1ff70e3b6cf08d12c677f7c126fd7 127.0.0.1:6385 127.0.0.1:6379
#添加从节点时需要指定--slave参数
#--slave和--master-id必须写在前面,否则会报错
# 若不设置--master-id,则会随机选择主节点
#5.0版本
redis-cli --cluster add-node 127.0.0.1:6385 127.0.0.1:6379
redis-cli --cluster add-node --cluster-slave --cluster-master-id 7a735a2093c4f46e958aade004fe562ae2cf03fe 127.0.0.1:6385 127.0.0.1:6379
集群VS单机
对比项 | 集群 | 单机 |
---|---|---|
带宽消耗 | 集群规模越大越消耗带宽 | 带宽消耗小 |
发布订阅 | 集群内全部节点都会发布消息,会加大带宽 | 不会加大带宽 |
数据/请求倾斜 | 可能会发生 | 不会发生 |
bigKey分区 | 不支持,尽量少使用大key,减少数据倾斜 | 支持 |
数据迁移 | 只能从单机到集群,使用./redis-trib.rb import --from 源 目标集群任意节点 命令 |
持久化文件 |
批量操作 | 支持有限,在单个节点可以 | 支持 |
事务操作 | 支持有效,在单个节点可以 | 支持 |
主从复制 | 每个主节点只支持一层从节点 | 支持多层 |
多数据库 | 不支持 | 支持 |
综上所述分布式Redis不一定好,大多数情况下Redis Sentinel已经满足需求
Comments NOTHING