简介
redis以高效的读写性能,被非常多的企业用户用来做数据缓存层,很多公司会根据自己业务场景,部署单点或者是redis的集群,但是单点的redis如果发生故障,是无法保证服务高可用的,所以现在缓存服务通常都是redis的集群。
redis的集群方案目前使用的比较多的几种方式;
基于codis的架构,部署的redis集群。
基于redis的哨兵模式,部署一个master多slave的集群模式。
redis从3.0开始支持集群的部署方式,而且是无中心化的,多master多slave模式。
本次主要介绍redis自己支持的cluster的方式部署以及简单应用
集群架构
首先是我们准备的三台服务器资源,规划每台服务器上部署一个master实例和一个slave实例。在每台服务器上安装好redis,规划服务器列表如下:
介绍一下redis支持的cluster集群架构,redis cluster 集群节点类型分为master和slave,我们用组的概念理解一下,以master为类型进行分组,一个组里面只有一个master,我们可以认为是组长,其他的slave是组员,那数据存到redis集群里面,怎么存储呢,redis集群默认实现了一个hash slot的方式,哈希槽默认是16384个,也就是0-16383个槽,集群在创建的时候,每个组master会均匀分配这些槽,每个master占据一定的hash slot 范围,往redis集群里面存储数据的时候,redis默认通过crc16(key)%16384 计算这个key的hash值,然后根据hash值找到对应的master,直连该master进行数据访问和存储。redis集群里面所有实例都有hash slot对应的master信息,所以连接任何一个实例信息,都能获取到集群的ip和端口列表,然后根据计算出来的hash值,选择具体的ip进行直接访问。
安装Redis
下载安装redis:
## redis下载目录可以参考:http://download.redis.io/releases/
wget http://download.redis.io/releases/redis-4.0.8.tar.gz
## 解压文件
tar -xzf redis-4.0.8.tar.gz
## 进入解压目录,编译安装
make
make install
centos服务器,也可以通过yum install redis 方式安装,测试安装结果。
redis-cli-v
在每台服务器上启动redis-server,因为服务器资源有限,所以每台服务器上部署两个redis实例,当然如果只有一台服务器的情况下,也可以在一个服务器上部署多个实例,redis的配置文件如下,如果要部署多个实例注意修改端口号以及工作目录和日志文件信息。
port 9002
bind 0.0.0.0
daemonize yes
logfile "/root/temp/redis/redis-4.0.8/logs/node-9002.log"
cluster-enabled yes
cluster-config-file /root/temp/redis/redis-4.0.8/config/nodes-9002.conf
cluster-node-timeout 5000
cluster-slave-validity-factor 10
appendonly yes
dir /root/temp/redis/redis-4.0.8/data/9002
dbfilename dump-9002.rdb
appendfilename "appendonly-9002.aof"
redis-server启动以后可以查看每台上的实例状态,下面开始构建集群。
如果是通过源码安装的redis,在src目录下会有redis-trib.rb文件,这是ruby写的一个脚本文件,redis的集群创建管理就是通过这个脚本来操作,所以在执行这个脚本之前,首先还要保证服务器可以执行ruby文件。执行该脚本需要ruby版本>=2.2.2或者以上,检查本机ruby的版本,如果版本过旧,可以通过以下操作升级ruby版本:
## 安装rvm,Ruby version manager,Ruby的一个版本管理工具,如果使用过node,就跟nvm一样,通过rvm可以看到ruby版本列表,可以很方便的进行安装和删除。
## 安装rvm的方式;
## gpg2是mac上的命令,linux服务器命令是gpg
gpg2 --keyserver hkp://keys.gnupg.net --recv-keys \ 409B6B1796C275462A1703113804BB82D39DC0E3 \ 7D2BAF1CF37B13E2069D6956105BD0E739499BDB
curl -sSL https://get.rvm.io | bash -s stable
执行命令安装好以后,会有提示让执行source命令,让rvm生效:
source $HOME/.rvm/scripts/rvm
## 查看安装好的信息
rvm -v
## 查看ruby 版本列表
rvm list known
##安装ruby版本2.5
rvm install 2.5
##安装好ruby以后,还需要安装redis的模块。
gem install redis
## 如果在安装redis模块的时候很慢,可以更换ruby仓库源,使用aliyun的源仓库。
## gem source -a http://mirrors.aliyun.com/rubygems/
以上就安装好ruby相关的环境了,然后我们就可以通过redis-trib.rb命令来管理集群了。
Redis集群创建
redis-trib.rb我是通过脚本的形式执行的,也可以把他放到/usr/bin/目录下,然后就可以执行redis-trib命令了,可以查看redis-trib命令的先关帮助信息:
ruby redis-trib.rb help
Usage: redis-trib <command> <options> <arguments ...>
create host1:port1 ... hostN:portN
--replicas <arg>
check host:port
info host:port
fix host:port
--timeout <arg>
reshard host:port
--from <arg>
--to <arg>
--slots <arg>
--yes
--timeout <arg>
--pipeline <arg>
rebalance host:port
--weight <arg>
--auto-weights
--use-empty-masters
--timeout <arg>
--simulate
--pipeline <arg>
--threshold <arg>
add-node new_host:new_port existing_host:existing_port
--slave
--master-id <arg>
del-node host:port node_id
set-timeout host:port milliseconds
call host:port command arg arg .. arg
import host:port
--from <arg>
--copy
--replace
help (show this help)
For check, fix, reshard, del-node, set-timeout you can specify the host and port of any working node in the cluster.
上面的帮助详细介绍了redis-trib的使用说明,首先我们创建一个集群:
ruby redis-trib.rb create --replicas 1 192.168.35.66:7001 192.168.35.73:7001 192.168.35.74:7001 192.168.35.66:9002 192.168.35.73:9002 192.168.35.74:9002
介绍一下命令的参数信息:
create 是子命令,表示创建一个新的集群,并且创建集群的master节点必须>=3个才可以创建集群,replicas表示每个master有几个slave,这里是1,表示每个master至少有一个slave实例。集群创建会弹出一个提示窗口,确认没有问题的话,输入yes,即可完成集群创建,然后通过check检查集群状态:
ruby redis-trib.rb check 192.168.35.74:7001
>>> Performing Cluster Check (using node 192.168.35.74:7001)
M: c0be24752e6a23b566112c5389c20c35e97a5182 192.168.35.74:7001
slots:5461-5797,10923-16383 (5798 slots) master
1 additional replica(s)
M: c1e9e762e553b5b62a674c439c8272447b2efc82 192.168.35.73:7001
slots:5798-10922 (5125 slots) master
1 additional replica(s)
S: e00b99df51149512a4b4e6d64eec4d7550a63ede 192.168.35.73:9002
slots: (0 slots) slave
replicates 7a70746b2e23ee06e1bedb07f1952bfc9501a859
M: 7a70746b2e23ee06e1bedb07f1952bfc9501a859 192.168.35.66:7001
slots:0-5460 (5461 slots) master
1 additional replica(s)
S: 82da7bd42cdd96785a2f131445c7b189c48c75a3 192.168.35.74:9002
slots: (0 slots) slave
replicates c0be24752e6a23b566112c5389c20c35e97a5182
S: 899a7a0199eeab0fcc5bce108efc2a57cf3d1d23 192.168.35.66:9002
slots: (0 slots) slave
replicates c1e9e762e553b5b62a674c439c8272447b2efc82
[OK] All nodes agree about slots configuration.
>>> Check for open slots...
>>> Check slots coverage...
[OK] All 16384 slots covered.
上面显示我的集群状态信息,slot分配的结果看上去可能不太均匀,这是因为我做过增减节点,slot重新分配过,如果想让均衡一下,可以执行如下命令:
##这个命令用来重新规划集群各个势力的负载
ruby redis-trib.rb rebalance 192.168.35.74:7001
##重新规划以后再通过check命令检查,会发现redis的各个实例分配的slots很均匀了(命令输出结果就不在这里贴出来了)
ruby redis-trib.rb check 192.168.35.74:7001
然后就会发现每个master分配的hash slot非常均衡了,有两个是5461个slots,一个是5462个slots,执行的命令就是rebalance。到此,我们的redis集群已经创建完成了。
访问redis集群
现在通过redis-cli和应用程序来访问一下集群信息。
## redis 访问集群的时候,需要执行-c参数。
redis-cli -c -h 192.168.35.74 -p 7001
192.168.35.74:7001> get name
-> Redirected to slot [5798] located at 192.168.35.73:7001
"ggcc"
192.168.35.73:7001>
通过上面的执行,发现获取name值的时候一开始我们连的是74这台服务器,get命令执行的时候,却重定向到了73这台服务器,所以client访问集群的时候,首先是根据key计算出来hash值,再根据hash值找到对应的master实例节点信息,直连对应的master实例,进行数据的读写。
redis-cli命令是这样执行的,那么我们看Java程序是怎么读写数据的。
package redis;
import redis.clients.jedis.HostAndPort;
import redis.clients.jedis.JedisCluster;
import java.util.HashSet;
import java.util.Scanner;
import java.util.Set;
public class RedisClusterCli {
public static void main(String[] args) {
HostAndPort hostAndPort = new HostAndPort("192.168.35.74", 9002);
Set<HostAndPort> hostAndPorts = new HashSet<>();
hostAndPorts.add(hostAndPort);
JedisCluster cluster = new JedisCluster(hostAndPorts);
String value;
while (true) {
try {
System.out.println("请输入key:");
Scanner scanner = new Scanner(System.in);
String key = scanner.next();
value = cluster.get(key);
System.out.println(value);
} catch (Exception ex) {
ex.printStackTrace();
System.out.println("异常信息忽略");
}
}
}
}
测试代码写的比较简单,单独执行的话,能够正常运行,但是看不出具体的执行细节,我们查看一下关键部分的源码,然后看一下它执行的具体逻辑。redis.clients.jedis.JedisClusterCommand.runWithRetries方法
private T runWithRetries(byte[] key, int attempts, boolean tryRandomNode, boolean asking) {
if (attempts <= 0) {
throw new JedisClusterMaxRedirectionsException("Too many Cluster redirections?");
}
Jedis connection = null;
try {
if (asking) {
// TODO: Pipeline asking with the original command to make it
// faster....
connection = askConnection.get();
connection.asking();
// if asking success, reset asking flag
asking = false;
} else {
if (tryRandomNode) {
connection = connectionHandler.getConnection();
} else {
connection = connectionHandler.getConnectionFromSlot(JedisClusterCRC16.getSlot(key));
}
}
return execute(connection);
} catch (JedisNoReachableClusterNodeException jnrcne) {
throw jnrcne;
} catch (JedisConnectionException jce) {
// release current connection before recursion
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. For each time-outed request.
//TODO make tracking of successful/unsuccessful operations for node - do renewing only
//if there were no successful responses from this node last few seconds
this.connectionHandler.renewSlotCache();
//no more redirections left, throw original exception, not JedisClusterMaxRedirectionsException, because it's not MOVED situation
throw jce;
}
return runWithRetries(key, attempts - 1, tryRandomNode, asking);
} 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 or renewing
releaseConnection(connection);
connection = null;
if (jre instanceof JedisAskDataException) {
asking = true;
askConnection.set(this.connectionHandler.getConnectionFromNode(jre.getTargetNode()));
} else if (jre instanceof JedisMovedDataException) {
} else {
throw new JedisClusterException(jre);
}
return runWithRetries(key, attempts - 1, false, asking);
} finally {
releaseConnection(connection);
}
}
首先注意看第21行代码,这里是表示根据key的hash值,计算出来slot,然后根据slot信息获取相应的节点ip和端口信息,获取具体IP的代码逻辑在redis.clients.jedis.JedisSlotBasedConnectionHandler.getConnectionFromSlot这个方法中,具体代码如下:
public Jedis getConnectionFromSlot(int slot) {
JedisPool connectionPool = cache.getSlotPool(slot);
if (connectionPool != null) {
// It can't guaranteed to get valid connection because of node
// assignment
return connectionPool.getResource();
} else {
renewSlotCache(); //It's abnormal situation for cluster mode, that we have just nothing for slot, try to rediscover state
connectionPool = cache.getSlotPool(slot);
if (connectionPool != null) {
return connectionPool.getResource();
} else {
//no choice, fallback to new connection to random node
return getConnection();
}
}
}
}
这个代码逻辑比较简单,首先是从缓存获取,缓存的数据结构是个map对象,map对象用了一个最简单直接的方法,存储了16384对象信息,key分别是对应的0-16383个数字,value就是JedisPool对象,也就是保存的具体的服务器IP和端口。如果缓存没有获取到,执行第8行代码,加载信息,加载到缓存以后,如果依然没有获取到,那就返回一个随机的,理论上应该都是能获取到的,当然也不排除极端的情况,比如访问其中一个master节点挂机,redis.clients.jedis.JedisClusterCommand.runWithRetries这个方法会抛出一个异常JedisConnectionException,但是这个异常里面会递归调用多次,也就是说会默认重试调用,默认的次数是5次,当最后一次调用的时候,依然报了这个异常,那么就会重新加载redis的集群信息,因为master挂机以后,新的slave产生,slots对应的IP信息也会出现变化。
redis集群中的一个master挂机以后,新的slave晋升为master,这中间有个心跳检查时间,根据redis的参数cluster-slave-validity-factor配置,在这个时间内,如果获取挂机master实例上的数据,将会频繁报错,通常在应用场景中,就会穿透缓存层,直接访问数据库。因为是redis集群,每个master上面分配的数据都只是一部分,即便是穿透缓存层,也是一部分数据而已,而且根据我们部署的redis集群的数量,redis master实例越多,数据分配的越是均匀,故障的发生率越是低,影响的面就越小,这样redis集群就能很好的保证我们服务的高可用性。
Redis集群扩容
redis集群的扩容和缩容,同样使用redis-trib来操作,下面是操作执行流程,执行记录比较多,而且比较长,可以直接跳过,看后面操作命令的总结,实际操作时,有问题再看具体的执行流程记录:
## 新增加一个master节点
ruby redis-trib.rb add-node 192.168.35.73:9003 192.168.35.74:9002
>>> Adding node 192.168.35.73:9003 to cluster 192.168.35.74:9002
>>> Performing Cluster Check (using node 192.168.35.74:9002)
S: 82da7bd42cdd96785a2f131445c7b189c48c75a3 192.168.35.74:9002
slots: (0 slots) slave
replicates c0be24752e6a23b566112c5389c20c35e97a5182
S: e00b99df51149512a4b4e6d64eec4d7550a63ede 192.168.35.73:9002
slots: (0 slots) slave
replicates 7a70746b2e23ee06e1bedb07f1952bfc9501a859
M: 7a70746b2e23ee06e1bedb07f1952bfc9501a859 192.168.35.66:7001
slots:0-5460 (5461 slots) master
1 additional replica(s)
M: c1e9e762e553b5b62a674c439c8272447b2efc82 192.168.35.73:7001
slots:5461-10922 (5462 slots) master
1 additional replica(s)
S: 899a7a0199eeab0fcc5bce108efc2a57cf3d1d23 192.168.35.66:9002
slots: (0 slots) slave
replicates c1e9e762e553b5b62a674c439c8272447b2efc82
M: c0be24752e6a23b566112c5389c20c35e97a5182 192.168.35.74:7001
slots:10923-16383 (5461 slots) master
1 additional replica(s)
[OK] All nodes agree about slots configuration.
>>> Check for open slots...
>>> Check slots coverage...
[OK] All 16384 slots covered.
>>> Send CLUSTER MEET to node 192.168.35.73:9003 to make it join the cluster.
[OK] New node added correctly.
以上记录很多,但是实际上操作步骤没有多少,这里总结一下:
##第一个命令,首先是增加一个master实例,第一个IP:PORT信息是需要新增的实例地址,后面的IP:PORT是集群中任意一个实例的端口和ip信息,就是集群的入口。
ruby redis-trib.rb add-node 192.168.35.73:9003 192.168.35.74:9002
##第二个命令,这个命令就是为刚才新增的那个master增加一个slave实例,注意--slave参数表示增加的是slave实例,master-id就是刚才增加的master的node id值,可以通过check查看。第一个IP:PORT信息就是新增的slave实例信息,第二个参数和第一个命令一样,就是告诉集群的入口。
ruby redis-trib.rb add-node --slave --master-id 67d82f2e6e1f9c0afbc15090732d7f2f41064fbc 192.168.35.73:9004 192.168.35.74:9002
##第三个命令,新增的实例信息,其实是没有分配slot值的,即便是执行rebalance也是没有值的,就是认为该实例还没有启用,所以需要执行一下,给他分配一些slots值过去。reshard就是slots迁移命令,from和to 分别表示从哪里迁移到目标实例。如果我们需要下掉某一个master实例,我们就可以通过这个操作把当前实例的所有slots值全部迁移走。
ruby redis-trib.rb reshard --from e00b99df51149512a4b4e6d64eec4d7550a63ede --to 67d82f2e6e1f9c0afbc15090732d7f2f41064fbc --slots 3099 --yes --timeout 30 192.168.35.74:9002
##最后一步命令是可选的,因为你手动迁移的slots值,其实是不够均衡的,所以对于强迫症的用户来说,必须要执行一下rebalance命令,然后系统会重新分配slots信息,均匀分配
ruby redis-trib.rb rebalance 192.168.35.74:9002
以上操作对于应用程序来说都是无感知的,除非是迁移的时候搞down机了,应用程序会报错,但是报错的范围也会很小。
总结
Redis Cluster方案,适合高并发读写的场景,无中心化的配置,保障集群的高可用性,并且通过指定多个slave的方式,保障了数据的安全性,而且提供了相对简单的集群管理配置,没有三方服务的依赖,用来构建成产线缓存服务集群,再合适不过。