• Redis的高可用:哨兵和集群


    Redis的高可用:哨兵和集群

    我们在讨论分布式系统的时候,曾经谈过分布式系统要解决的是高并发、大数量和快速响应的问题。事实上,在互联网中,大部分的业务还是以查询数据为主,而非更改数据为主。在互联网出现高并发的时刻,查询关系数据库,会造成关系数据库的压力增大,容易导致系统宕机的严重后果。为了解决这个问题,一些开发者提出了数据缓存技术,数据缓存和关系数据库最大的不同在于,缓存的数据是保存在计算机内存上的,而关系数据库的数据主要保存在磁盘上。计算机检索内存的速度是远超过检索磁盘的,所以缓存技术可以在很大程度上提高整个系统的性能,降低数据库的压力。现今最流行的缓存技术当属NoSQL技术,其中又以现今的主流技术Redis最为成功,所以本章会从Redis的角度来讨论分布式缓存技术的应用,如果你使用的是其他缓存技术,也可参考其中的思想。

    使用缓存技术最大的问题是数据的一致性问题,缓存中存储的数据是关系数据库中数据的副本,因为缓存机制与数据库机制不同,所以它们的数据未必是同步的。虽然我们可以使用弱一致性去同步数据,但是现实很少会那么做,因为在互联网系统中,往往查询是可以允许部分数据不实时的,甚至是失真的,例如,一件商品的真实库存是100件,而现在显示是99件,这并不会妨碍用户继续购买。如果使用弱一致性,一方面会造成性能损失,另外一方面也会造成开发者工作量的大量增加。

    缓存技术可以极大提升读写数据的速度,但是也有弊端,这就如同人类发明的水库一样。在平时,对水库进行蓄水,当干旱时,就可以把水库的水放出来,维持正常的工作和生活。如果水库设计得太大,那么显然会造成资源的浪费。水库还有另外一个功能,就是当水量过大,在下游有被淹没的危险的时候,关闭闸门,不让水流淹没下游。不过水库也会造成威胁,当水量实在太大,超过有限的水库容量的时候,就会溢出,这时会以更强的冲击力冲毁下游。缓存技术也是一样的,因为它是基于内存的,内存的大小要比磁盘小得多,同时成本也比磁盘高得多。因此缓存太大会浪费资源,过小,则在面临高并发的时候,可能会被快速填满,从而导致内存溢出、缓存服务失败,进而引发一系列的严重问题。

    在一般情况下,单服务器缓存已经很难满足分布式系统大数量的要求,因为单服务器的内存空间是有限的,所以当前也会使用分布式缓存来应对。分布式缓存的分片算法和分布式数据库系统的算法大同小异,这里就不再讨论分片算法了。一般情况下,缓存技术使用起来比关系数据库简单,因为分布式数据库还会有事务和协议,而缓存数据一般不要求一致性,数据类型也远不如关系数据库丰富。缓存数据的用途大多是查询,查询和更新不同,对实时性没有那么高的要求,允许有一定的失真,这就给性能的优化带来了更大的空间。

    当然相对关系数据库来说,缓存技术速度更快,正常来说,使用Redis的速度会是使用MySQL的几倍到十几倍。可见缓存能极大地优化分布式系统的性能,但是并不是说缓存可以代替关系数据库。首先,缓存主要基于内存的形式存储数据,而关系数据库主要是基于磁盘;内存空间相对有限,价格相对较高,而磁盘空间相对较大,价格相对较低。其次,内存一旦失去电源,数据就会丢失。虽然Redis提供了快照(RDB)和记录追加写命令(AOF)这两种形式进行持久化,但是机制相对简单,难以保证数据不丢失。关系数据库则有其完整的理论和实现,能够有效使用事务和其他机制保证数据的完整性和一致性。因此,当前用缓存技术代替关系数据库技术是不太现实的,但是可以使用缓存技术来实现网站常见的数据查询,这能大幅度地提升性能。一般来说,适合使用缓存的场景包含以下几种。

    • 大部分是读业务数据的系统(一般互联网系统都满足该条件)。
    • 需要快速响应的系统。
    • 需要预备数据(在系统或者某项业务前准备那些经常访问的数据到缓存中,以便于系统开始就能够快速响应,也称为预热数据)的系统。
    • 对数据安全和一致性要求不太严格的系统。

    有适合使用缓存的场景,当然也会有不适合使用缓存的场景。

    • 读业务数据少且写入频繁的系统。
    • 对数据安全和一致性有严格要求的系统。

    在使用缓存前,我会从3个方面进行考虑。

    • 业务数据常用吗?后续命中率如何?命中率很低的数据,没有必要写入缓存。
    • 该业务数据是读的多还是写的多,如果是写的多,需要频繁写入关系数据库,也没有必要使用缓存。
    • 业务数据大小如何?如果要存储很庞大的内容,就会给缓存系统带来很大的压力,有没有必要?能截取最有价值的部分进行缓存而不全部缓存吗?

    经过以上考虑,觉得有必要使用缓存,就可以启动缓存了。在当前互联网中,缓存系统一般由Redis来完成,所以后续我们会集中讨论Redis,就不再讨论其他缓存系统了。本书采用的是Redis的5.0.5版本,如果采用别的版本,在配置项上会有少量不同,不过也大同小异,不会有太大的问题。

    16.1 Redis的高可用

    在Redis中,缓存的高可用分两种,一种是哨兵,另外一种是集群,下面我们会用两节分别讨论它们。不过在讨论它们之前,需要引入对Redis的依赖,如代码清单16-1所示。

    代码清单16-1 引入spring-boot-redis依赖(chapter16模块)

    <dependency>
       <groupId>org.springframework.boot</groupId>
       <artifactId>spring-boot-starter-data-redis</artifactId>
       <exclusions>
          <!--不依赖Redis的异步客户端lettuce-->
          <exclusion>
             <groupId>io.lettuce</groupId>
             <artifactId>lettuce-core</artifactId>
          </exclusion>
       </exclusions>
    </dependency>
    <!--引入Redis的客户端驱动jedis-->
    <dependency>
       <groupId>redis.clients</groupId>
       <artifactId>jedis</artifactId>
    </dependency>

    这里引入了Redis的依赖,并且选用Jedis作为客户端,没有使用Lettuce。这里解释一下不使用Lettuce的原因。Lettuce是一个可伸缩的线程安全的Redis客户端,多个线程可以共享同一个Redis连接,因为线程安全,所以会牺牲一部分的性能。但是一般来说,使用缓存并不需要很高的线程安全,更注重的是性能。Jedis是一种多线程非安全的客户端,具备更高的性能,所以企业选择的时候往往还是以使用它为主。

    16.1.1 哨兵模式

    在Redis的服务中,可以有多台服务器,还可以配置主从服务器,通过配置使得从机能够从主机同步数据。在这种配置下,当主Redis服务器出现故障时,只需要执行故障切换(failover)即可,也就是作废当前出故障的主Redis服务器,将从Redis服务器切换为主Redis服务器即可。这个过程可以由人工完成,也可以由程序完成,如果由人工完成,则需要增加人力成本,且容易产生人工错误,还会造成一段时间的程序不可用,所以一般来说,我们会选择使用程序完成。这个程序就是我们所说的哨兵(sentinel),哨兵是一个程序进程,它运行于系统中,通过发送命令去检测各个Redis服务器(包括主从Redis服务器),如图16-1所示。

     

     

    图16-1 单个哨兵模式

    图16-1中有2个Redis从服务器,它们会通过复制Redis主服务器的数据来完成同步。此外还有一个哨兵进程,它会通过发送命令来监测各个Redis主从服务器是否可用。当主服务器出现故障不可用时,哨兵监测到这个故障后,就会启动故障切换机制,作废当前故障的主Redis服务器,将其中的一台Redis从服务器修改为主服务器,然后将这个消息发给各个从服务器,使得它们也能做出对应的修改,这样就可以保证系统继续正常工作了。通过这段论述大家可以看出,哨兵进程实际就是代替人工,保证Redis的高可用,使得系统更加健壮。

    然而有时候单个哨兵也可能不太可靠,因为哨兵本身也可能出现故障,所以Redis还提供了多哨兵模式。多哨兵模式可以有效地防止单哨兵不可用的情况,如图16-2所示。

     

     

    图16-2 多哨兵模式

    在图16-2中,多个哨兵会相互监控,使得哨兵模式更为健壮,在这个机制中,即使某个哨兵出现故障不可用,其他哨兵也会监测整个Redis主从服务器,使得服务依旧可用。不过,故障切换方式和单哨兵模式的完全不同,这里我们通过假设举例进行说明。假设Redis主服务器不可用,哨兵1首先监测到了这个情况,这个时候哨兵1不会立即进行故障切换,而是仅仅自己认为主服务器不可用而已,这个过程被称为主观下线。因为Redis主服务器不可用,跟着后续的哨兵(如哨兵2和3)也会监测到这个情况,所以它们也会做主观下线的操作。如果哨兵的主观下线达到了一定的数量,各个哨兵就会发起一次投票,选举出新的Redis主服务器,然后将原来故障的主服务器作废,将新的主服务器的信息发送给各个从Redis服务器做调整,这个时候就能顺利地切换到可用的Redis服务器,保证系统持续可用了,这个过程被称为客观下线。

    为了演示这个过程,我先给出自己的哨兵和Redis服务器的情况,如表16-1所示。

    表16-1 服务分配情况(略)

    这样设计的架构,就如同图16-2一样,下面我们需要对各个服务进行配置。首先修改Redis主服务器配置(192.168.224.131)的内容,在Redis安装目录中找到redis.config文件,打开它,可以发现有很多配置项和注释。只需要对某些配置项进行修改即可,需要修改的配置项代码如下:

    # 禁用保护模式
    protected-mode no
    # 修改可以访问的IP,0.0.0.0代表可以跨域访问
    bind 0.0.0.0
    # 设置Redis服务密码
    requirepass 123456

    然后再修改两台从服务器的配置,请注意,它们俩的配置是相同的。在Redis安装目录中找到redis.config文件,然后也是对相关的配置项进行修改,代码如下:

    # 禁用保护模式
    protected-mode no
    # 修改可以访问的IP,0.0.0.0代表可以跨域访问
    bind 0.0.0.0
    # 设置Redis服务密码
    requirepass 123456
    # 配置从哪里复制数据(也就是配置主Redis服务器) 
    replicaof 192.168.224.131 6379
    # 配置主Redis服务器密码 
    masterauth 123456

    以上的配置都有清晰的注释,请自行参考。从服务器的配置只是比主服务器多了replicaof和masterauth两个配置项。

    上述的两个配置只是在配置Redis的服务器,此外我们还需要配置哨兵。同样,在Redis安装目录下,找到sentinel.conf文件,然后把3个哨兵服务的配置都改成以下配置。

    # 禁止保护模式
    protected-mode no
    
    # 配置监听的主服务器,这里sentinel monitor 代表监控,
    # mymaster 代表服务器名称,可以自定义
    # 192.168.224.131 代表监控的主服务器
    # 6379代表端口
    # 2 代表只有在2个或者2个以上的哨兵认为主服务器不可用的时候,才进行客观下线
    sentinel monitor mymaster 192.168.224.131 6379 2
    
    # sentinel auth-pass定义服务的密码
    # mymaster 服务名称
    # 123456 Redis服务器密码
    sentinel auth-pass mymaster 123456

    上述的配置只是在原有的其他配置项上按需进行修改。代码中已经给出了清晰的注释,请读者自行参考。

    有了这些配置,我们就可以进入Redis的安装目录,使用下面的命令启动服务了。

    # 启动Redis服务
    ./src/redis-server ./redis.conf
    
    # 启动哨兵进程服务
    ./src/redis-sentinel ./sentinel.conf

    需要注意的是启动的顺序,首先是主Redis服务器,然后是从Redis服务器,最后才是3个哨兵。启动之后,观察最后一个启动的哨兵,可以看到图16-3所示的信息。

     

     

    图16-3 哨兵进程输出信息

    从图16-3中可以看到主从服务器和哨兵的相关信息,说明我们的多哨兵模式已经搭建好了。

    上述的哨兵模式配置好后,就可以在Spring Boot环境中使用了。首先需要配置YAML文件,如代码清单16-2所示。

    代码清单16-2 在Spring Boot中配置哨兵(chapter16模块)

    spring:
      redis:
        # 配置哨兵
        sentinel:
          # 主服务器名称
          master: mymaster
          # 哨兵节点
          nodes: 192.168.224.131:26379,192.168.224.133:26379,192.168.224.134:26379
        # 登录密码
        password: 123456
        # Jedis配置
        jedis:
          # 连接池配置
          pool:
            # 最大等待1秒
            max-wait: 1s
            # 最大空闲连接数
            max-idle: 10
            # 最大活动连接数
            max-active: 20
            # 最小空闲连接数
            min-idle: 5

    这样就配置好了哨兵模式下的Redis,为了测试它,可以修改Spring Boot的启动类,如代码清单16-3所示。

    代码清单16-3 测试哨兵(chapter16模块)

    package com.spring.cloud.chapter16.main;
    /**** imports ****/
    @SpringBootApplication
    @RestController
    @RequestMapping("/redis")
    public class Chapter16Application {
    
       public static void main(String[] args) {
           SpringApplication.run(Chapter16Application.class, args);
       }
    
       // 注入StringRedisTemplate对象,该对象操作字符串,由Spring Boot机制自动装配
       @Autowired
       private StringRedisTemplate stringRedisTemplate = null;
    
       // 测试Redis写入
       @GetMapping("/write")
       public Map<String, String> testWrite() {
          Map<String, String> result = new HashMap<>();
          result.put("key1", "value1");
          stringRedisTemplate.opsForValue().multiSet(result);
          return result;
       }
    
       // 测试Redis读出
       @GetMapping("/read")
       public Map<String, String> testRead() {
          Map<String, String> result = new HashMap<>();
          result.put("key1", stringRedisTemplate.opsForValue().get("key1"));
          return result;
       }    
    }

    这里的testWrite方法是写入一个键值对,testRead方法是读出键值对。我们先在浏览器请求http://localhost:8080/redis/write,然后到各个Redis主从服务器中查看,都可以看到键值对(key1->value1)。当某个哨兵、Redis服务器或者主Redis服务器出现故障时,哨兵都会进行监测,并且通过主观下线或者客观下线进行修复,使得Redis服务能够具备高可用的特性。只是,在进行客观下线的时候,也需要一个时间间隔进行修复,这是我们需要注意的。默认是30秒,可以通过Redis的sentinel.conf文件的sentinel down-after-milliseconds进行修改,例如修改为60秒:

    sentinel down-after-milliseconds mymaster 60000

    16.1.2 Redis集群

    除了可以使用哨兵模式外,还可以使用Redis集群(cluster)技术来实现高可用,不过Redis集群是3.0版本之后才提供的,所以在使用集群前,请注意你的Redis版本。不过在学习Redis集群前,我们需要了解哈希槽(slot)的概念,为此先看一下图16-4。

     

     

    图16-4 哈希槽概念

    图16-4中有整数1~6的图形为一个哈希槽,哈希槽中的数字决定了数据将发送到哪台主Redis服务器进行存储。每台主服务器会配置1台到多台从Redis服务器,从服务器会同步主服务器的数据。那么它的工作机制是什么样的呢?下面我们来进行解释。

    我们知道Redis是一个key-value缓存,假如计算key的哈希值,得到一个整数,记为hashcode。如果此时执行:

    n = hashcode % 6 + 1

    得到的n就是一个1到6之间的整数,然后通过哈希槽就能找到对应的服务器。例如,n=4时就会找到主服务器1的Redis服务器,而从服务器1就是其从服务器,会对数据进行同步。

    在Redis集群中,大体也是通过相同的机制定位服务器的,只是Redis集群的哈希槽大小为(214=16 384),也就是取值范围为区间[0, 16383],最多能够支持16 384个节点,Redis设计师认为这个节点数已经足够了。对于key,Redis集群会采用CRC16算法计算key的哈希值,关于CRC16算法,本书就不论述了,感兴趣的读者可以自行查阅其他资料进行了解。当计算出key的哈希值(记为hashcode)后,通过对16 384求余就可以得到结果(记为n),根据它来寻找哈希槽,就可以找到对应的Redis服务器进行存储了。它们的计算公式为:

    # key为Redis的键,通过CRC16算法求哈希值
    hashcode = CRC16(key);
    # 求余得到哈希槽中的数字,从而找到对应的Redis服务器 
    n = hashcode % 16384

    这样n就会落入Redis集群哈希槽的区间[0, 16383]内,从而进一步找到数据。下面举例进行说明,如图16-5所示。

     

     

    图16-5 Redis集群工作原理

    这里假设有3个Redis主服务器(或者称为节点),用来存储缓存的数据,每一个主服务器都有一个从服务器,用来复制主服务器的数据,保证高可用。其中哈希槽分配如下。

    • Redis主服务器1:分配哈希槽区间为[0, 5460]。
    • Redis主服务器2:分配哈希槽区间为[5461, 10922]。
    • Redis主服务器3:分配哈希槽区间为[10923, 16383]。

    这样通过CRC16算法求出key的哈希值,再对16 384求余数,就知道n会落入哪个哈希槽里,进而决定数据存储在哪个Redis主服务器上。

    注意,集群中各个Redis服务器不是隔绝的,而是相互连通的,采用的是PING-PONG机制,内部使用了二进制协议优化传输速度和带宽,如图16-6所示。

    从图16-6中可以看出,客户端与Redis节点是直连的,不需要中间代理层,并且不需要连接集群所有节点,只需连接集群中任何一个可用节点即可。在Redis集群中,要判定某个主节点不可用,需要各个主节点进行投票,如果半数以上主节点认为该节点不可用,该节点就会从集群中被剔除,然后由其从节点代替,这样就可以容错了。因为这个投票机制需要半数以上,所以一般来说,要求节点数大于3,且为单数。因为如果是双数,如4,投票结果可能会为2:2,就会陷入僵局,不利于这个机制的执行。

     

     

    图16-6 Redis集群中各个节点是联通的

    在某些情况下,Redis集群会不可用,当集群不可用时,所有对集群的操作做都不可用。那么什么时候集群不可用呢?一般来说,分为两种情况。

    • 如果某个主节点被认为不可用,并且没有从节点可以代替它,那么就构建不成哈希槽区间[0, 16383],此时集群将不可用。
    • 如果原有半数以上的主节点发生故障,那么无论是否存在可代替的从节点,都认为该集群不可用。

    Redis集群是不保证数据一致性的,这也就意味着,它可能存在一定概率的数据丢失现象,所以更多地使用它作为缓存,会更加合理。

    有了上述的理论知识,下面让我们来搭建Redis集群环境。我使用的是Ubuntu来搭建Redis环境,首先进入root用户,然后执行以下命令:

    cd /usr
    # 创建Redis目录,并进入目录
    mkdir redis
    cd ./redis 
    # 下载Redis
    wget http://download.redis.io/releases/redis-5.0.5.tar.gz
    # 解压缩安装包
    tar xzf redis-5.0.5.tar.gz
    # 进入安装目录
    cd redis-5.0.5
    # 编译安装Redis
    make

    执行上述命令就安装好了Redis,然后在/usr/redis/redis-5.0.5下创建文件夹cluster,并在其下面创建目录7001、7002、7003、7004、7005和7006,接着将/usr/redis/redis-5.0.5/redis.conf文件复制到目录7001、7002、7003、7004、7005下,最后执行如下命令。

    # 进入安装目录
    cd /usr/redis/redis-5.0.5
    # 创建文件夹cluster和其子目录
    mkdir cluster
    cd ./cluster 
    mkdir 7001 7002 7003 7004 7005 7006
    # 复制文件
    cp ../redis.conf ./7001
    cp ../redis.conf ./7002
    cp ../redis.conf ./7003
    cp ../redis.conf ./7004
    cp ../redis.conf ./7005
    cp ../redis.conf ./7006
    # 赋予目录下所有文件全部权限
    chmod -R 777 ./

    这样从7001到7006的目录下就都有一份Redis的启动配置文件了,之所以让目录起名为这些数字,是因为我将会使用这些数字作为端口来分别启动Redis服务。下面,我们首先来修改7001下的redis.conf文件,只修改文件的部分配置,修改的内容如下:

    # 关闭保护模式
    protected-mode no
    # 允许跨域访问
    bind 0.0.0.0
    # 主机密码
    masterauth 123456
    # 登录密码
    requirepass 123456
    # 端口7001
    port 7001
    # 启用集群模式
    cluster-enabled yes
    # 集群配置文件
    cluster-config-file nodes-7001.conf
    # 和集群节点通信的超时时间
    cluster-node-timeout 5000
    # 采用添加写命令的模式备份
    appendonly yes
    # 备份文件名称
    appendfilename "appendonly-7001.aof"
    # 采用后台运行Redis服务
    daemonize yes
    # PID命令文件
    pidfile /var/run/redis_7001.pid

    然后再修改7002到7006目录下的redis.conf文件,修改时将所有配置项中的“7001”替换为对应的数字即可,这样我们就可以得到6个启动Redis服务的配置文件了。

    接下来就是配置和创建集群了,这里Redis 5也为此提供了工具,并且放在Redis安装目录的子文件夹/utils/create-cluster(我使用的系统全路径为/usr/redis/redis-5.0.5/utils/create-cluster)中。打开这个目录,就可以发现一个create-cluster文件,我们修改它的权限(命令chmod 777 eate-cluster),然后打开它,修改它的内容,代码如下:

    #!/bin/bash
    
    # Settings
    # 端口,从7000开始,SHELL会自动加1后,找到7001到7006的Redis服务实例
    PORT=7000
    # 创建超时时间
    TIMEOUT=2000
    # Redis节点数
    NODES=6
    # 每台主机的从机数
    REPLICAS=1 # ①
    # 密码,和我们配置的一致
    PASSWORD=123456
    
    ......
    #### 以下给redis-cli 命令添加配置的密码 #### 
    if [ "$1" == "create" ]
    then
       HOSTS=""
       while [ $((PORT < ENDPORT)) != "0" ]; do
          PORT=$((PORT+1))
          HOSTS="$HOSTS 192.168.224.135:$PORT"
       done
       ../../src/redis-cli --cluster create $HOSTS -a $PASSWORD --cluster-replicas $REPLICAS
       exit 0
    fi
    
    if [ "$1" == "stop" ]
    then
       while [ $((PORT < ENDPORT)) != "0" ]; do
          PORT=$((PORT+1))
          echo "Stopping $PORT"
          ../../src/redis-cli -p $PORT -a $PASSWORD shutdown nosave
       done
       exit 0
    fi
    
    if [ "$1" == "watch" ]
    then
       PORT=$((PORT+1))
       while [ 1 ]; do
          clear
          date
          ../../src/redis-cli -p $PORT -a $PASSWORD cluster nodes | head -30
          sleep 1
       done
       exit 0
    fi
    ......
    if [ "$1" == "call" ]
    then
       while [ $((PORT < ENDPORT)) != "0" ]; do
          PORT=$((PORT+1))
          ../../src/redis-cli -p $PORT -a $PASSWORD $2 $3 $4 $5 $6 $7 $8 $9
       done
       exit 0
    fi
    ...... 

    这段配置看起来挺复杂,实际是很简单的,我修改的是代码中加粗的部分,其余的并未改动。首先修改了端口,例如,端口从7000开始遍历,这样循环加1,就可以找到7001到7006的服务实例。其次给redis-cli命令,加入配置的密码,修改IP。这里尽量不要使用localhost和127.0.01指向本机IP,应该使用该服务器在网络中的IP,否则不在本机客户端登录时,就会出现一些没有必要的错误。至此,所有的配置就都完成了。

    跟着我们需要编写脚本,使得我们能够创建、停止和启动集群。为此,在Linux中以root用户登录,然后执行以下命令:

    # 进入集群目录
    cd /usr/redis/redis-5.0.5/cluster
    # 创建3个脚本文件
    touch create.sh start.sh shutdown.sh
    # 赋予脚本文件全部权限
    chmod 777 *.sh

    从命令中可以看出,我们创建了3个Shell脚本文件。

    • create.sh:用来启动Redis服务,然后创建集群。
    • start.sh:用来在集群关闭后,启动集群的各个节点。
    • shutdown.sh:关闭运行中的集群的各个节点。

    跟着来编写start.sh,代码如下:

    # 进入集群工具目录
    cd /usr/redis/redis-5.0.5/utils/create-cluster
    # 启动集群各个Redis实例,参数为start
    ./create-cluster start

    这个脚本是运行集群的各个节点,只是此时集群还没有被创建,所以还不能运行这个脚本。跟着是shutdown.sh的编写,代码如下:

    # 进入集群工具目录
    cd /usr/redis/redis-5.0.5/utils/create-cluster
    # 停止集群各个Redis实例,参数为stop
    ./create-cluster stop

    这个脚本是停止集群中的各个实例,当然集群现在没有创建和运行,所以它暂时也不能运行。

    为了让start.sh和shutdown.sh能够运行,我们需要创建Redis集群,下面编写create.sh,内容如下:

    # 在不同端口启动各个Redis服务 ①
    /usr/redis/redis-5.0.5/src/redis-server /usr/redis/redis-5.0.5/cluster/7001/redis.conf
    
    /usr/redis/redis-5.0.5/src/redis-server /usr/redis/redis-5.0.5/cluster/7002/redis.conf
    
    /usr/redis/redis-5.0.5/src/redis-server /usr/redis/redis-5.0.5/cluster/7003/redis.conf
    
    /usr/redis/redis-5.0.5/src/redis-server /usr/redis/redis-5.0.5/cluster/7004/redis.conf
    
    /usr/redis/redis-5.0.5/src/redis-server /usr/redis/redis-5.0.5/cluster/7005/redis.conf
    
    /usr/redis/redis-5.0.5/src/redis-server /usr/redis/redis-5.0.5/cluster/7006/redis.conf
    
    # 创建集群,使用参数create  ②
    cd /usr/redis/redis-5.0.5/utils/create-cluster
    ./create-cluster create 

    这里分为两段,其中第①段是让Redis在各个端口下启动实例,第②段是创建集群。然后我们运行create.sh脚本,就可以看到图16-7所示的提示。

     

     

    图16-7 创建Redis集群的提示信息

    注意图16-7中框中的信息,信息类型大致分为两种。第一种是哈希槽的分配情况,这里提示了分为3个主节点,然后第一个的哈希槽区间为[0, 5460],第二个的为[5461, 10922],第三个的为[10923, 16383]。第二种是从节点的情况,7005端口为7001端口的从节点,7006端口为7002端口的从节点,7004端口为7003端口的从节点。然后它询问我们是否接受该配置,只要输入“yes”回车后,稍等一会儿,它就会创建Redis集群了。

    创建好了Redis集群,可以通过命令来验证它,我们先通过redis-cli登录集群,在Linux中执行如下命令。

    # 进入目录
    cd /usr/redis/redis-5.0.5
    # 登录Redis集群:
    # -c代表以集群方式登录 
    # -p 选定登录的端口 
    # -a 登录集群的密码
    ./src/redis-cli -c -p 7001 -a 123456

    这样就能够登录Redis集群了,然后我们可以执行几个Redis的命令来观察执行的情况,执行的命令如下:

    set key1 value1
    Set key2 value2
    set key3 value3
    Set key4 value4
    set key5 value5

    我执行的结果如图16-8所示。

     

     

    图16-8 验证集群

    在图16-8中可以看到,在执行命令的时候,Redis会打印出一个哈希槽的数字,然后重新定位到具体的Redis服务器。这些都是Redis集群机制完成的,对于客户端来说,一切都是透明的。

    至此,Redis集群我们就搭建成功了。当我们想停止集群的时候,可以执行之前创建好的shutdown.sh。当我们需要启动已经停止的集群的时候,只需要执行start.sh即可。

    上述我们搭建了Redis的集群,跟着就要在Spring Boot中使用它了。在Spring Boot中使用它并不麻烦,只需要先注释掉代码清单16-3中的配置,然后在application.yml文件中加入代码清单16-4所示的代码即可。

    代码清单16-4 Spring Boot配置Redis集群(chapter16模块)

    spring:
      redis:
        # 登录密码
        # Jedis配置
        jedis:
          # 连接池配置
          pool:
            # 最大等待1秒
            max-wait: 1s
            # 最大空闲连接数
            max-idle: 10     
            # 最大活动连接数
            max-active: 20
            # 最小空闲连接数
            min-idle: 5
        # 配置Redis集群信息
        cluster:
           # 集群节点信息
          nodes: 192.168.224.135:7001,192.168.224.135:7002,192.168.224.135:7003,192.168.224.135:7004,192.168.224.135:7005,192.168.224.135:7006
          # 最大重定向数,一般设置为5,
          # 不建议设置过大,过大容易引发重定向过多的异常
          max-redirects: 5
        password: 123456

    这样就在Spring Boot中配置好了,可以像往常一样通过RedisTemplate或者StringRedisTemplate来操作Redis集群了。

    16.2 使用一致性哈希(ShardedJedis)

    在我们讨论了Redis集群后,大家可以知道,集群实际包含了高可用,也包含了缓存分片两个功能。但是对于集群来说,分片算法是固定且不透明的,可能会因为某种原因使得多数的数据,落入同一个Redis服务中,使负荷不同。有时候,我们还希望使用一致性哈希算法,关于该算法,我们在分布式数据库分片算法中也进行了详尽的介绍,所以这里就不再重复了。在Jedis中还提供了类ShardedJedis,有了这个类,我们可以很容易地在Jedis客户端中使用一致性哈希算法。

    ShardedJedis内部已经采用了一致性哈希算法,并且为每个Redis服务器提供了虚拟节点(虚拟节点个数为权重×160)。下面让我们通过代码来学习如何使用ShardedJedis。首先,我们需要创建一个ShardedJedis连接池,于是在Spring Boot的启动类(Chapter16Application.java)中加入代码清单16-5所示的代码。

    代码清单16-5 使用ShardedJedis(chapter16模块)

    // ShardedJedis 连接池
    private ShardedJedisPool pool = null;
    
    @Bean
    public ShardedJedisPool initJedisPool() {
        // 端口数组
        int[] ports = {7001, 7002, 7003};
        // 权重数组
        int[] weights = {1, 2, 1};
        // 服务器
        String host = "192.168.224.136";
        // 密码
        String password = "123456";
        // 连接超时时间
        int connectionTimeout = 2000;
        // 读超时时间
        int soTimeout = 2000; 
        List<JedisShardInfo> shardList = new ArrayList<>();
        for (int i=0; i < ports.length; i++) { 
            // 创建JedisShard信息 
            JedisShardInfo shard = new JedisShardInfo(
                 host, ports[i], connectionTimeout, soTimeout,weights[i]); //①
            // 设置密码
            shard.setPassword(password);
            // 加入到列表中
            shardList.add(shard);
        }
        // 连接池配置
        JedisPoolConfig poolCfg = new JedisPoolConfig();
        poolCfg.setMaxIdle(10);
        poolCfg.setMinIdle(5);
        poolCfg.setMaxIdle(10);
        poolCfg.setMaxTotal(30);
        poolCfg.setMaxWaitMillis(2000);
        // 创建ShardedJedis连接池
        pool = new ShardedJedisPool(poolCfg, shardList); // ②
        return pool;
    }

    这里我在一台机器上模拟了3个Redis服务,它们的端口分别为7001、7002和7003。现实中每台服务器的性能都可能是不同的,这里假设7002端口的服务性能要好很多,所以在权重数组中将它的权重设置为2,这样数据缓存到7002服务中的概率就更高了。在代码①处,创建了单个JedisShardInfo对象,然后将它放到一个列表中。代码②处创建了一个JedisShard连接池对象。

    上面的代码创建了JedisShard连接池,这样就可以从中取出ShardedJedis对象去操作Redis了。下面让我们在启动类(Chapter16Application.java)中加入代码清单16-6所示的代码来进行演示。

    代码清单16-6 使用ShardedJedis(chapter16模块)

    // 测试Redis写入
    @GetMapping("/test2")
    public Map<String, String> test2() {
       Map<String, String> result = new HashMap<>();
       ShardedJedis jedis = null;
       try {
          // 获得ShardedJedis对象    ①
          jedis = pool.getResource(); 
          // 写入Redis
          jedis.set("key1", "value1"); 
          // 从Redis读出
          result.put("key1", jedis.get("key1")); 
          return result;
       } finally {
          // 最后释放连接
          jedis.close(); // ②
       }
    }

    代码也比较简单,其中①处是获取ShardedJedis对象,然后设置一个键值对,再从中读出来放到Map中。②处是关闭连接,以避免过多的空闲连接得不到释放。

    ShardedJedis使用起来也比较方便,但是无法与Spring提供RedisTemplate和StringRedisTemplate结合。同时,也没有类似哨兵模式和集群模式下主从机主动修复的机制,所以在高可用方面较差。因为它的缺点,所以选择它时需要慎重。

    ShardedJedis的原理其实也不难,我们知道Redis是键值对(key-value)缓存,要操作数据就必须要有键(key),所以在做Redis命令操作时,会先根据key求出其哈希值(hashcode),然后再根据哈希值和一致性哈希算法,选择具体的Redis节点。在ShardedJedis的一致性哈希算法中,会给每一个真实的Redis节点制造出“160×权重”个虚拟节点,使数据尽可能平均地分布到每一个节点中。

    16.3 分布式缓存实践

    在分布式缓存中,还会遇到许多的问题。例如,保存的对象过大,网络传输较慢,又如缓存雪崩等,所以要用好分布式缓存也需要考虑一些常见的问题。

    16.3.1 大对象的缓存

    在Java中,有些对象可能很大,尤其是那些读取文件的对象。对于大的对象,一次性读出来需要使用很多的网络传输资源,这样会引发性能瓶颈。在Redis官网中,建议我们使用Redis的哈希(Hash)结构去缓存大对象的内容,把它的属性保存到哈希结构的字段(field)中。在读取很大的对象时,往往只需要先读取部分内容,后续再根据需要读取对应的字段即可,如图16-9所示。

     

     

    图16-9 将大对象以哈希结构缓存到Redis中

    也许还有一种可能,就是哈希结构中的某个字段的值也是大对象,例如一本书有几十万字。一般来说,这个时候会做两方面的考虑。一方面是有必要全部保存吗?是否保存部分最常用的即可?另一方面,可以拆分字符串,将原有的字段拆分为多个字段,拿图16-3来说,假如field3需要存储的是很大的字符串,我们可以将其拆分为field3_1, field3_2, …, field3_n,分段保存字符串,然后读取的时候,也分段读取即可。

    16.3.2 缓存穿透、并发和雪崩

    当客户端通过一个键去访问缓存时,缓存没有数据,跟着又去访问数据库,数据库也没有数据,这时因为数据库返回也为空,所以不会将该数据放到缓存中,我们把这样的情况称为缓存穿透,如图16-10所示。

     

    图16-10 缓存穿透

    如果我们再次请求这个的键,还是会按照此流程再走一遍。如果出现高并发访问这个键的情况,数据就会频繁访问数据库,给数据库带来很大的压力,甚至可能导致数据库出现故障,这便是缓存穿透带来的危害。

    为了解决这个问题,相信大家很快想到,如果在访问数据库后也得到控制,可以在缓存中记录一个字符串(如“null”,代表是空值),即可解决这个问题。但是这样会引发一个问题,就是在很多时候我们访问数据库也得不到数据,这样就会在缓存中存储大量的空值,这显然也会给缓存带来一定的浪费。为此可以增加一个判断,就是判断该键是否是一个常用的数据,如果是常用的,就将它也写入缓存中,这样就不会出现缓存穿透导致数据库被频繁访问的情况了,如图16-11所示。

     

     

    图16-11 解决缓存穿透问题

    在使用缓存的过程中,我们往往还会设置超时时间,当数据超时的时候,就不能从缓存中读取数据了,而是到数据库中读取。有些数据是热点数据,例如我们最畅销的产品,假如在高并发期间,这个产品和它的关联信息在缓存中超时失效了,就会导致大量的请求访问数据库,给数据库带来很大的压力,甚至可能导致数据库宕机,类似这样的情况,我们称为缓存并发,如图16-12所示。

     

     

    图16-12 缓存并发

    为了防止出现缓存并发的情况,一般来说,我们可以采用以下几种方式避免缓存并发。

    • 限流:也就是防止过多的请求来访问缓存系统,从而导致压垮数据库,例如使用Resilience4j进行限流,但是这会影响并发线程数量。
    • 加锁:对缓存数据加锁,使得线程只能一条条地通过去访问,而不能并发访问,这样就能避免缓存并发的现象,但是分布式锁比较难以实现,所以一般来说我们不会考虑这个办法。
    • 错峰失效:网站一般是在上网高峰期或者热门商品抢购时,才会出现高并发现象,而这是有规律的,所以可以自己设置那些需要经常访问的缓存,错过这段时间失效,一般就不会出现缓存并发的现象了,这个做法的成本相对低,也容易实现,所以我比较推荐它。

    上述我们谈了缓存穿透和缓存并发,事实上,还有一种缓存雪崩,那什么是缓存雪崩呢?典型的情况是,我们在启动系统的时候,一般会把最常用的数据放入缓存中,并且设置一个固定的超时时间,这便是我们常说的预热数据,它有助于系统性能的提高。但是,因为设置了一个固定的超时时间,所以会导致在某个时间点有大量缓存的键值对数据超时,如果在这个时间点出现高并发,就会导致请求大量访问数据库,造成数据库压力过大,甚至宕机,这便是缓存雪崩,如图16-13所示。

     

     

    图16-13 缓存雪崩

    这里容易混淆的是缓存并发和缓存雪崩的概念,缓存并发是针对一个键值对来说的,而缓存雪崩是针对多个键值对在某个时间点同时超时来说的。一般来说,为了避免缓存雪崩,我们需要在预热数据的时候,防止所有数据都在一个时间点上超时。为此,可以设置不同的超时时间,来避免多个键值对同时失效。例如,key1失效是1小时,key2是1.5小时、key3是30分钟……这样就能够避免数据同时失效了。

    16.3.3 缓存实践的一些建议

    对于缓存的使用,我们需要遵循一定的规则,避免一些没有必要的麻烦。下面是我的一些建议。

    • 对于采用了微服务架构的系统,建议缓存服务器只存储某项业务的数据,不掺杂其他业务的数据,这样可以避免业务数据的耦合。
    • 对于存入缓存的预热数据,尽量设置不同的超时时间,以避免同时超时引发缓存雪崩。
    • 在使用缓存前,要判断应不应该使用缓存。
    • 对于大数据对象的缓存,应该考虑分而治之的办法,化简为零。
    • 缓存会造成数据的不一致,也可能存在一定的失真,但是性能好,能够支持高并发的访问,所以多用于读取数据,而对于更新数据,一定要以数据库为基准,不要轻信缓存。
    • 对于热门数据,应该考虑错峰失效,错峰更新,避免出现缓存并发现象。
    • 在需要大量操作Redis的时候,可以考虑采用流水线(pipeline)的方式,这样可以在很大程度上提高传输的效率。
    • 在读数据的时候,先读缓存再读数据库。在写数据的时候,先写数据库再写缓存。

    有了这些良好的习惯,相信在使用分布式缓存的时候,会减少许多不必要的麻烦。

    本文摘自《Spring Cloud微服务和分布式系统实践》

    国内流行的早期的微服务解决方案是阿里巴巴的Dubbo,但这是一个不完整的方案,当前Spring Cloud已成为业界流行的微服务搭建方案。因此,本书以讲解Spring Cloud为主。

    Pivotal团队收集了各个企业成功的分布式组件,用Spring Boot的形式对其进行封装,最终得到了Spring Cloud,简化了开发者的工作。Spring Cloud当前主要是通过Netflix(网飞)公司的组件来实施微服务架构,但是因为Netflix的组件更新较慢(如Zuul 2.x版本经常不能如期发布,最后取消),并且只按自身企业需要进行更新(如Hystrix停止增加新功能),所以Spring Cloud有“去Netflix组件”的趋势。不过,“去Netflix组件”也需要一定的时间,所以当前还是以Netflix组件为主,这也是本书的核心内容之一。从另外一个角度来看,组件的目的是完成分布式的某些功能,虽类别不同但思想相近,也就是“换汤不换药”。因此,现在学了Netflix组件,即使将来不再使用,也可以吸收其思想和经验,通过这些来对比将来需要学习的新组件,也是大有裨益的。

    为什么还要讲微服务之外的分布式系统的知识

    在编写本书的时候,我考虑了很久,除了Spring Cloud微服务的内容外,还要不要加入其他分布式系统的内容,如分布式发号机、分布式数据库、分布式事务和缓存等。加入这些内容,本书似乎就没有鲜明的特点了,内容会显得有点杂;不加入这些内容,企业构建分布式系统的讲解就会不全面。

    反复思考之后,我最终决定将一些常用的分布式知识也纳入本书进行讨论。换一个角度来考虑,微服务作为分布式系统的一种,其自身也是为了简化分布式系统的开发,满足企业生产实践的需要,同样,加入这些知识的讲解也是为了让企业能更好地搭建网站,和微服务架构的目的是一致的

  • 相关阅读:
    css选择器的优先级
    ECHO.js 纯javascript轻量级延迟加载
    Chrome测试网站加载时间与流量消耗
    演示:纯CSS实现自适应布局表格
    通过jquery-ui中的sortable来实现拖拽排序
    XAMPP下的composer的安装
    jQuery动态五星评分
    HTML,CSS编码规范
    mysql排序,可以对统计的数据进行排序
    thinkphp中配置信息的二维数组设置与使用
  • 原文地址:https://www.cnblogs.com/zeenzhou/p/13255535.html
Copyright © 2020-2023  润新知