在Redis中,缓存的高可用分两种,一种是哨兵,另外一种是集群,下面我们会用两节分别讨论它们。不过在讨论它们之前,需要引入对Redis的依赖,如代码清单16-1所示。
代码清单16-1 引入spring-boot-redis依赖(chapter16模块)
org.springframework.boot
spring-boot-starter-data-redis
io.lettuce
lettuce-core
redis.clients
jedis
这里引入了Redis的依赖,并且选用Jedis作为客户端,没有使用Lettuce。这里解释一下不使用Lettuce的原因。Lettuce是一个可伸缩的线程安全的Redis客户端,多个线程可以共享同一个Redis连接,因为线程安全,所以会牺牲一部分的性能。但是一般来说,使用缓存并不需要很高的线程安全,更注重的是性能。Jedis是一种多线程非安全的客户端,具备更高的性能,所以企业选择的时候往往还是以使用它为主。
在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 服务分配情况
服务进程类型 |
是否Redis主服务器 |
IP地址 |
服务端口 |
---|---|---|---|
Redis |
是 |
192.168.224.131 |
6397 |
Redis |
否 |
192.168.224.133 |
6397 |
Redis |
否 |
192.168.224.134 |
6397 |
Sentinel |
— |
192.168.224.131 |
26379 |
Sentinel |
— |
192.168.224.133 |
26379 |
Sentinel |
— |
192.168.224.134 |
26379 |
这样设计的架构,就如同图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 testWrite() {
Map result = new HashMap<>();
result.put("key1", "value1");
stringRedisTemplate.opsForValue().multiSet(result);
return result;
}
// 测试Redis读出
@GetMapping("/read")
public Map testRead() {
Map 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
除了可以使用哨兵模式外,还可以使用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主服务器(或者称为节点),用来存储缓存的数据,每一个主服务器都有一个从服务器,用来复制主服务器的数据,保证高可用。其中哈希槽分配如下。
这样通过CRC16算法求出key的哈希值,再对16 384求余数,就知道n会落入哪个哈希槽里,进而决定数据存储在哪个Redis主服务器上。
注意,集群中各个Redis服务器不是隔绝的,而是相互连通的,采用的是PING-PONG机制,内部使用了二进制协议优化传输速度和带宽,如图16-6所示。
从图16-6中可以看出,客户端与Redis节点是直连的,不需要中间代理层,并且不需要连接集群所有节点,只需连接集群中任何一个可用节点即可。在Redis集群中,要判定某个主节点不可用,需要各个主节点进行投票,如果半数以上主节点认为该节点不可用,该节点就会从集群中被剔除,然后由其从节点代替,这样就可以容错了。因为这个投票机制需要半数以上,所以一般来说,要求节点数大于3,且为单数。因为如果是双数,如4,投票结果可能会为2:2,就会陷入僵局,不利于这个机制的执行。
图16-6 Redis集群中各个节点是联通的
在某些情况下,Redis集群会不可用,当集群不可用时,所有对集群的操作做都不可用。那么什么时候集群不可用呢?一般来说,分为两种情况。
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脚本文件。
跟着来编写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集群了。
本文摘自《Spring Cloud微服务和分布式系统实践》
本书是讲述Spring Cloud微服务及其组件的专业技术书籍。微服务系统作为分布式系统的一种形式,必然会带有分布式系统的各种弊病,因此本书也会介绍分布式系统的一些常见知识,以更好满足企业构建系统的需求。
本书首先介绍分布式系统和微服务的概念以及技术基础;然后介绍Spring Cloud的主要组件,包含服务治理和服务发现、服务调用、断路器、API网关、服务配置和服务监控等,这部分是本书的主要内容;接着介绍企业实践中经常用到的分布式技术,包括分布式数据库事务、分布式Redis缓存等;最后介绍远程过程调用(RPC)以及微服务设计和高并发实践。