03_Redis集群搭建

搭建Redis集群环境并与Spring整合使用

Posted by silhouette on June 19, 2020

前言

参考文档如下:

一、Redis 的主从复制

1.1 什么是主从复制

持久化保证了即使 redis 服务重启也不会丢失数据,因为redis 服务重启后会将硬盘上持久化的数据恢复到内存中,但是当 redis 服务器的硬盘损坏了可能会导致数据丢失,如果通过redis 的主从复制机制就可以避免这种单点故障。

说明:

  • 主 redis 中的数据有两个副本(replication)即从redis1 和 从redis2,即使一台 redis 服务器宕机其他两台 redis 服务器也可以继续提供服务
  • 主redis 中的数据和从 redis 上的数据保持实时同步,当主 redis 写入数据时通过主从复制机制复制到两个从 redis 服务上。
  • 只有一个 redis ,可以有多个从 redis
  • 主从复制不会阻塞 master ,在同步数据时,master 可以继续处理 client 请求

1.2 主从复制实现

在 redis 从服务器上添加其对应的主服务器的信息,修改从 redis 服务器上的 redis.conf 文件,添加 slaveof 主 redis 服务器的 IP 和端口号

启动主从服务器之后即完成了对 Redis 主从复制的配置。

  • 架构图如下:

  • 日志输出

1.3测试主从复制机制

  • 一个Master,两个Slave,Slave只能读不能写

    • 操作主机

    • 操作从机

  • Master挂掉后,Master关系依然存在,Master重启即可恢复。先关闭master的 redis 进程

    • 停掉主机,通过从机查看信息

    • 重启主机,通过分级查看信息可知,主机重启,master恢复

  • 当Master挂掉后,Slave可键入命令 slaveof no one使当前redis停止与其他Master redis数据同步,转成Master redis。

1.4 主从复制优缺点

优点

  • 支持主从复制,主机会自动将数据同步到从机,可以进行读写分离
  • 为了分在分载 Master 的读操作压力,Slave 服务器可以为客户端提供只读操作的服务,写服务仍然由Master 来完成
  • Slave 同样可以接受其他Slaves 的连接和请求,这样可以有效的分载 Master的同步压力
  • Master 服务器是以非阻塞的方式为 Slaves 提供服务。所以在 Master 服务器同步期间,客户端仍然可以提交查询或修改请求
  • Slave服务器同样是以非阻塞的方式完成数据同步。在同步期间,如果有客户端提交查询请求,Redis 则返回同步之前的数据

缺点

  • Redis 不具备自动容错和恢复功能,主机从机的宕机都会导致前端部分写请求失败,需要等待机器重启或者手动切换前端的IP才能恢复。
  • 主机宕机,宕机前有部分数据未能及时同步到从机,切换IP 后还会引入数据不一致的问题,降低了系统的可用性。
  • redis 较难支持在线扩容,在集群容量达到上限时在线扩容就变很复杂。

二、Sentinel哨兵模式

2.1 主从模式的缺陷

上述主从模式中我们可以看到,当master server宕机后,可以通过指令来将一个从服务器升级为主服务器,以便继续提供服务,但是这个过程得人工操作完成。为了完善这个缺陷,redis 2.8 版本后提供了哨兵机制来实现自动化的系统监控和故障恢复功能。

哨兵模式的作用就是监控 redis 系统的运行状态。它的主要功能包括如下三点:

  1. 监控(Monitoring):Sentinel 会不断地检查你的主服务器和从服务器是否运行正常
  2. 提醒(Notification):当被监控的某个 Redis 服务器出现问题时,Sentinel 可以通过 API 向管理员或者其他应用程序发送通知
  3. 自动故障迁移(Automatic Failover):当一个主服务器不能正常工作时,Sentinel会开始一次自动故障迁移操作,它会进行自动选举,将其中一个从服务器升级为主服务器,并让失效主服务器的其他从服务器改为复制新的主服务器;当客户端试图连接失效的主服务器时,集群也会向客户端返回新主服务器的地址,是得集群可以使用新主服务器代替失效服务器。

2.2 哨兵的工作方式

  • 每个Sentinel(哨兵)进程以每秒钟一次的频率向整个集群中的Master主服务器,Slave从服务器以及其他Sentinel(哨兵)进程发送一个 PING 命令。
  • 如果一个实例(instance)距离最后一次有效回复 PING 命令的时间超过 down-after-milliseconds 选项所指定的值, 则这个实例会被 Sentinel(哨兵)进程标记为主观下线(SDOWN)
  • 如果一个Master主服务器被标记为主观下线(SDOWN),则正在监视这个Master主服务器的所有 Sentinel(哨兵)进程要以每秒一次的频率确认Master主服务器的确进入了主观下线状态
  • 当有足够数量的 Sentinel(哨兵)进程(大于等于配置文件指定的值)在指定的时间范围内确认Master主服务器进入了主观下线状态(SDOWN), 则Master主服务器会被标记为客观下线(ODOWN)
  • 在一般情况下, 每个 Sentinel(哨兵)进程会以每 10 秒一次的频率向集群中的所有Master主服务器、Slave从服务器发送 INFO 命令。
  • 当Master主服务器被 Sentinel(哨兵)进程标记为客观下线(ODOWN)时,Sentinel(哨兵)进程向下线的 Master主服务器的所有 Slave从服务器发送 INFO 命令的频率会从 10 秒一次改为每秒一次。
  • 若没有足够数量的 Sentinel(哨兵)进程同意 Master主服务器下线, Master主服务器的客观下线状态就会被移除。若 Master主服务器重新向 Sentinel(哨兵)进程发送 PING 命令返回有效回复,Master主服务器的主观下线状态就会被移除。

2.3 自动故障切换

监控同一个Master的Sentinel 会自动互联,组成一个分布式的 Sentinel网络,互联通信并交换彼此关于被监视服务器的信息。下图中,三个监控 s1 的Sentinel ,自动组成Sentinel 网络结构。

Tips:当只有一个Sentinel 的时候,如果这个Sentinel 挂掉了,那么就无法显示自动故障切换了,所以必须使用sentinel网络。在此网络结构中,只要有一个 Sentinel存活,即可完成故障迁移。

  • 故障迁移的过程
    1. 投票机制(半数原则)
      • 当任何一个 Sentinel 发现被监控的 Master 下线时,会通知其他的Sentinel开会,投票确定该Master 是否下线(半数以上,所有 Sentinel 通常配奇数个)
    2. 选举
      • 当Sentinel收到 Master下线的通知后,会在所有的Slave中,选举一个新的节点,升级为 Master节点。其他Slaves节点转变为该节点的从节点
    3. 原 Master 重新上线
      • 当原Master 节点重新上线后,自动转为当前master节点的从节点

2.4 哨兵模式实现

  1. 环境搭建

    # 前提:已经存在一个正在运行的主从模式
    $ cd /usr/local
    $ mdkir redis-sentinel
    $ cd redis-sentinel
    $ cp -r /usr/local/redis-master-slave/* ./
    $ cp /usr/local/redis/bin/redis-sentinel ./
    
  2. 配置 Sentinel

    # 配置三个Sentinel实例,监控同一个Master节点
    $ mkdir s1 s2 s3
    $ cp /usr/local/redis-3.0.0/sentinel.conf  ./s1
    $ cp /usr/local/redis-3.0.0/sentinel.conf  ./s2
    $ cp /usr/local/redis-3.0.0/sentinel.conf  ./s3
    # 依次修改s1、s2、s3子目录中的sentinel.conf文件,修改端口,并指定要监控的主节点
     port 26379
     #                主节点别名 主节点地址 端口  触发故障切换的最少哨兵数
     sentinel monitor mymaster 127.0.0.1 6380 2
    
  3. 启动三个哨兵示实例,并观察日志输出

    # 新开三个窗口,分次启动sentinel实例
    $ cd /usr/local/redis-sentinel/
    $ ./redis-sentinel ./s1/sentinel.conf 
    
  4. 测试

    1. 关闭master服务,查看日志发现,会重新制定一个新master

      • 可以在客户端使用info 命令查看信息

    2. 再次上线6380节点。发现,6380节点成为了新的主节点的从节点。

2.5哨兵模式的优缺点

优点:

  • 哨兵模式是基于主从模式的,所有主从的优点,哨兵模式都具有。
  • 主从可以自动切换,系统更健壮,可用性更高。

缺点:

  • Redis较难支持在线扩容,在集群容量达到上限时在线扩容会变得很复杂。

三、Redis-Cluster 集群的搭建

3.1 redis 集群架构分析

单个 redis 服务不仅读写能力有限,而且不稳定,当redis宕机时,就没有可用的服务了,为了解决这一问题,如是引入了redis集群这一概念。集群就是将多个系统连接到一起,使多台服务器能够向一台机器那样工作或者看起来好像一台机器的技术。即redis 集群就是多个redis 服务同时提供服务,避免因一个redis 服务宕机导致整个服务消失的问题,并且redis 集群也强化=了单体服务的读写能力。

redis集群中,每一个redis称之为一个节点。为了使在部分节点失败或者大部分节点无法通信的情况下集群仍然可用,所以集群使用了主从复制模型,每个节点都会有N-1个复制品,redis-cluster 见下图:

redis 中没有leader 和 follower的概念,所有的节点都是平等的。即判断节点存活使用的是投票容错机制

架构细节:

  1. 所有的 redis 机诶单彼此互联(PING-PONG 机制),内部使用二进制协议优化传输速度和带宽。
  2. 节点的 fail 是通过集群中超过半数的节点检测失效时才会生效。
  3. 客户端与 redis 节点直连,不需要中间 proxy 层,客户端不需要连接集群所有节点,连接集群中任意一个可用节点即可。
  4. redis-cluster 把所有的物理节点映射到 [0~16383] slot 上,cluster 负责维护,node < - > slot <-> value。
    • redis 集群中内置了 16383 个哈希槽,当需要在 redis 集群中放置一个 key-value 时,redis 先对 key 使用 crc16 算法算出一个结果,然后把结果对 16383 求余数,这样每个 key 都会对应一个编号在 0~16383 之间的哈希槽,redis 会根据节点数量大致均等的将哈希槽映射到不同的节点。
    • 一个服务器一个槽,即redis集群中节点最多为16384,因为redis集群中就内置了 16383 个哈希槽

3.2 Redis 集群的搭建

Redis 集群中至少应该有三个节点。要保证集群的高可用,需要每个节点有一个备份机。redis 集群至少需要 6台服务器。搭建为分布式。可以使用一台虚拟机运行 6个 redis 实例。需要修改 redis 的端口号7001~7006

3.2.1 集群环境搭建

  1. 使用 ruby 脚本搭建集群。需要 ruby 的运行环境。

    $ yum install ruby
    $ yum install rubygems
    
  2. 安装 ruby 脚本运行使用的包,将 redis -3.0.0.gem.tar.gz 上传到根目录

    $  mv redis-3.0.0.gem /usr/local/
    $ gem install redis-3.0.0.gem 
    
  3. 拷贝 redis -3.0.0/src 下的 ruby 脚本到 redis -cluster 下

    $  mkdir /usr/local/redis-cluster
    $  cp /usr/local/redis-3.0.0/src/redis-trib.rb /usr/local/redis-cluster/
    
  4. 在redis -cluster 复制6个单机版的redis

    $ cd /usr/local
    $ cp -r redis redis-cluster/redis7001
    $ cp -r redis redis-cluster/redis7002
    $ cp -r redis redis-cluster/redis7003
    $ cp -r redis redis-cluster/redis7004
    $ cp -r redis redis-cluster/redis7005
    $ cp -r redis redis-cluster/redis7006
    

3.2.2 搭建步骤

  1. 修改修改端口号和 cluster-enable

    $  cd /usr/local/redis-cluster/redis7001/bin
    # 必须保证redis 服务是空的
    $ rm -rf appendonly.aof dump.rdb 
    $ cd /usr/local/redis-cluster/redis7001/etc
    $ vim redis.conf
    	# 修改端口
    	port 7001
    	# cluster-enable属性
    	cluster-enabled yes
    
  2. 启动每个redis 实例

    $ vim start-all.sh
    ##编写如下内容
    cd redis7001/bin
     ./redis-server ../etc/redis.conf
    cd ../../
    cd redis7002/bin
     ./redis-server ../etc/redis.conf
    cd ../../
    cd redis7003/bin
     ./redis-server ../etc/redis.conf
    cd ../../
    cd redis7004/bin
     ./redis-server ../etc/redis.conf
    cd ../../
    cd redis7005/bin
     ./redis-server ../etc/redis.conf
    cd ../../
    cd redis7006/bin
     ./redis-server ../etc/redis.conf
    cd ../../
    ##保存文件并退出
    # 修改批处理文件权限
    $ chmod u+x start-all.sh
    
  3. 测试启动脚本

  4. 创建关闭集群的脚本

    $  vim shutdown-all.sh
    ##编写如下内容
    ./redis7001/bin/redis-cli -p 7001 shutdown
    ./redis7002/bin/redis-cli -p 7002 shutdown
    ./redis7003/bin/redis-cli -p 7003 shutdown
    ./redis7004/bin/redis-cli -p 7004 shutdown
    ./redis7005/bin/redis-cli -p 7005 shutdown
    ./redis7006/bin/redis-cli -p 7006 shutdown
    ##保存文件并退出
    # 修改批处理文件权限
    $ chmod u+x shutdown-all.sh
    
  5. 使用 ruby 脚本搭建集群

    $ ./start-all.sh
    $ ./redis-trib.rb create --replicas 1 192.168.0.112:7001 192.168.0.112:7002 192.168.0.112:7003 192.168.0.112:7004 192.168.0.112:7005 192.168.0.112:7006
    

3.2.3 集群的使用方法

使用 redis-cli 命令连接集群,使用参数-p指定连接的端口,-c代表连接的是 redis 集群

四、使用Jedis 操作 redis

需要把 jedis 依赖的 jar 包添加到工程中。Maven 工程中需要把 jedis 的坐标添加到依赖。

4.1 使用Java 客户端连接redis集群版

实现步骤:

  1. 使用 JedisCluster 对象。需要一个 Set参数。Redis 节点的列表。
  2. 直接使用 JedisCluster 对象操作 redis 。在系统中单例存在。
  3. 打印结果
  4. 系统关闭前,关闭 JedisCluster 对象。

代码实现:

@Test
public void testJedisCluster() throws IOException {
    // 创建jedisCluster对象
    Set<HostAndPort> nodes = new HashSet<HostAndPort>();
    nodes.add( new HostAndPort("192.168.0.112" , 7001));
    nodes.add( new HostAndPort("192.168.0.112" , 7002));
    nodes.add( new HostAndPort("192.168.0.112" , 7003));
    nodes.add( new HostAndPort("192.168.0.112" , 7004));
    nodes.add( new HostAndPort("192.168.0.112" , 7005));
    nodes.add( new HostAndPort("192.168.0.112" , 7006));
    JedisCluster jedisCluster = new JedisCluster(nodes);
    // 操作数据库
    jedisCluster.set( "name", " silhouette" );
    String str = jedisCluster.get( "name" );
    System.out.println( str );
    // 关闭资源
    jedisCluster.close();
}

【注意:运行报错】

【解决方案】

方法一:查看代码中的配置是否有空格之类的,我就是这个问题

方法二:删除以上所有里面这个node.conf文件,将redis.conf 文件中 bind 命令绑定具体的IP ,然后通过上述的ruby 脚本重新搭建集群。参考链接

$ ./start-all.sh
$ ./redis-trib.rb create --replicas 1 192.168.0.112:7001 192.168.0.112:7002 192.168.0.112:7003 192.168.0.112:7004 192.168.0.112:7005 192.168.0.112:7006

4.2 Spring 整合 redis 集群

常用的操作 redis 的方法提取出一个接口,分别对应单机版和集群版创建两个实现类。

4.2.1 接口定义

/**
 *
 * @ClassName: JedisClient
 * @Description: JedisClient接口
 */
public interface JedisClient {
    
    String set(String key, String value);
    
    Long del(String key);
    
    String get(String key);
    
    Boolean exists(String key);
    
    Long expire(String key, int seconds);
    
    Long ttl(String key);
    
    Long incr(String key);
    
    Long hset(String key, String field, String value);
    
    String hget(String key, String field);
    
    Long hdel(String key, String... field);
}

4.2.2 单机版实现类

  • 代码实现

    /**
     *
     * @ClassName: JedisClientPool
     * @Description: Jedis单机版实现类
     *
     */
    public class JedisClientPool implements JedisClient {
      
        @Autowired
        private JedisPool jedisPool;
      
        /**
         * @return the jedisPool
         */
        public JedisPool getJedisPool() {
            return jedisPool;
        }
      
        /**
         * @param jedisPool
         *            the jedisPool to set
         */
        public void setJedisPool(JedisPool jedisPool) {
            this.jedisPool = jedisPool;
        }
      
        @Override
        public String set(String key, String value) {
            Jedis jedis = jedisPool.getResource();
            String result = jedis.set(key, value);
            jedis.close();
            return result;
        }
      
        @Override
        public String get(String key) {
            Jedis jedis = jedisPool.getResource();
            String result = jedis.get(key);
            jedis.close();
            return result;
        }
      
        @Override
        public Boolean exists(String key) {
            Jedis jedis = jedisPool.getResource();
            Boolean result = jedis.exists(key);
            jedis.close();
            return result;
        }
      
        @Override
        public Long expire(String key, int seconds) {
            Jedis jedis = jedisPool.getResource();
            Long result = jedis.expire(key, seconds);
            jedis.close();
            return result;
        }
      
        @Override
        public Long ttl(String key) {
            Jedis jedis = jedisPool.getResource();
            Long result = jedis.ttl(key);
            jedis.close();
            return result;
        }
      
        @Override
        public Long incr(String key) {
            Jedis jedis = jedisPool.getResource();
            Long result = jedis.incr(key);
            jedis.close();
            return result;
        }
      
        @Override
        public Long hset(String key, String field, String value) {
            Jedis jedis = jedisPool.getResource();
            Long result = jedis.hset(key, field, value);
            jedis.close();
            return result;
        }
      
        @Override
        public String hget(String key, String field) {
            Jedis jedis = jedisPool.getResource();
            String result = jedis.hget(key, field);
            jedis.close();
            return result;
        }
      
        @Override
        public Long hdel(String key, String... field) {
            Jedis jedis = jedisPool.getResource();
            Long result = jedis.hdel(key, field);
            jedis.close();
            return result;
        }
      
        /**
         * @Title: del
         * @Description: TODO(这里用一句话描述这个方法的作用)
         * @param key
         * @return
         */
        @Override
        public Long del(String key) {
            Jedis jedis = jedisPool.getResource();
            Long result = jedis.del(key);
            jedis.close();
            return result;
        }
      
    }
    
  • 配置:applicationContext-redis.xml

     <!-- 加载redis配置 -->
    <context:property-placeholder location="classpath:redis.properties" />
      
    <!-- 链接redis 单机版 -->
    <!-- 初始化jedisClientPool -->
    <bean id= "jedisClientPool" class =" com.silhouette.utils.JedisClientPool">
      	<property name = "jedisPool" ref ="jedisPool" />
    </bean>
    <!-- 配置一个jedisPool -->
    <bean id= "jedisPool" class= "redis.clients.jedis.JedisPool">
      <constructor-arg name="host" value ="${redis.host}"/>
      <constructor-arg name="port" value ="${redis.port}"/>
    </bean>
    
  • 测试

     @Test
    public void testJedisClient() throws Exception{
        ApplicationContext context = new ClassPathXmlApplicationContext(
          	"classpath:spring/applicationContext-redis.xml");
        JedisClient jedisClient = context.getBean(JedisClient.class);
        jedisClient.set("name","silhouette");
        String str = jedisClient.get("name");
        System. out .println(str);
    }
    

4.2.3 集群版实现类

  • 代码实现

    /**
     *
     * @ClassName: JedisClientCluster
     * @Description: Jedis集群版工具类
     *
     */
    public class JedisClientCluster implements JedisClient {
      
        @Autowired
        private JedisCluster jedisCluster;
      
        /**
         * @return the jedisCluster
         */
        public JedisCluster getJedisCluster() {
            return jedisCluster;
        }
      
        /**
         * @param jedisCluster the jedisCluster to set
         */
        public void setJedisCluster(JedisCluster jedisCluster) {
            this.jedisCluster = jedisCluster;
        }
      
        @Override
        public String set(String key, String value) {
            return jedisCluster.set(key, value);
        }
      
        @Override
        public String get(String key) {
            return jedisCluster.get(key);
        }
      
        @Override
        public Boolean exists(String key) {
            return jedisCluster.exists(key);
        }
      
        @Override
        public Long expire(String key, int seconds) {
            return jedisCluster.expire(key, seconds);
        }
      
        @Override
        public Long ttl(String key) {
            return jedisCluster.ttl(key);
        }
      
        @Override
        public Long incr(String key) {
            return jedisCluster.incr(key);
        }
      
        @Override
        public Long hset(String key, String field, String value) {
            return jedisCluster.hset(key, field, value);
        }
      
        @Override
        public String hget(String key, String field) {
            return jedisCluster.hget(key, field);
        }
      
        @Override
        public Long hdel(String key, String... field) {
            return jedisCluster.hdel(key, field);
        }
      
        /**
         * @Title: del
         * @Description: TODO(这里用一句话描述这个方法的作用)
         * @param key
         * @return
         */
        @Override
        public Long del(String key) {
            return jedisCluster.del(key);
        }
      
    }
    
  • 配置:applicationContext-redis.xml

    <bean id="jedisClientCluster" class="com.silhouette.utils.JedisClientCluster">
      	<property name="jedisCluster" ref="jedisCluster" />
    </bean>
    <bean id="jedisCluster" class="redis.clients.jedis.JedisCluster">
        <constructor-arg name="nodes">
          <set>
              <bean class="redis.clients.jedis.HostAndPort">
                <constructor-arg name="host" value ="${redis.host}"/>
                <constructor-arg name="port" value="7001" />
              </bean>
              <bean class="redis.clients.jedis.HostAndPort">
                <constructor-arg name="host" value ="${redis.host}"/>
                <constructor-arg name="port" value="7002" />
              </bean>
              <bean class="redis.clients.jedis.HostAndPort">
                <constructor-arg name="host" value ="${redis.host}"/>
                <constructor-arg name="port" value="7003" />
              </bean>
              <bean class="redis.clients.jedis.HostAndPort">
                <constructor-arg name="host" value ="${redis.host}"/>
                <constructor-arg name="port" value="7004" />
              </bean>
              <bean class="redis.clients.jedis.HostAndPort">
                <constructor-arg name="host" value ="${redis.host}"/>
                <constructor-arg name="port" value="7005" />
              </bean>
              <bean class="redis.clients.jedis.HostAndPort">
                <constructor-arg name="host" value ="${redis.host}"/>
                <constructor-arg name="port" value="7006" />
              </bean>
            </set>
        </constructor-arg>
    </bean>
    
  • 测试

     @Test
    public void testJedisClient() throws Exception{
        ApplicationContext context = new ClassPathXmlApplicationContext(
          	"classpath:spring/applicationContext-redis.xml");
        JedisClient jedisClient = context.getBean(JedisClient.class);
        jedisClient.set("name","silhouette");
        String str = jedisClient.get("name");
        System. out .println(str);
    }
    

注意:集群版的和单机不能同时使用