redis cluster是redis官方提供的分布式解决方案。主要作用有两点:
分布式的方案要解决把整个数据集按照分区规则映射到多个节点的问题,即把数据集划分到多个节点上,每个节点负责整体数据的一个子集。
Redis Cluser采用的分区方案叫做虚拟槽分区,所有数据的键值根据哈希函数映射到0~16383整数槽内,计算式:slot=CRC16(key)&16383。所有的槽(slot)分布到各个节点上,根据数据的键值计算所属的槽确定其存储在哪一个集群的节点中。
接下来部署一个简单的redis cluster,通过部署的过程理解redis cluster到底是怎么回事以及怎么使用。
redis cluster因为是分布式方案,自然要求多个节点。另外为了保证集群的高可用性,会给每一个主节点配置一个或者多个从节点。主节点故障时从节点可以自动替换上去,不至于因为单个节点的故障而导致集群的故障。
接下来我们使用docker部署6个redis节点组成的redis cluster,三主三从。因为在同一台机器上模拟,所以节点的区别主要体现在端口不一致。下文各个节点名称分别使用 “redis-端口号” 来区分。
每个节点的相关信息如下:
节点名称 | 节点角色 | 端口 | 配置文件 |
---|---|---|---|
redis-6380 | 主节点6380 | 6380 | redis-6380.conf nodes-6380.conf |
redis-6381 | 主节点6381 | 6381 | redis-6381.conf nodes-6381.conf |
redis-6382 | 主节点6382 | 6382 | redis-6382.conf nodes-6382.conf |
redis-6383 | 从节点6383(所属主节点6380) | 6383 | redis-6383.conf nodes-6383.conf |
redis-6384 | 从节点6384(所属主节点6381) | 6384 | redis-6384.conf nodes-6384.conf |
redis-6385 | 从节点6385(所属主节点6382) | 6385 | redis-6385.conf nodes-6385.conf |
redis本身内置提供cluster的功能。不需要额外安装其他组件。redis部署的时候和单机的redis部署并没有太大的区别,主要区别体现在配置文件当中的cluster部分配置要打开。如下给出了 6380 节点的配置。其他节点也只要修改相应的地址和端口信息即可。
redis-6380.conf:
# 节点端口
port 6380
# 开启集群模式
cluster-enabled yes
# 节点超时时间,单位毫秒
cluster-node-timeout 15000
# 集群内部配置文件,这份配置文件会记录集群当中的节点信息。由redis自动维护,不要手动去修改,防止破坏集群的相关配置。
cluster-config-file "nodes-6380.conf"
#主节点密码,部分节点会成为从节点
masterauth password
使用docker部署各个容器,命令如下:
尤其要注意的是redis使用docker部署集群的时候必须使用host模式。
docker run --net=host --name redis-6380 -v /data/redis-cluster/conf/redis-6380.conf:/usr/local/etc/redis/redis.conf -v /data/redis-cluster/conf/nodes-6380.conf:/data/nodes-6380.conf -d 192.168.168.98:5000/redis redis-server /usr/local/etc/redis/redis.conf
docker run --net=host --name redis-6381 -v /data/redis-cluster/conf/redis-6381.conf:/usr/local/etc/redis/redis.conf -v /data/redis-cluster/conf/nodes-6381.conf:/data/nodes-6381.conf -d 192.168.168.98:5000/redis redis-server /usr/local/etc/redis/redis.conf
docker run --net=host --name redis-6382 -v /data/redis-cluster/conf/redis-6382.conf:/usr/local/etc/redis/redis.conf -v /data/redis-cluster/conf/nodes-6382.conf:/data/nodes-6382.conf -d 192.168.168.98:5000/redis redis-server /usr/local/etc/redis/redis.conf
docker run --net=host --name redis-6383 -v /data/redis-cluster/conf/redis-6383.conf:/usr/local/etc/redis/redis.conf -v /data/redis-cluster/conf/nodes-6383.conf:/data/nodes-6383.conf -d 192.168.168.98:5000/redis redis-server /usr/local/etc/redis/redis.conf
docker run --net=host --name redis-6384 -v /data/redis-cluster/conf/redis-6384.conf:/usr/local/etc/redis/redis.conf -v /data/redis-cluster/conf/nodes-6384.conf:/data/nodes-6384.conf -d 192.168.168.98:5000/redis redis-server /usr/local/etc/redis/redis.conf
docker run --net=host --name redis-6385 -v /data/redis-cluster/conf/redis-6385.conf:/usr/local/etc/redis/redis.conf -v /data/redis-cluster/conf/nodes-6385.conf:/data/nodes-6385.conf -d 192.168.168.98:5000/redis redis-server /usr/local/etc/redis/redis.conf
6个redis容器部署完成
进入其中一个容器当中,使用cluster nodes
命令查看cluster的节点信息。也可以使用cluster info
查看集群的状态。
可见当前的redis cluster中只有一个节点。各个节点还需要通过握手的过程相互通信,然后组成一个可以相互通信的集群。
节点握手是指一批运行在集群模式下的节点通过Gossip协议彼此通信,达到感知对方的过程。节点握手是集群彼此通信的第一步,由客户端发起命令:cluster meet {ip} {port}
。
登录到其中的一个节点中,使用cluster meet
将所有的节点连接起来。
再来看节点信息,可见6个节点全部能够发现了。
前面说了,redis cluster根据分区规则将数据分布到不同的redis节点上,降低单个redis节点的读写压力。
redis cluster将数据的分区分为16384个槽,数据存取的时候根据哈希算法将key值计算出对应的值(0-16383),然后去对应的槽里取值。这16384个槽需要全部分配到redis节点上,如果有槽没有分配,则redis不能够使用。
接下来将槽分配到我们部署的三个主节点中。
redis-cli -h 127.0.0.1 -p 6380 -a password cluster addslots {0..5461}
redis-cli -h 127.0.0.1 -p 6381 -a password cluster addslots {5462..10924}
redis-cli -h 127.0.0.1 -p 6382 -a password cluster addslots {10925..16383}
再来看节点信息,可以看见三个主节点信息末尾多了分配的槽的信息。
为了保证集群的高可用性,每个redis主节点需要设置从节点。登录从节点,使用cluster replicate
命令设置其所属主节点,主节点的标识是cluster nodes结果中给redis节点生成的标识。这个标识同样会写在cluster-config-file文件中,这个文件不删除,redis启动的时候会去这个文件中读取标识和集群信息继续使用。
redis-cli -h 127.0.0.1 -p 6383 -a password cluster replicate 389c66679b44ea421ac685b3b44b64e9a7a32e5c
redis-cli -h 127.0.0.1 -p 6384 -a password cluster replicate e65d837c859c8ad19ee1962829ee7e9fa20cdfc7
redis-cli -h 127.0.0.1 -p 6385 -a password cluster replicate 0304888a12fe7f7fae5df5f444cc4a8832e8d170
再看cluster nodes节点信息,可见redis-6383,redis-6384,redis-6885三个节点都显示为slave,并且有所属的主节点的标识信息。
至此,一个简单的redis-cluster搭建完成。它由6个节点构成,redis-6380,redis-6381,redis-6382这3个主节点负责处理槽和相关数据,redis-6383,redis-6384,redis-6385这3个从节点负责故障转移。
前文说过,数据根据键值通过哈希算法计算得到0-16383的整数值,这些slot已在分配槽的过程当中分配到不同的节点。
根据 cluster keyslot {key}
可以计算键值的哈希值。
前文slot分配如下
redis节点 | slot范围 |
---|---|
redis-6380 | 0…5461 |
redis-6381 | 5462…10924 |
redis-6382 | 10925…16383 |
key为“name”的数据所属slot为5798,数据会在redis-6381当中存储。
如果在别的节点操作数据,会得到 MOVED 的结果,显示该数据应该存储在哪个节点。
在redis-6381执行数据的存储
也可以使用 redis-cli -c
加上参数登录redis客户端,在数据存取节点不正确时,客户端会自动做重定向。
接下测试一下主从节点的故障转移,把 redis-6381 节点停掉。redis-6384是其从节点,观察日志可以看出,刚开始是连接失败,而后redis-6384成为了主节点,替代redis-6381继续提供服务。
查看各个节点的状态,可见redis-6384成为了主节点并且将分配给redis-6381的槽“5462-10924”分配给了redis-6384。同时数据也转移到了redis-6384上。
将redis-6381节点启动,查看集群节点信息,如下图。可见重新启动的节点自动成为了redis-6384的从节点,作为故障转移的备用节点。
redis-trib.rb是redis官方提供的一个工具,在redis的源码包的src目录下。可以下载源码包获得该工具
wget http://download.redis.io/releases/redis-3.0.6.tar.gz
该工具是使用ruby开发的,使用该工具前需要安装ruby的环境
sudo apt-get install ruby
我安装了这个工具之后会出现一些使用上的问题,应该是我安装的问题。建议可以下载一个redis-trib.rb 的docker镜像来使用。
当redis-trib.rb可使用后,我们讲讲这个工具的使用方法。
使用redis-trib.rb创建集群之前同样要先准备好各个redis节点。同我们手工配置的方式一致,提供各个节点的配置文件,然后创建各个节点。配置文件和创建节点的方式前面已经讲述。
有几点区别以及注意事项:
准备好各个节点之后,我们使用redis-trib.rb创建集群,命令如下:
redis-trib.rb create --replicas 1 172.17.0.1:6380 172.17.0.1:6381 172.17.0.1:6382 172.17.0.1:6383 172.17.0.1:6384 172.17.0.1:6385
--replicas 1
表示每个节点有一个从节点,redis-trib.rb会自动分配主从关系。
执行结果如下:
可见一个命令就把我们之前做的 握手,分配槽,主从节点的配置等等工作都完成了。工具的使用非常方便。
我们可以继续使用 cluster nodes
查看节点信息
或者redis-trib.rb提供了check方法,检查整个集群。
redis-trib.rb check 172.17.0.1:6380
可见检查结果显示提示集群所有的槽都已分配到节点。
集群可对现有的拓扑结构进行调整,就是节点的新增和减少。下面讲讲集群扩容(新增节点)和收缩(减少节点)的方式。
扩容的操作步骤如下:
现在加入两个新的redis节点,
节点角色 | 端口 | 配置文件 |
---|---|---|
主节点6386 | 6386 | redis-6386.conf nodes-6386.conf |
从节点6387(所属主节点6386) | 6387 | redis-6387.conf nodes-6387.conf |
前两个步骤不再赘述,和前面说的集群的搭建是一致的。
准备两个节点的配置文件
创建docker容器
docker run --net=host --name redis-6386 -v /data/redis-cluster/conf/redis-6386.conf:/usr/local/etc/redis/redis.conf -v /data/redis-cluster/conf/nodes-6386.conf:/data/nodes-6386.conf -d 192.168.168.98:5000/redis redis-server /usr/local/etc/redis/redis.conf
docker run --net=host --name redis-6387 -v /data/redis-cluster/conf/redis-6387.conf:/usr/local/etc/redis/redis.conf -v /data/redis-cluster/conf/nodes-6387.conf:/data/nodes-6387.conf -d 192.168.168.98:5000/redis redis-server /usr/local/etc/redis/redis.conf
两个新节点加入集群
cluster meet 172.17.0.1 6386
cluster meet 172.17.0.1 6387
新加入的节点如果是作为从节点使用,则直接给指定所属主节点即可。
如果是要作为主节点分担读写数据的压力,那么需要做槽和数据的迁移。下面演示手动迁移指定槽 5798 的步骤
目标节点准备导入槽5798的数据
源节点准备导出槽5798数据
批量获取槽5798对应的键
使用migrate批量迁移键
这里出现一个问题,因为6386设置了密码,下面的结果会显示无权限访问。但是设置了密码参数确显示格式不对。所以我暂时把6386的密码认证关了,进行的键迁移。
最后,通知所有主节点槽5798指派给目标节点6386
127.0.0.1:6386> cluster setslot 5798 node 30c5e546f7d2ced550057d6b113ed982301bcd33
127.0.0.1:6380> cluster setslot 5798 node 30c5e546f7d2ced550057d6b113ed982301bcd33
127.0.0.1:6384> cluster setslot 5798 node 30c5e546f7d2ced550057d6b113ed982301bcd33
127.0.0.1:6382> cluster setslot 5798 node 30c5e546f7d2ced550057d6b113ed982301bcd33
然后可以在6386看到槽的分布,5798这个槽都分配给了6386。并且 name 这个值也迁移了过来
同样的,可以将6387设置为6386的从节点
redis-cli -h 127.0.0.1 -p 6387 -a password cluster replicate 30c5e546f7d2ced550057d6b113ed982301bcd33
收缩就意味着把节点去掉。我们可以根据扩容的流程,反其道而行之即可。现在我们把5798这个槽迁回6381。
#分别导出,导入5798槽
127.0.0.1:6384> cluster setslot 5798 importing ccb75e92d2c4d99196cff6749e427207577f1e3b
OK
127.0.0.1:6386> cluster setslot 5798 migrating 30c5e546f7d2ced550057d6b113ed982301bcd33
OK
#将数据进行迁移
127.0.0.1:6386> migrate 172.17.0.1 6384 "" 0 5000 keys name
OK
#通知所有节点5798槽所属的节点
127.0.0.1:6386> cluster setslot 5798 node ccb75e92d2c4d99196cff6749e427207577f1e3b
127.0.0.1:6380> cluster setslot 5798 node ccb75e92d2c4d99196cff6749e427207577f1e3b
127.0.0.1:6384> cluster setslot 5798 node ccb75e92d2c4d99196cff6749e427207577f1e3b
127.0.0.1:6382> cluster setslot 5798 node ccb75e92d2c4d99196cff6749e427207577f1e3b
槽迁移完成之后,6386,6387节点就没有了数据。可以使用cluster forget node-id
移除出集群当中
127.0.0.1:6384> cluster forget 30c5e546f7d2ced550057d6b113ed982301bcd33
OK
127.0.0.1:6384> cluster forget b240523731efca286c9aff97d12e0794d49258a5
OK
至此收缩完成。
上面我们手动执行执行过集群的扩容和收缩。扩容和收缩的过程因为涉及到槽的迁移以及槽中数据迁移非常的复杂。幸好使用redis-trib.rb工具提供了槽的迁移功能,这里介绍一下。
以下命令可以容易的进行不同节点之间的槽迁移
docker run --rm -it zvelo/redis-trib reshard 172.17.0.1:6380
redis-trib.rb reshard host:port --from --to --slots --yes --timeout
--pipeline
命令的参数说明如下:
下面我们来测试一下,先看当前节点的槽分布:
例如我们要从redis-6382中迁移100个槽给redis-6381。执行命令如下
redis-trib.rb reshard 172.17.0.1:6380
用redis-trib.rb执行命令的时候会有错误出现,是我安装的工具的问题。于是我下了一个redis-trib的docker镜像来执行这个命令
再查看各个节点的槽分布:
和上文迁移前的槽分布对比,可见redis-6382的槽少了100个,而redis-6381的节点多了100个。
客户端初始化连接时,将redis cluster 的slot在各个节点中的分配情况获取到并且保存在客户端的缓存当中。客户端在执行数据的增删查改时,首先计算键值的slot值,再根据slot值找到要操作的节点,获取该节点的连接后执行数据的操作。
下面给出java连接redis cluster的示例代码。
引入依赖,要注意早期版本的jedis不支持有密码的cluster的操作。
<dependency>
<groupId>redis.clientsgroupId>
<artifactId>jedisartifactId>
<version>2.9.0version>
dependency>
示例代码:
package test;
import java.util.HashSet;
import java.util.Set;
import org.apache.commons.pool2.impl.GenericObjectPoolConfig;
import redis.clients.jedis.HostAndPort;
import redis.clients.jedis.JedisCluster;
public class JedisClusterDemo {
public static JedisCluster jedisCluster;
public static void initJedisCluster() {
Set<HostAndPort> hostAndPortsSet = new HashSet<HostAndPort>();
hostAndPortsSet.add(new HostAndPort("172.17.0.1", 6380));
hostAndPortsSet.add(new HostAndPort("172.17.0.1", 6381));
hostAndPortsSet.add(new HostAndPort("172.17.0.1", 6382));
hostAndPortsSet.add(new HostAndPort("172.17.0.1", 6383));
hostAndPortsSet.add(new HostAndPort("172.17.0.1", 6384));
hostAndPortsSet.add(new HostAndPort("172.17.0.1", 6385));
hostAndPortsSet.add(new HostAndPort("172.17.0.1", 6386));
hostAndPortsSet.add(new HostAndPort("172.17.0.1", 6387));
GenericObjectPoolConfig config = new GenericObjectPoolConfig();
//参数: redis节点集合, 连接超时时间, 数据操作超时时间, 重试次数, 密码, 连接池配置
jedisCluster = new JedisCluster(hostAndPortsSet, 10000, 10000, 5, "password", config);
}
public static void main(String args[]) {
initJedisCluster();
int incre = 0;
while (true) {
try {
incre++;
Thread.currentThread().sleep(10000);
jedisCluster.set("number"+incre, String.valueOf(incre));
String value = jedisCluster.get("number"+incre);
System.out.println(value);
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
控制台显示程序执行的结果:
如下图,可见redis-6384节点断开了连接,redis-6381节点成为了主节点
而我们的程序会出现拒绝连接的异常,在故障转移完成之后继续正常运行
redis-6381中的数据正常,部分是从redis-6384节点中同步过来的
依赖配置
<dependency>
<groupId>org.springframeworkgroupId>
<artifactId>spring-beansartifactId>
<version>4.0.8.RELEASEversion>
dependency>
<dependency>
<groupId>org.springframeworkgroupId>
<artifactId>spring-coreartifactId>
<version>4.0.8.RELEASEversion>
dependency>
<dependency>
<groupId>org.springframeworkgroupId>
<artifactId>spring-contextartifactId>
<version>4.0.8.RELEASEversion>
dependency>
<dependency>
<groupId>redis.clientsgroupId>
<artifactId>jedisartifactId>
<version>2.9.0version>
dependency>
<dependency>
<groupId>org.springframework.datagroupId>
<artifactId>spring-data-redisartifactId>
<version>1.8.0.M1version>
dependency>
<dependency>
<groupId>com.fasterxml.jackson.coregroupId>
<artifactId>jackson-databindartifactId>
<version>2.8.4version>
dependency>
spring的配置
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context.xsd"
xmlns:cache="http://www.springframework.org/schema/cache
http://www.springframework.org/schema/cache
http://www.springframework.org/schema/cache/spring-cache.xsd">
<bean id="poolConfig" class="redis.clients.jedis.JedisPoolConfig" >
<property name="maxIdle" value="10000" />
<property name="maxWaitMillis" value="10000" />
<property name="testOnBorrow" value="true" />
bean >
<bean id="redisClusterConfiguration" class="org.springframework.data.redis.connection.RedisClusterConfiguration">
<property name="clusterNodes">
<set>
<bean id="clusterRedisNodes1" class="org.springframework.data.redis.connection.RedisNode">
<constructor-arg name="host" value="172.17.0.1" />
<constructor-arg name="port" value="6380" type="int" />
bean>
<bean id="clusterRedisNodes2" class="org.springframework.data.redis.connection.RedisNode">
<constructor-arg name="host" value="172.17.0.1" />
<constructor-arg name="port" value="6381" type="int" />
bean>
<bean id="clusterRedisNodes3" class="org.springframework.data.redis.connection.RedisNode">
<constructor-arg name="host" value="172.17.0.1" />
<constructor-arg name="port" value="6382" type="int" />
bean>
<bean id="clusterRedisNodes4" class="org.springframework.data.redis.connection.RedisNode">
<constructor-arg name="host" value="172.17.0.1" />
<constructor-arg name="port" value="6383" type="int" />
bean>
<bean id="clusterRedisNodes5" class="org.springframework.data.redis.connection.RedisNode">
<constructor-arg name="host" value="172.17.0.1" />
<constructor-arg name="port" value="6384" type="int" />
bean>
<bean id="clusterRedisNodes6" class="org.springframework.data.redis.connection.RedisNode">
<constructor-arg name="host" value="172.17.0.1" />
<constructor-arg name="port" value="6385" type="int" />
bean>
<bean id="clusterRedisNodes7" class="org.springframework.data.redis.connection.RedisNode">
<constructor-arg name="host" value="172.17.0.1" />
<constructor-arg name="port" value="6386" type="int" />
bean>
<bean id="clusterRedisNodes8" class="org.springframework.data.redis.connection.RedisNode">
<constructor-arg name="host" value="172.17.0.1" />
<constructor-arg name="port" value="6387" type="int" />
bean>
set>
property>
bean>
<bean id="connectionFactory" class="org.springframework.data.redis.connection.jedis.JedisConnectionFactory" >
<property name="password" value="password" />
<property name="timeout" value="30000" >property>
<constructor-arg name="clusterConfig" ref="redisClusterConfiguration">constructor-arg>
<constructor-arg name="poolConfig" ref="poolConfig">constructor-arg>
bean >
<bean id="keySerializer" class="org.springframework.data.redis.serializer.GenericToStringSerializer">
<constructor-arg index="0" type="java.lang.Class" value="java.lang.Object" />
bean>
<bean id="serializer" class="org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer">
bean>
<bean id="redisTemplate" class="org.springframework.data.redis.core.RedisTemplate">
<property name="connectionFactory" ref="connectionFactory" />
<property name="defaultSerializer" ref="serializer" />
<property name="keySerializer" ref="keySerializer" />
<property name="hashKeySerializer" ref="keySerializer" />
bean>
beans>
代码示例:
public class Main {
public static void main(String[] args) {
ClassPathXmlApplicationContext context
= new ClassPathXmlApplicationContext("spring-redis.xml");
RedisTemplate<String, String> template
= (RedisTemplate<String, String>) context.getBean("redisTemplate");
int incre = 0;
while (true) {
try {
incre++;
Thread.currentThread().sleep(3000);
template.opsForValue().set("springnumber"+incre, String.valueOf(incre));
String number = template.opsForValue().get("springnumber"+incre);
System.out.println("key:"+("springnumber"+incre)+"; value:"+number);
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
程序的执行结果
各个redis节点中的数据分布如下:
模拟redis-6380的宕机,查看故障转移的结果如下图。可见redis-6380断开了连接,而redis-6383节点接替了其继续提供服务。
程序执行的输出如下图,也可看出出现了连接的异常,而后恢复正常。
查看redis-6383节点的数据正常
redis cluster能够正常的提供分布式以及高可用的解决方案。搭建时可以使用redis-trib.rb工具进行搭建,可以很大幅度的提高效率。
在对于已有的redis cluster进行扩容或者收缩时要慎重。在实验过程当中出现过一些问题导致数据的丢失。
在使用redis cluster时,要注意客户端jar包的差异。早期的jar包对于密码的支持受限。
在使用redis cluster时也会存在一些限制。如每个节点只能使用db0,因为分布式存储在不同节点,对于redis的事务支持和批量操作也仅限于同一个节点上。在使用时要注意这些问题。