「码呗学院」Redis 集群部署

简介

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进行直接访问。


相同颜色的实例为一组,master负责读写请求,slave为备份

安装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的方式,保障了数据的安全性,而且提供了相对简单的集群管理配置,没有三方服务的依赖,用来构建成产线缓存服务集群,再合适不过。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,937评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,503评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,712评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,668评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,677评论 5 366
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,601评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,975评论 3 396
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,637评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,881评论 1 298
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,621评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,710评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,387评论 4 319
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,971评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,947评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,189评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,805评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,449评论 2 342

推荐阅读更多精彩内容