Redis 在 3.0 版本中提供了 Redis Cluster (集群) 来满足分布式的需求。Redis Cluster 采用无中心结构,每个节点保存数据和整个集群的状态,每个节点都和其他所有节点连接,节点之间使用流言协议 (Gossip Protocols) 去传播信息以及发现新的节点。

Redis Cluster 的主要目的是将不同的 key 分散放置到不同的 Redis 节点。

1.分片

分片:按照某种规则去划分数据库,分散存储在多个节点上。

常见的两种分片方式是顺序分区和 Hash 分区:

说明

特点

典型产品

顺序分区

按顺序进行分区

数据分散度易倾斜,键值业务相关,可顺序访问,支持批量操作

BigTable、HBase

哈希分区

key 取 Hash 进行分区

数据分散度高,键值分布业务无关,无法顺序访问,支持批量操作

一致性哈希Memcache、Redis Cluster

Hash 分区主要有三种方式:

1、节点取余分区

客户端分片,分区位置 = hash(key) % nodes,nodes 指节点数。

存在的问题:如果扩容,约 80% 的数据会做漂移,如果是翻倍扩容,约 50% 的数据会做漂移,大量的漂移会影响系统性能,建议翻倍扩容。

2、一致性哈希分区

客户端分片,分区位置 = hash(key) % (2 的 32 次方),是对节点取余分区的一个优化,为数据做一个 Hash 环,token = 0 ~ 2 的 32 次方,然后为每一个节点分配一个 token 范围,然后根据 hash(key) % (2 的 32 次方) 值顺时针去找节点名。

cluster java配置 redis redis cluster keys_故障转移

存在的问题:如果在 node1 和 node2 之间加一个 node5,node1 到 node5 之间的哈希就会落在 node5 上了,数据仍然会存在漂移,但是有一个好处是,添加节点后,不会影响 node1、node3、node4 的数据,影响范围会小很多。随着节点数量的增加,漂移影响范围也会越来越小。适合节点比较多的情况。

3、虚拟槽分区

预设虚拟槽:每个槽映射一个数据子集,一般比节点数大。

Redis Cluster 使用的分区方式,服务端分片。Redis Cluster 中有一个16384 (0~16383) 长度的虚拟槽。

分区位置 = hash(key) % 16383。

虚拟槽分配:

cluster java配置 redis redis cluster keys_故障转移_02


cluster java配置 redis redis cluster keys_哨兵_03

像节点取余分区和一致性哈希分区都有一个问题,就是添加节点之后,数据会进行漂移,存在丢数据的可能性,只能作为缓存场景来使用。而虚拟槽分区是不存在这样的问题的,因为每个槽负责的范围是固定的,加了新节点,也不会把其他节点的槽抢过去。

1.基本架构

cluster java配置 redis redis cluster keys_哨兵_04

节点:Redis Cluster 中会有多个节点,节点之间是相互通信的,且每个节点都负责读写。

meet 操作(gossip 协议):节点之间相互通信的基础。假如现在有 5 个节点,node1 节点对 node2、node3、node4、node5 节点分别发送了一个 meet 操作,node2 等节点会各自返回一个 pong 命令(表示 Redis 服务运行正常),其他节点可以自动找到,最终所有节点都可以相互通信。

分配槽:需要给节点分配虚拟槽。

节点

虚拟槽(slot)

node1

0~3276

node2

3277~6553

node3

6554~9829

node4

9830~13105

node5

13106~16383

对于客户端来说,只需要计算 slot = hash(key) %16383。

复制:为了保证高可用,每一个节点都有一个 slave 节点。

2.搭建集群

配置开启 Redis,原生命令安装和官方工具安装这一步是一样的。这里 Redis 五个节点用五个端口进行区分,分别是 7000、7001、7002、7003、7004。

Redis 节点 redis/config/redis-7000.conf 配置(redis.conf 模板文件在 redis/redis.conf,这里只给出一个节点配置,其余节点只能端口号不同):

# 关闭保护模式
protected-mode no
# 配置启动端口
port 7000
# 配置后台启动
daemonize yes
# 修改pidfile指向路径 redis-${port}.pid
pidfile /var/run/redis-7000.pid
# 日志记录方式 redis-${port}.log
logfile "redis-7000.log"
# 配置dump数据存放目录
dir "/opt/soft/redis/data/"
# 配置dump数据文件名 redis-${port}.rdb
dbfilename dump-7000.rdb
# 开启集群模式
cluster-enabled yes
# cluster节点超时时间,毫秒
cluster-node-timeout 15000
# cluster配置文件
cluster-config-file "nodes-7000.conf"
# 是否需要集群内所有节点都能提供服务才认为集群是正确的,默认yes
cluster-require-full-coverage no

启动命令:

# redis-server redis-7000.conf

继续操作,分别启动剩余 7001、7002、7003、7004 端口的节点。此时各个节点没有进行任何通信,各自都是孤立的。

1.原生命令安装(理解架构)

首先进行 meet 操作(gossip 协议):

# redis-cli -h 127.0.0.1 -p 7000 cluster meet 127.0.0.1 -p 7001   //在 7000 上执行命令,7000 端口的 redis 节点 meet 7001端口的 redis 节点
# redis-cli -h 127.0.0.1 -p 7000 cluster meet 127.0.0.1 -p 7002
# redis-cli -h 127.0.0.1 -p 7000 cluster meet 127.0.0.1 -p 7003
# redis-cli -h 127.0.0.1 -p 7000 cluster meet 127.0.0.1 -p 7004

然后分配槽:

# redis-cli -h 127.0.0.1 -p 7000 cluster addslots {0...3276}   //在 7000 上执行命令
# redis-cli -h 127.0.0.1 -p 7001 cluster addslots {3277...6553}
# redis-cli -h 127.0.0.1 -p 7002 cluster addslots {6554...9829}
# redis-cli -h 127.0.0.1 -p 7003 cluster addslots {9830...13105}
# redis-cli -h 127.0.0.1 -p 7004 cluster addslots {13106...16383}

这样所有槽分配之后,集群就算基本建立完成了。

最后需要设置主从(只有有了主从关系后,才可以实现故障自动转移):

# redis-cli -h 127.0.0.1 -p 8000 cluster replicate ${node-id-7000}   //在 8000 上执行命令

node-id 是指集群的一个节点 id,在集群启动的时候就会进行分配,需要注意的是,这里的 node-id 非单机节点的 runid,runid 重启会重置,node-id 重启不会重置。

2.官方工具安装(生产推荐)

Redis Cluster 官方提供了 Ruby 的安装脚本,相比于原生命令安装要容易很多。

Linux 上安装 Ruby 环境:

# wget -P /usr/local http://cache.ruby-lang.org/pub/ruby/2.6/ruby-2.6.3.tar.gz
# cd /usr/local
# tar -zxvf ruby-2.6.3.tar.gz                   //解压缩
# cd ruby-2.6.3
# ./configure -prefix=/usr/local/ruby                       //配置
# make
# make install                                 //安装
# cd /usr/local/ruby
# cp bin/ruby /usr/local/bin
# cp bin/gem /usr/local/bin

安装 Ruby Redis 客户端:

# wget -P /usr/local http://rubygems.org/downloads/redis-4.1.2.gem
# cd /usr/local
# gem install -l redis-4.1.2.gem   //安装rubygem redis
# gem list --check redis gem
# cp /usr/local/redis/src/redis-trib.rb /usr/local/bin   //安装redis-trib.rb

redis-trib 安装 Redis Cluster:

一键开启:

./redis-trib.rb create --replicas 1 127.0.0.1:7000 127.0.0.1:7001 127.0.0.1:7002 127.0.0.1:7003 127.0.0.1:7004 127.0.0.1:8000 127.0.0.1:8001 127.0.0.1:8002 127.0.0.1:8003 127.0.0.1:8004

这个命令表示创建集群,–replicas 1 表示每个主节点配备一个从节点,前五个 7000 到 7004 的端口表示主节点,后五个 8000 到 8004 的端口表示从节点,7000 对应 8000,7001 对应 8001,以此类推。

相比原生命令安装,官方工具安装更高效、准确,生成环境可使用。

4.集群伸缩

1.伸缩原理

一个 node1、node2、node3 组成的集群,加入 node4 的过程,其实就是槽和数据在节点之间的移动。

cluster java配置 redis redis cluster keys_故障转移实现原理_05

2.扩容集群

127.0.0.1:7000 127.0.0.1:7001 127.0.0.1:7002 组成的集群,加入 127.0.0.1:7003。

1.原生命令安装(理解架构)

1、准备新节点

需要新节点是集群模式(cluster-enabled yes),配置需要和其他节点统一,然后启动改节点。

2、加入集群

通过 meet 操作(gossip 协议)来完成的:

# redis-cli -h 127.0.0.1 -p 7000 cluster meet 127.0.0.1 -p 7003   //在 7000 上执行命令,7000 端口的 redis 节点 meet 7003端口的 redis 节点
# redis-cli -h 127.0.0.1 -p 7000 cluster nodes   //加入集群,观察集群配置

3、迁移槽和数据

首先进行槽迁移计划,平均槽数据,计算每个节点应该迁移到新节点的槽的数量。

然后迁移数据,迁移数据的过程是比较复杂的:

1)对目标节点发送 cluster setslot {slot} importing {sourceNodeId} 命令,让目标节点准备导入槽数据;
2)对源节点发送 cluster setslot {slot} migrating {targetNodeId} 命令,让源节点准备迁出槽的数据;
3)源节点循环执行 cluster getkeysinslot {slot} {count} 命令,每次获取 count 个属于槽的键;
4)在源节点上执行 migrate {targetIp} {targetPort} key 0 {timeout} 命令把指定 key 迁移;
5)重复执行 3~4 直到槽下所有的键数据迁移到模板节点;
6)向集群内所有 master 节点发送 cluster setslot {slot} node {targetNodeId} 命令,通知槽分配给目标节点。

迁移数据的完整流程图:

cluster java配置 redis redis cluster keys_故障转移实现原理_06

4、添加从节点

2.官方工具安装(生产推荐)

扩容集群命令:./redis-trib.rb add-node new_host:new_port existing_host:existing_port --slave --master-id < arg>

# ./redis-trib.rb add-node 127.0.0.1:7003 127.0.0.1:7000

建议使用 redis-trib.rb,能够避免新节点已经加入了其他集群,造成故障。

2.缩容集群

127.0.0.1:7000 127.0.0.1:7001 127.0.0.1:7002 127.0.0.1:7003 组成的集群,缩容 127.0.0.1:7003。

1.原生命令安装(理解架构)

1、下线迁移槽

将下线节点持有的槽均匀的迁移到其他节点,迁移命令跟扩容集群的命令相同,不再赘述。

2、忘记节点

# redis-cli -h 127.0.0.1 -p 7000 cluster forget 7003   //在7000节点上执行,让7000忘记7003节点

这个命令有一个注意点,60s 后如果集群中还有节点没有忘记该节点,则集群中仍然会扩散消息,所以想要节点真正下线,需要对所以节点执行忘记节点操作。

3、关闭节点

2.官方工具安装(生产推荐)

1、集群缩容

集群缩容命令:

# ./redis-trib.rb reshard --from {7003 nodeid} --to {7000…7002 nodeid} --slots 1366 127.0.0.1:7003   //任一端口上执行,分别迁移槽位到之前的三个主节点

建议使用 redis-trib.rb,能够避免新节点已经加入了其他集群,造成故障。

2、下线节点

先下线从节点,再下线主节点。

# ./redis-trib.rb del-node 127.0.0.1:7000 {7003nodeid}
# ./redis-trib.rb del-node 127.0.0.1:7000 {8003nodeid}

下线节点包含了从集群中 remove 节点、从集群中 forget 节点、shutdown 节点。

3、忘记节点

# redis-cli -h 127.0.0.1 -p 7000 cluster forget {nodeId}
# redis-cli -p cluster slots   //查看节点情况

5.客户端路由

1.moved重定向

moved 异常:

cluster java配置 redis redis cluster keys_故障转移_07

槽命中,直接返回:

cluster java配置 redis redis cluster keys_Redis Sentinel_08

槽不命中,moved 异常:

cluster java配置 redis redis cluster keys_Redis Sentinel_09

2.ask重定向

在集群扩容缩容的时候,会对槽进行迁移,槽的迁移是遍历槽中的 key,然后逐步执行 migrate 命令把指定 key 迁移,这个操作本身是比较慢的,假如此时客户端记录了槽的信息是在源节点,此时去访问,发现 key 已经迁移到目标节点了,这个时候就引出了 ask 重定向。

ask 异常:

cluster java配置 redis redis cluster keys_客户端连接_10

moved 重定向和 ask 重定向两者都是客户端重定向,不同是 moved 槽已经确定迁移,ask 槽还在迁移中 。

3.smart客户端

1、smart 客户端使用:例如 Java 客户端 JedisCluster。

2、smart 客户端原理

smart 客户端首要目标就是追求性能,不可以使用代理模式(影响性能),Redis 作者建议直连对应槽的节点,但是碰到 moved 异常和 ask 异常需要做兼容,基本过程是这样的:

  1. 从集群中选一个可运行节点,执行 cluster slots 命令初始化槽和节点映射;
  2. 将 cluster slots 的结果映射到本地,为每个节点创建 JedisPool;
  3. 准备执行命令。

执行命令的基本过程:

cluster java配置 redis redis cluster keys_故障转移实现原理_11

3、JedisCluster 执行源码分析

打开 JedisCluster 类,看下 set 方法的实现:

@Override
public String set(final String key, final String value) {
    return new JedisClusterCommand<String>(connectionHandler, maxAttempts) {
        @Override
        public String execute(Jedis connection) {
            return connection.set(key, value);
        }
    }.run(key);
}

可以看到使用了一个匿名内部类然后进行封装,跟进去看下 JedisClusterCommand 的实现:

public abstract class JedisClusterCommand<T> {
    /**
     * 在cluster初始化的时候,它会帮我们初始化所有的槽和节点的对应关系
     */
    private final JedisClusterConnectionHandler connectionHandler;
    /**
     * 最大尝试次数
     */
    private final int maxAttempts;

    public JedisClusterCommand(JedisClusterConnectionHandler connectionHandler, int maxAttempts) {
        this.connectionHandler = connectionHandler;
        this.maxAttempts = maxAttempts;
    }

    public abstract T execute(Jedis connection);

    public T run(String key) {
        if (key == null) {
            throw new JedisClusterException("No way to dispatch this command to Redis Cluster.");
        }
        return runWithRetries(JedisClusterCRC16.getSlot(key), this.maxAttempts, false, null);
    }

    private T runWithRetries(final int slot, int attempts, boolean tryRandomNode, JedisRedirectionException redirect) {
        // 尝试初始化的次数,attempts默认传入5
        if (attempts <= 0) {
            throw new JedisClusterMaxRedirectionsException("Too many Cluster redirections?");
        }
        // jedis连接
        Jedis connection = null;
        try {
            // 第一次执行的时候redirect是null
            if (redirect != null) {
                connection = this.connectionHandler.getConnectionFromNode(redirect.getTargetNode());
                if (redirect instanceof JedisAskDataException) {
                    // Ask异常
                    // TODO: Pipeline asking with the original command to make it faster....
                    connection.asking();
                }
            } else {
                // 是否尝试随机节点,第一次执行的时候tryRandomNode是false
                if (tryRandomNode) {
                    connection = connectionHandler.getConnection();
                } else {
                    // 通过JedisClusterCRC16.getSlot(key)算出key对应的槽,通过槽获取对应的连接
                    connection = connectionHandler.getConnectionFromSlot(slot);
                }
            }
            // 执行命令
            return execute(connection);
        // 无节点可达,直接抛出异常
        } catch (JedisNoReachableClusterNodeException jnrcne) {
            throw jnrcne;
        // 连接异常
        } catch (JedisConnectionException jce) {
            // 释放当前连接
            releaseConnection(connection);
            connection = null;

            if (attempts <= 1) {
                //We need this because if node is not reachable anymore - we need to finally initiate slots
                //renewing, or we can stuck with cluster state without one node in opposite case.
                //But now if maxAttempts = [1 or 2] we will do it too often.
                //TODO make tracking of successful/unsuccessful operations for node - do renewing only
                //if there were no successful responses from this node last few seconds
                // 刷新本地slot缓存
                this.connectionHandler.renewSlotCache();
            }
            return runWithRetries(slot, attempts - 1, tryRandomNode, redirect);
        // 重定向异常
        } catch (JedisRedirectionException jre) {
            // if MOVED redirection occurred,
            if (jre instanceof JedisMovedDataException) {
                // it rebuilds cluster's slot cache recommended by Redis cluster specification
                this.connectionHandler.renewSlotCache(connection);
            }
            // release current connection before recursion
            releaseConnection(connection);
            connection = null;

            return runWithRetries(slot, attempts - 1, false, jre);
        } finally {
            // 释放当前连接
            releaseConnection(connection);
        }
    }
}

执行命令就回到 JedisCluster 类,看下 execute 这个模板方法:

@Override
public String set(final String key, final String value) {
    return new JedisClusterCommand<String>(connectionHandler, maxAttempts) {
        @Override
        public String execute(Jedis connection) {
            return connection.set(key, value);
        }
    }.run(key);
}

这样就完成了一次 Redis 命令的操作。

4、JedisCluster 多节点命令实现

// 获取所有节点的JedisPool
Map<String, JedisPool> jedisPoolMap = JedisCluster.getClusterNodes();
// 遍历
for(Entry<String, JedisPool> entry: jedisPoolMap.entrySet()) {
    // 获取每个节点的Jedis连接
    Jedis jedis = entry.getValue().getResource();
    // 只需要在master节点执行命令
    if(!isMaster(jedis)) {
        continue;
    }
    // 执行命令
    
    // finally close
}

5、JedisCluster 批量操作实现

Redis Cluster 在执行 mget mset 时,要求 key 必须在一个槽上,这种条件是十分苛刻的。下面来看下四种批量优化的方法:

方法

描述

优点

缺点

时间复杂度

串行 mget

写一个 for 循环,遍历 keys,分别去对应节点拿到对应值,然后做汇总。

编程简单,少量 keys 满足需求

大量 keys 请求延迟严重

O(keys)

串行 IO

对串行 mget 做了一个优化,在客户端本地做了一个内聚,算出每个 key 对应的槽,然后按照槽对 key 进行分组,分组数为 nodes,然后执行 nodes 次 pipeline 就能完成取值。

编程简单,少量节点满足需求

大量 node 延迟严重

O(nodes)

并行 IO

对串行 IO 的一个改变,分组之后可以启动一定数量线程,并行执行 pipeline。

利用并行特性,延迟取决于最慢的节点

编程复杂,超时定位问题难

O(max_slow(node))

hash_tag

在客户端对 key 进行 hash_tag 的包装 ,{tag}key,mget(hash_tag),这样只需要去一个节点取就可以了

性能最高

读写增加 tag 维护成本,tag 分布易出现数据倾斜

O(1)

6.集群实现原理

1.故障转移

Redis Cluster 自身实现了高可用,不需要安装 Redis Sentinel(哨兵)。如果某个节点出现了故障,Redis Cluster 可以通过其他节点对当前节点的监控,然后根据一定方法,实现故障转移。

1、故障发现

通过 ping/pong 消息实现故障发现(不需要 Sentinel)。

下线也分为主观下线(某个节点认为另一个节点不可用)和客观下线(当半数以上持有槽的主节点都标记某节点不可用)。

主观下线流程:

cluster java配置 redis redis cluster keys_Redis Sentinel_12

客观下线流程:

接收到其他节点发来的 PING 消息,如果包含了 pfail(主观下线)消息,就会将 pfail 消息的内容添加到自身的故障列表中,这个故障列表包含了当前节点收到的每一个节点对其他节点的信息。

cluster java配置 redis redis cluster keys_哨兵_13

尝试客观下线流程:

通知集群内所有节点标记故障节点为客观下线;通知故障节点的从节点触发故障转移流程。

cluster java配置 redis redis cluster keys_哨兵_14

2、故障恢复

发生客观下线之后,客观下线通知给从节点,从节点接收到消息后,就开始做故障恢复,从而保证集群的高可用,主要分为资格检查、准备选举时间、选举投票、替换主节点。

资格检查:

1)每个从节点检查与故障主节点的断线时间;
2)如果超过了 cluster-node-timeout * cluster-slave-validity-factor 取消资格,cluster-node-timeout 默认 15 秒,cluster-slave-validity-factor 默认是 10。

准备选举时间:

当资格检查通过后,需要更新触发故障选举的时间,只有达到该时间才有可能触发后续的流程。为了保证偏移量比较大的从节点有更小的延迟,达到选举时间,我们会给它更小的选举时间,让它首先达到选举时间,获得更多的票数,最终成为 master 节点。

偏移量越大,数据越接近主节点,更有可能成为 master 节点。

选举投票:

从节点达到选举时间之后,它会让主节点发起一个选举,然后主节点进行投票。偏移量越大,选举时间越短,更有可能获得更多的票数。

替换主节点:

1)当前从节点取消复制变为主节点,即 slave no one;
2)执行 clusterDelSlot 撤销故障主节点负责的槽,并执行 clusterAddSlot 把这些槽分配给自己;
3)向集群广播自己的 PONG 消息,表明已经替换了故障从节点。

7.开发运维场景问题

1.集群完整性

集群完整性实际上指的就是 cluster-require-full-coverage 参数,表示是否需要集群中的所有节点都是一个在线的状态,而且所有槽都在服务的状态,才会认为集群是完整的,默认为 yes。

  • 集群中 16384 个槽全部可用:保证集群完整性;
  • 节点故障或者正在故障转移会报 (error) CLUSTERDOWN The cluster is down;

集群完整性大多数业务无法容忍,cluster-require-full-coverage 参数建议设置为 no。

2.带宽消耗

官方建议 Redis Cluster 节点个数不要超过 1000 个。节点直接会交换 PING/PONG 消息。

  • 消息发送频率:节点发现与其他节点最后通信时间超过 cluster-node-timeout/2 时会直接发送 PING 消息;
  • 消息数据量:slots 槽数组(2KB 空间)和整个集群 1/10 的状态数据(10 个节点状态数据约 1 KB);
  • 节点部署的机器规模:集群分布的机器越多且每台机器划分的节点数越均匀,则集群内整体的可用带宽越高

一个例子:

规模:200 个节点,20 台物理机(每台 10 个节点)。
如果 cluster-node-timeout = 15000, 那么 PING/PONG 带宽大致为 25 Mb。
如果 cluster-node-timeout = 20000, 那么 PING/PONG 带宽会低于 15 Mb。

优化方案:

  • 避免使用 “大” 集群:避免多业务使用一个集群,大业务可以多集群;
  • cluster-node-timeout:带宽和故障转移速度的均衡;
  • 尽量均匀分配到多机器上:保证高可用和带宽。

3.Pub/Sub广播

Pub/Sub广播,也就是发布订阅在集群下会有什么问题?其实它会有一个很大的问题,我们对任意一个节点执行 publish(发布消息),这个节点会将消息在集群中进行传播,会导致节点的带宽开销会很大。

优化方案:

  • 单独 “走” 一套 Redis Sentinel。

4.集群倾斜

1、数据倾斜:内存不均。

通常由四个方面造成的:

  • 节点和槽分配不均;
  • 不同槽对应的键值数量差异较大;
  • 包含 bigkey;
  • 内存相关配置不一样。

节点和槽分配不均:

  • redis-trib.rb info ip:port 查看节点、槽、键值分布;
  • redis-trib.rb rebalance ip:port 进行均衡(涉及槽的迁移,谨慎使用)。

不同槽对应的键值数量差异较大:

  • Hash 算法 CRC16 正常情况下比较均匀;
  • 可能存在 hash_tag;
  • cluster countkeyssinslot {slot} 获取槽对应键值个数。

包含 bigkey:

  • 例如大字符串、几百万的元素的 hash、set 等;
  • 从节点上执行 redis-cli --bigkeys 发现 bigkey;
  • 优化方案:优化数据结构。

内存相关配置不一样:

  • hash-max-ziplist-value、set-max-intset-entries 等配置不一致;
  • 优化方案:定期 “检查” 配置一致性。

2、请求倾斜:热点。

某个 key 请求量非常高,也就是常见的热点 key 问题。热点 key 常见于重要的 key 或者 bigkey。

优化方案:

  • 避免 bigkey;
  • 热键不要用 hash_tag,因为 hash_tag 会落到一个节点上;
  • 如果真有热点 key 而且业务对一致性要求不高时,可以用本地缓存 + MQ 解决。

5.读写分离

1、只读连接:集群模式的从节点不接受任何读写请求。

  • 重定向到负责槽的主节点;
  • readonly 命令可以读:连接级别命令。

2、读写分离:更加复杂。

  • 同样的问题:复制延迟、读取过期数据、从节点故障;
  • 修改客户端:cluster slaves {nodeId}。

6.数据迁移

1、官方迁移工具:redis-trib.rb import。

  • 只能从单机迁移到集群;
  • 不支持在线迁移:source 需要停写;
  • 不支持断点续传;
  • 单线程迁移:影响速度。

2、第三方在线迁移工具:唯品会的 redis-migrate-tool 和豌豆荚的 redis-port。

推荐使用 redis-migrate-tool 或 redis-port。

7.集群VS单机

集群限制:

  • key 批量操作支持有限,例如 mget、mset 必须在一个 slot;
  • key 事务和 lua 支持有限,操作的 key 必须在一个节点上;
  • key 是数据分区的最小粒度,不支持 bigkey 分区;
  • 不支持多个数据库,集群模式下只有一个 db 0;
  • 复制只支持一层,不支持树形复制结构。

Redis Cluster 不一定好。像 Redis Cluster 这样的分布式 Redis,可以满足容量和性能的扩展性,但是实际上很多业务是不需要的。Redis Cluster 有以下缺点:

  • 大多数时候客户端性能会降低;
  • 命令无法跨节点使用(mget、keys、scan、flush、sinter 等);
  • lua 和事务无法跨节点使用;
  • 客户端维护更复杂,SDK 和应用本身消耗(例如更多的连接池)。

很多场景 Redis Sentinel 已经足够好。

8.集群总结

  • Redis Cluster 数据分区规则采用虚拟槽方式(16384 个槽),每个节点负责一部分槽和相关数据,实现数据和请求的负载均衡;
  • 搭建集群划分位四个步骤:准备节点、节点握手、分配槽、复制,redis-trib.rb 工具用于快速搭建集群;
  • 集群伸缩通过在节点之间移动槽和相关数据实现;
  • 扩容时根据槽迁移计划把槽从源节点迁移到新节点;
  • 收缩时如果下线的节点有负责的槽需要迁移到其它节点,再通过 cluster forget 命令让集群内所有节点忘记被下线节点;
  • 使用 smart 客户端操作集群达到通信效率最大化,客户端内部负责计算维护键 -> 槽 -> 节点的映射,用于快速定位到目标节点;
  • 集群自动故障转移过程分为故障发现和节点恢复。节点下线分为主观下线和客观下线,当超过半数主节点认为故障节点为主观下线时标记它为客观下线状态。从节点负责对客观下线的主节点触发故障恢复流程,保证集群的可用性;