Redis主备集群的每个节点存有集群中的所有数据,从而导致
集群的总数据存储量受限于可用存储内存最小的节点,形成了木桶效应。由于Redis是基于内存存储的,因此这个问题就显得尤为突出。在redis3.0之前,通过在客户端去做的分片,通过hash环的方式对key进行分片存储。
s
harding
分片(客户端分片)虽然能解决各节点的存储压力,但是导致维护成本高、增加/移除节点比较繁琐。因此在redis3.0以后的版本最大的一个好处就是支持集群功能,集群中
至少应该有奇数个节点,所以至少有三个节点,官方
推荐三主三从的配置方式。集群的特点在于拥有和单机实例一样的性能,同时在网络分区以后能提供一定的可访问性以及对主数据库故障恢复的支持。哨兵和集群是两个独立的功能,当不需要对数据进行分片时使用哨兵就够了,如果要进行水平扩容,集群是个比较好的方式。
拓扑结构
一个Redis Cluster由多个Redis节点组构成。不同节点组服务的数据没有交集,也就是每个节点组对应数据 sharding的一个分片。节点组内部分为主备两类节点,对应master和slave节点。
两者数据准实时一致,通过异步化的主备复制机制来保证。一个节点组有且只有一个master节点,同时可以有0到多个slave节点,在这个节点组中只有master节点对用户提供写服务,读服务可以由master或者slave提供。
Redis-cluster是
基于gossip协议实现的无中心化节点的集群,因为去中心化的架构不存在统一的配置中心,
各个节点对整个集群状态的认知来自于节点之间的信息交互。在Redis Cluster,这个信息交互是通过Redis Cluster Bus来完成的。
Redis的数据分区
分布式数据库首要解决把整个数据集按照分区规则映射到多个节点的问题,即把数据集划分到多个节点上,每个节点负责整个数据的一个子集, Redis Cluster采用哈希分区规则,采用虚拟槽分区。虚拟槽分区巧妙地使用了哈希空间,使用分散度良好的哈希函数把所有的数据映射到一个固定范围内的整数集合, 整数定义为槽(slot)。比如Redis Cluster
槽的范围是0 ~ 16383。槽是集群内数据管理和迁移的基本单位。采用大范围的槽的主要目的是为了方便数据的拆分和集群的扩展,每个节点负责一定数量的槽。
计算公式:
slot = CRC16(key)%16383。每一个节点负责维护一部分槽以及槽所映射的键值数据。
HashTags
通过分片手段,可以将数据合理的划分到不同的节点上,这本来是一件好事。但是有时候,我们希望对相关联的业务以原子方式进行操作。举个简单的例子:我们在单节点上执行MSET , 它是一个原子性的操作,所有给定的key会在同一时间内被设置,不可能出现某些指定的key被更新另一些指定的key没有改变的情况。但是
在集群环境下,我们仍然可以执行MSET命令,但它的操作不在是原子操作,会存在某些指定的key被更新,而另外一些指定的key没有改变,原因是多个key可能会被分配到不同的机器上。所以这里就会存在一个矛盾点,既要求key尽可能的分散在不同机器,又要求某些相关联的key分配到相同机器。从前面的分析中我们了解到,分片其实就是一个hash的过程,对key做hash取模然后划分到不同的机器上。所以为了解决这个问题,我们需要考虑如何让相关联的key得到的hash值都相同。如果key全部相同是不现实的,那如何解决呢?在redis中引入了HashTag的概念,可以使得数据分布算法可以
根据key的某一个部分进行计算,然后让相关的key落到同一个数据分片。
举个简单的例子,加入对于用户的信息进行存储, user:user1:id、user:user1:name/ 那么通过hashtag的方式, user:{user1}:id、user:{user1}.name; 表示
当一个key包含 {} 的时候,就不对整个key做hash,而仅对 {} 包括的字符串做hash。
重定向客户端
Redis Cluster并不会代理查询,如果客户端访问了一个key并不存在的节点,这个节点是怎么处理的呢?比如我想获取key为msg的值,msg计算出来的槽编号为254,当前节点正好不负责编号为254的槽,那么就会返回客户端下面信息:
-MOVED 254 127.0.0.1:6381//表示客户端需要的254槽由运行在IP为127.0.0.1,端口为6381的Master实例服务。
如果根据key计算得出的槽恰好由当前节点负责,则当前节点会立即返回结果;
分片迁移
在一个稳定的Redis cluster下,每一个slot对应的节点是确定的,但是在某些情况下,节点和分片对应的关系会发生变更:
1. 新加入master节点;2. 某个节点宕机;
也就是说当动态添加或减少node节点时,需将16384个槽做个再分配,槽中的键值也要迁移。当然这过程,在
目前实现中,还处于半自动状态,需要人工介入。
新增一个主节点
新增一个节点D,redis cluster的这种做法是从各个节点的前面各拿取一部分slot到D上。大致就会变成这样:
节点A覆盖1365-5460;节点B覆盖6827-10922;节点C覆盖12288-16383;节点D覆盖0-1364,5461-6826,10923-12287;
删除一个主节点
先将节点的数据移动到其他节点上,然后才能执行删除;
槽迁移的过程
槽迁移过程中有个不稳定状态,这个不稳定状态会有一些规则,这些规则定义客户端的行为,从而使得
Redis Cluster不必宕机的情况下可以执行槽的迁移。下图描述了迁移编号为1、2、3的槽的过程中,他们在MasterA节点和MasterB节点中的状态。
简单的工作流程
1. 向MasterB发送状态变更命令,把Master B对应的slot状态设置为IMPORTING;2. 向MasterA发送状态变更命令,将Master A对应的slot状态设置为MIGRATING;
当MasterA的状态设置为MIGRANTING后,表示对应的slot正在迁移,为了保证slot数据的一致性,MasterA此时对于slot内部数据提供读写服务的行为和通常状态下是有区别的。
MIGRATING状态
1. 如果客户端访问的Key还没有迁移出去,则正常处理这个key;2. 如果key已经迁移或者不存在这个key,则回复客户端ASK信息让它跳转到MasterB去执行;
IMPORTING状态
当MasterB的状态设置为IMPORTING后,表示对应的slot正在向MasterB迁入,即使Master仍然能对外提供该slot的读写服务,但和通常状态下也是有区别的。
1. 当来自客户端的正常访问不是从ASK跳转过来的,说明客户端还不知道迁移正在进行,很有可能操作了一个目前还没迁移完成的并且还存在于MasterA上的key,如果此时这个key在A上已经被修改了,那么B和A的修改则会发生冲突。所以对于MasterB上的slot上的所有非ASK跳转过来的操作,MasterB都不会去执行,而是通过MOVED 命令让客户端跳转到MasterA上去执行。这样的状态控制保证了同一个key在迁移之前总是在源节点上执行,迁移后总是在目标节点上执行,防止出现两边同时写导致的冲突问题。而且迁移过程中新增的key一定会在目标节点上执行,源节点也不会新增key,使得整个迁移过程既能对外正常提供服务,又能在一定的时间点完成slot的迁移。
搭建 Redis 集群环境(分片)
redis搭建集群有
两种方式。一种是纯
手工的方式搭建,便于理解原理;另一种是
使用官方提供的redis-trib.rb搭建集群更加高效、准确,建议生产环境下使用。两种方式首先都需要启动预期的redis实例数,区别在于手工方式启动多实例后,每个集群内只有自己,需要手工使集群之间互相握手融合并设置每个分片的从节点以及槽的分配。官方提供的redis-trib.rb脚本可以一键自动创建集群的分片节点、自动分配Hash槽以及指定有几个从节点(通过选项指定)。
创建虚拟节点目录、修改配置文件
-
创建 Redis 虚拟节点目录
创建 cluster 目录,并在 cluster 目录下创建目录:7000、7001、7002、7003、7004、7005。需要执行的命令:
#在redisCluster目录下执行CoderX-MacBook-Pro:redisCluster xiaochangjiang$ mkdir 7000 7001 7002 7003 7004 7005
-
修改配置文件
拷贝 Redis 默认的配置文件(/usr/local/etc/redis.conf)到 7000-7005 这6个目录中。修改每一个目录下的配置文件,这里以 7000 为例:
# cp redis.conf redis/cluster/7000/7000.confport 7000 # Redis 节点的端口号cluster-enabled yes # 实例以集群模式运行cluster-config-file nodes-7000.conf # 节点配置文件路径cluster-node-timeout 5000 # 节点间通信的超时时间appendonly yes # 数据持久化
启动 Redis,并验证各个节点的状态
启动6个Redis节点(后台方式):
CoderX-MacBook-Pro:redisCluster xiaochangjiang$ redis-server ./7000/redis.conf &CoderX-MacBook-Pro:redisCluster xiaochangjiang$ redis-server ./7001/redis.conf &CoderX-MacBook-Pro:redisCluster xiaochangjiang$ redis-server ./7002/redis.conf &CoderX-MacBook-Pro:redisCluster xiaochangjiang$ redis-server ./7003/redis.conf &CoderX-MacBook-Pro:redisCluster xiaochangjiang$ redis-server ./7004/redis.conf &CoderX-MacBook-Pro:redisCluster xiaochangjiang$ redis-server ./7005/redis.conf &
通过命令启动之后,查看当前系统是否存在对应的进程。如果能看到如下类似的结果,则说明启动成功。
ps -ef | grep redis501 77537 76169 0 5:32下午 ttys004 0:00.11 redis-server 127.0.0.1:7000 [cluster]501 77539 76169 0 5:32下午 ttys004 0:00.07 redis-server 127.0.0.1:7001 [cluster]501 77541 76169 0 5:33下午 ttys004 0:00.05 redis-server 127.0.0.1:7002 [cluster]501 77545 76169 0 5:33下午 ttys004 0:00.03 redis-server 127.0.0.1:7003 [cluster]501 77546 76169 0 5:33下午 ttys004 0:00.02 redis-server 127.0.0.1:7004 [cluster]501 77547 76169 0 5:33下午 ttys004 0:00.02 redis-server 127.0.0.1:7005 [cluster]
创建集群
当前系统中已经有了6个正在运行的 Redis 实例,创建方式分为两种:
方式一(手动)
我们已经把所有的节点启动了,但此时他们都是互相独立的单个集群节点。要想实现集群,必须将他们关联起来,随便进入一个节点的 redis-cli。
关联各个节点
,执行下面的命令:
redis-cli -p 7000127.0.0.1:7000> cluster meet 127.0.0.1 7001OK127.0.0.1:7000> cluster meet 127.0.0.1 7002OK127.0.0.1:7000> cluster meet 127.0.0.1 7003OK127.0.0.1:7000> cluster meet 127.0.0.1 7004OK127.0.0.1:7000> cluster meet 127.0.0.1 7005OK
此时,所有的节点都关联起来了。
我们需要
将 redis Cluster 的16384个 slot (槽)分散到这其中 3 个节点里
(3 主 3 从)。执行命令:
redis-cli -p 7000 cluster addslots 0..5461redis-cli -p 7001 cluster addslots 5462..10922redis-cli -p 7002 cluster addslots 10923..16383#addslots子命令支持多个参数,但是不支持区间参数,所以当需要添加区间槽的时候可以用shell写个for循环的脚本来添加槽。#批量添加槽的shell脚本叫’addSlotsShell.sh’存在百度网盘里了。
此时节点已经分配好了。通过以下命令验证:
redis-cli -p 7000 cluster nodes4d664f426dc01962716051573b867c8449ebfd1d 127.0.0.1:7005@17005 master - 0 1584338103519 5 connected6a374b18a53e44ea0e5c5c661a7053c6ffcdb947 127.0.0.1:7004@17004 master - 0 1584338102696 4 connectedce903598788eff0962c2703e8e8b4270b6a5b79b 127.0.0.1:7003@17003 master - 0 1584338102000 3 connected20d66f54b7b467e1c76c85af6a2312c318502994 127.0.0.1:7001@17001 master - 0 1584338102000 1 connected 5462-10922fafd2a64d0ee8b865f11c99deea968b75d79137b 127.0.0.1:7002@17002 master - 0 1584338103208 0 connected 10923-16383eb3cd1c1b507b04e88fd48a346cd8d78da93f180 127.0.0.1:7000@17000 myself,master - 0 1584338101000 2 connected 0-5461
主节点已经有了 slot,最后一步就是
将主节点和从节点进行关联,形成主从复制的关系
。命令如下:
redis-cli -p 7003 cluster replicate 7000的NodeIDredis-cli -p 7004 cluster replicate 7001的NodeIDredis-cli -p 7005 cluster replicate 7002的NodeID#注意:需要在从节点的 cli 命令窗口关联主节点。不能反着来。
手动方式集群创建完毕,可以使用命令查看集群节点信息:
CoderX-MacBook-Pro:bin xiaochangjiang$ redis-cli -p 7000 cluster nodes4d664f426dc01962716051573b867c8449ebfd1d 127.0.0.1:7005@17005 slave fafd2a64d0ee8b865f11c99deea968b75d79137b 0 1584339923000 5 connected6a374b18a53e44ea0e5c5c661a7053c6ffcdb947 127.0.0.1:7004@17004 slave 20d66f54b7b467e1c76c85af6a2312c318502994 0 1584339923125 4 connectedce903598788eff0962c2703e8e8b4270b6a5b79b 127.0.0.1:7003@17003 slave eb3cd1c1b507b04e88fd48a346cd8d78da93f180 0 1584339922101 3 connected20d66f54b7b467e1c76c85af6a2312c318502994 127.0.0.1:7001@17001 master - 0 1584339921589 1 connected 5462-10922fafd2a64d0ee8b865f11c99deea968b75d79137b 127.0.0.1:7002@17002 master - 0 1584339922000 0 connected 10923-16383eb3cd1c1b507b04e88fd48a346cd8d78da93f180 127.0.0.1:7000@17000 myself,master - 0 1584339922000 2 connected 0-5461
方式二(使用工具redis-trib.rb)
需要使用 Redis 集群命令行工具 redis-trib 来完成集群的创建工作。
redis-trib.rb是官方提供的Redis Cluster的管理工具,无需额外下载,默认位于源码包的src目录下,但因该工具是用ruby开发的,所以需要准备相关的依赖环境。这个程序通过向实例发送特殊命令来完成创建新集群,检查集群,或者对集群进行重新分片(reshared)等工作。需要安装 Redis 的 Ruby 模块。执行以下命令:
brew update#安装rubybrew install ruby#需要root权限sudo gem install redis
在 redis-trib.rb 文件所在目录执行命令:
# 无需指定哪个节点为 master,哪个节点为 slave,因为 redis 内部算法已经帮我们实现了# 使用 –replicas 1 创建集群,即每个 master 带一个 slave./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:7005
创建过程会打印类似如下的信息,表示创建集群成功:
>>> Creating clusterConnecting to node 127.0.0.1:7000: OKConnecting to node 127.0.0.1:7001: OKConnecting to node 127.0.0.1:7002: OKConnecting to node 127.0.0.1:7003: OKConnecting to node 127.0.0.1:7004: OKConnecting to node 127.0.0.1:7005: OK>>> Performing hash slots allocation on 6 nodes...Using 3 masters:...
使用 Redis 集群命令行工具 redis-trib 搭建集群完毕。
验证集群可用性
通过两种方式验证集群的可用性:
1. 通过 redis-trib 提供的命令;检查集群的状态:redis-trib check查看集群的信息:redis-trib info2. 登录客户端,执行操作。
集群常用命令
通过客户端连接到服务端,执行’ cluster help'能够查看集群管理的常用命令。包括查询节点信息、为节点添加槽、将新节点meet进集群等。
CoderX-MacBook-Pro:redisCluster xiaochangjiang$ redis-cli -p 7000127.0.0.1:7000> cluster help1) CLUSTER <subcommand> arg arg ... arg. Subcommands are:2) ADDSLOTS <slot> [slot ...] -- Assign slots to current node.3) BUMPEPOCH -- Advance the cluster config epoch.4) COUNT-failure-reports <node-id> -- Return number of failure reports for <node-id>.5) COUNTKEYSINSLOT <slot> - Return the number of keys in <slot>.6) DELSLOTS <slot> [slot ...] -- Delete slots information from current node.7) FAILOVER [force|takeover] -- Promote current replica node to being a master.8) FORGET <node-id> -- Remove a node from the cluster.9) GETKEYSINSLOT <slot> <count> -- Return key names stored by current node in a slot.10) FLUSHSLOTS -- Delete current node own slots information.11) INFO - Return onformation about the cluster.12) KEYSLOT <key> -- Return the hash slot for <key>.13) MEET <ip> <port> [bus-port] -- Connect nodes into a working cluster.14) MYID -- Return the node id.15) NODES -- Return cluster configuration seen by node. Output format:16) <id> <ip:port> <flags> <master> <pings> <pongs> <epoch> <link> <slot> ... <slot>17) REPLICATE <node-id> -- Configure current node as replica to <node-id>.18) RESET [hard|soft] -- Reset current node (default: soft).19) SET-config-epoch <epoch> - Set config epoch of current node.20) SETSLOT <slot> (importing|migrating|stable|node <node-id>) -- Set slot state.21) REPLICAS <node-id> -- Return <node-id> replicas.22) SLOTS -- Return information about slots range mappings. Each range is made of:23) start, end, master and replicas IP addresses, ports and ids127.0.0.1:7000>
应用集成
Jedis
使用Redis集群
新建RedisAutoConfiguration类
导入Maven依赖然后创建如下配置类
package com.xxx.util.redis;import org.apache.commons.pool2.impl.GenericObjectPoolConfig;@Configurationpublic class RedisAutoConfiguration {@Value("${redis.node1.ip:127.0.0.1}")private String node1Ip;@Value("${redis.node1.port:0}")private int node1Port;...@Value("${redis.node.password:null}")private String password;@Value("${redis.connection_timeout:2000}")private int connectionTimeout;@Value("${redis.so_timeout:2000}")private int soTimeout;@Value("${redis.max_attempts:10}")private int maxAttempts;@Value("${redis.pool.maxTotal:800}")private int maxTotal;@Value("${redis.pool.minIdle:50}")private int minIdle;@Value("${redis.pool.maxIdle:200}")private int maxIdle;@Value("${redis.pool.maxWait:3000}")private int maxWaitMillis;@Beanpublic JedisCluster jedisCluster() {Set<HostAndPort> nodes = new HashSet<HostAndPort>();if (!node1Ip.equals("127.0.0.1") && !(node1Port == 0)){nodes.add(new HostAndPort(node1Ip, node1Port));}if (!node2Ip.equals("127.0.0.1") && !(node2Port == 0)){nodes.add(new HostAndPort(node2Ip, node2Port));}if (!node3Ip.equals("127.0.0.1") && !(node3Port == 0)){nodes.add(new HostAndPort(node3Ip, node3Port));}JedisCluster jedisCluster = null;if (!nodes.isEmpty()){GenericObjectPoolConfig pool = new GenericObjectPoolConfig();pool.setMaxTotal(maxTotal);pool.setMinIdle(minIdle);pool.setMaxIdle(maxIdle);pool.setMaxWaitMillis(maxWaitMillis);jedisCluster = new JedisCluster(nodes, connectionTimeout, soTimeout, maxAttempts, password, pool);}return jedisCluster;}}
增加redis配置,然后在需要使用Redis的业务代码中使用Jedis
package com.xxx.redis;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.stereotype.Component;import redis.clients.jedis.JedisCluster;import javax.annotation.PostConstruct;@Slf4j@Componentpublic class RedisTest {@Autowiredprivate JedisCluster jedisCluster;public final static String PREFIX = "token:user:";@PostConstructpublic void init() {log.info(“init redis jedisCommands -------------");String token = "hjyqakteqxpfy6431045747319394309";jedisCluster.set(PREFIX+token,"1000");String value = jedisCluster.get(PREFIX+token);log.info("jedisCommands.get(\"test\") = {}------------", value);Long del = jedisCluster.del(PREFIX+token);log.info("jedisCommands.get(\"test\") = {}--del-{}----------", jedisCluster.get(PREFIX+token), del);//设置失效时长jedisCluster.setex(PREFIX+token,60,"1000");}}