1.前言
1.1 单机时代
刚接触redis
的时候,为了能快速学习和了解这门技术,我们通常会在自己的电脑上部署一个redis
服务,以此来开启redis
学习之路
1.2 主从时代
随着对redis
的进一步深入,很快就会发现这门技术在很多场景下都能得到应用,比如:并发场景下对共享资源的控制(分布式锁)、高并发场景下对系统的保护(限流)、高并发场景下对响应时间的要求(缓存)
在生产环境使用redis
服务是否能像当初我们学习时那样,仅仅部署一个单实例redis
就可以呢?如果选择单实例部署,当该实例出现故障,使用redis
的业务场景都会随之受影响。为了降低单实例故障带来的影响,通常会选择冗余的方式来保证服务的高可靠性,在redis
中,我们称之为主-从
1.3 哨兵时代
redis
进行主-从
部署后是不是就可以高枕无忧了呢?当然不是,你还需要时时刻刻监控redis主
的健康状态,当其出现故障后能第一时间发现并能在从库
中完成选主
任务,否则同样会给相关业务带来影响。在redis
中,我们通常会使用哨兵
机制来帮我们完成监控
、选主
和通知
操作,从而使我们的redis
服务具备一定的高可靠性
1.4 集群分片时代
随着业务的飞速发展,redis
实例中存放的数据也越来越多,当需要存储25G
以上的数据时,估计你会选择一台32G
的机器进行部署这个看似简单的选择题却隐藏着很严重的问题:
redis
中存放的数据越多,意味着宕机后的恢复时间也越长,从而导致服务长时间不可用- 数据越多进行
rdb fork
操作的时候,阻塞redis
主线程的时间也越长,从而导致服务响应慢
面对此类问题我们通常会基于大而化小、分而治之
的思想进行解决,在redis
中我们称之为集群分片
技术
开篇对单机、主从、哨兵、分片进行了简单介绍,下文将结合实战展开细说。
2.redis安装
磨刀不误砍柴工
,在开始之前首先要保证我们的redis
服务可以正常启动并能提供指令操作。
2.1下载安装包
wget https://download.redis.io/redis-stable.tar.gz
2.2解压
tar -zxvf redis-stable.tar.gz
2.3 编译
➜ redis-stable make & make install
2.4 启动
redis-server
2.5 客户端连接测试
redis-cli
127.0.0.1:6379> set name bobo
OK
127.0.0.1:6379> get name
"bobo"
完成服务的安装与测试,接下来就可以放手进行实战,实战的第一部分主从
3.主从
3.1 部署图
主从
一共由3台机器组成,演示环境通过端口号
进行区分
3.2 创建conf目录
主从
由3个redis
服务组成,每个redis
服务对应的配置都不相同,因此需要创建conf
文件夹用于存放配置文件
➜ redis-stable mkdir conf
3.3 生成redis配置文件
默认情况下redis.conf
配置文件会有很多注释说明,为了让配置文件看上去清晰明了,使用如下命令来去除配置文件中的注释以及空行
➜ redis-stable cat redis.conf | grep -v "#" | grep -v "^$" > ./conf/redis-6379.conf
3.3 添加复制配置
主从
方式,从
需要知道应该从哪一个主
进行数据复制,因此需要在从
再配置文件中添加如下配置
replicaof 127.0.0.1 6380
3.4 拷贝redis配置文件
之前也有过说明,演示环境通过端口号
进行区分,因此配置文件中除了端口号
以及数据保存路径
不一样之外,其它的都一样,这里可以通过如下命令进行配置文件拷贝
➜ redis-stable sed 's/6379/6380/g' conf/redis-6379.conf > conf/redis-6380.conf
➜ redis-stable sed 's/6379/6381/g' conf/redis-6379.conf > conf/redis-6381.conf
3.5 启动redis
万事俱备,只欠东风
,准备工作完成之后,只需要逐个启动服务即可
3.5.1 启动主6379
从
在启动的时候需要和主
建立连接,因此应先启动主6379
通过日志文件可以了解到主6379
在启动的过程中会去加载rdb
文件用于数据恢复,并且从rdb
文件中加载了一个key
3.5.2 启动从6380
从6380
启动后,可以看到主6379
的日志内容有所增加
从6380
向主6379
请求数据同步,由于是第一次会进行全量同步,主6379 fork
进程生成rdb
文件,然后将生成好的rdb
文件传输给6380
从6380
启动过程中会去连接主6379
,接收主6379
传过来的rdb
文件,在清空旧数据之后加载rdb
文件进行数据同步
3.5.3 启动从6381
从6381
启动和从6380
启动是一样的流程,不再进行细说
3.5.4 重启6381
到这里,你已经知道从
在第一次连接主
后会进行全量同步,估计也会好奇非第一次连接会如何进行同步。想知道结果,只需要重启其中一个从
即可,这里选择重启从6381
重启后,会发现从6381
请求的是增量
同步而非全量
同步
通过主6379
的日志可以看到其接受了从6381
增量同步的请求,并从backlog
偏移量183开始发送了245字节的数据
这里可以猜想一下,主6379
若想知道应该增量同步哪些数据给从6381
,那么它一定得知道从6381
上一次同步到哪里,因此重启再次连接的时候,从6381
应该会将上一次同步到哪了的信息发给了主6379
关于数据同步部分,可以得出不知道从哪开始同步就选择全量同步
,知道从哪开始同步就选择增量同步
的结论
3.6 主从如何保证数据一致性?
前面提到过,主6379
在接到从的全量同步请求后会生成rdb
文件,在生成rdb
文件的过程中以及将rdb
文件传给从
并且从
使用rdb
文件恢复数据的过程中都没有新命令产生,那么主从
的数据就可以保持一致。
如果这期间产生了新的命令会不会导致主从
数据不一致?
根据官方文档 How Redis replication works中的介绍,我们可以知道期间产生的新命令会被主
缓存起来,在从
加载完rdb
文件数据之后,主
会将这期间缓存的命令发送给从
,从
在接受并执行完这些新命令后,就可以继续保持与主
数据的一致性
4.哨兵
redis主从
固然可以提升服务的高可靠性,却依然需要人为去进行监控
、选主
和通知
。看上去似乎不是很靠谱,因为我们不可能做到7 * 24小时盯着redis
服务,在其出问题后手动进行故障转移并通知客户端新主
的地址。
redis
中我们可以通过哨兵
机制来实现监控
、选主
、通知
流程自动化,一来可以减轻开发人员压力;二来可以降低人为误操作率;三来可以提升故障恢复时效性。
接下来会展示如何去搭建哨兵
集群以及如何进行选主
和通知
客户端新主地址
4.1 生成sentinel配置文件
➜ redis-stable cat sentinel.conf | grep -v "#" | grep -v "^$" > ./conf/sentinel-26379.conf
4.2 拷贝sentinel配置文件
➜ redis-stable sed 's/26379/26380/g' conf/sentinel-26379.conf > conf/sentinel-26380.conf
➜ redis-stable sed 's/26379/26381/g' conf/sentinel-26379.conf > conf/sentinel-26381.conf
4.3 启动sentinel
4.3.1 启动26379
通过日志可以看到sentinel
在启动的时候会生成一个唯一id,也就是Sentinel Id
,并且还打印出了redis主从
中从
的相关信息,可是根据sentinel
配置文件中的配置sentinel monitor mymaster 127.0.0.1 6379 2
,sentine
是不知道从
的相关信息,那么它是从哪得到这些信息的呢?
要想获得这些信息,sentinel
只需要给监控的主
发送info
命令即可
4.3.2 启动26380
启动sentinel26380
的时候通过日志可以看到其发现了sentinel26379
的存在
查看配置文件,可以看到配置文件中新增了从
和其它sentitnel
相关配置
4.3.3 启动26381
同理sentinel26381
启动的时候发现了sentinel26379
和sentinel26380
的存在并在配置文件中新增了相关配置
4.3 sentinel是如何发现彼此的存在?
在sentinel26381
启动完成后,sentinel
集群也就搭建完成了,在搭建的过程中也留下了一个疑问:sentinel
是如何发现彼此的存在?
根据官方文档High availability with Redis Sentinel中Sentinels and replicas auto discovery的介绍,可以了解到sentinel
是通过Pub/Sub
机制来发现彼此的存在,当一个sentinel
与主
连接后,可以在__sentinel__:hello
通道中发布其对应的ip
、port
和runid
,同时订阅__sentinel__:hello
通道,这样其它sentinel
发布消息的时候就可以得知对应的ip
和port
。
4.4 sentinel功能验证
sentinel集群
搭建完成后,我们需要停掉主
来验证其是否完成监控
、选主
和通知
任务。
首先停掉主6379
,分别观察3个sentinel
的变化
4.4.1 观察sentinel26379
4.4.2 观察sentinel26380
4.4.3 观察sentinel26381
通过观察日志可以看到主6379
下线后的一些变化:
- 每个
sentinel
都监控到主6379
下线,主观上认为其下线了(由于网络原因,可能存在误判),对应日志中的+sdown
部分 - 当半数以上的
sentinel
认为主6379
下线,则客观上认为其下线了(排除误判),对应日志中的+odown
部分 sentinel
集群通过投票方式选出sentinel26379
作为leader
来执行选主
操作sentinel26379
选择redis6380
作为新的主
,修改slave
配置
4.4.5 重启redis6379
sentinel
选出新主
后,是否将新主
地址通知到各个客户端。想要验证这个问题,只需要重启redis6379
即可。
redis6379
启动后成功连接上新主redis6380
并进行数据同步
4.5 从和重启后的旧主是如何知道新主的地址?
通过上面的实战演示,可以得知,主
宕机选出新主
后,剩余的从
会自动连接上新主
并进行数据同步,旧主
重启后也可以正常连接上新主
并进行数据同步。对于从
能连接上新主还可以理解,毕竟redis主从切换
的过程中,从
起码是运行状态,但是旧主
在redis主从切换
的过程中处于宕机状态,为什么在重启后还可以正常连接上新主
?
猜想应该是:在redis主从切换
完成后,sentinel leader
向从
发送了slaveof 新主host 新主port
命令,这样从
就可以与新主
连接并进行数据同步。针对旧主
在其重启后,sentinel
与之建立连接并发送slaveof 新主host 新主port
命令,这样旧主
也可以与新主
连接并进行数据同步。
4.6 客户端应用是如何知道新主的地址?
使用springboot
集成redis sentinel
,通常会在application.yml
中添加如下配置:
spring:
redis:
sentinel:
master: mymaster
nodes:
- 127.0.0.1:26379
- 127.0.0.1:26380
- 127.0.0.1:26381
配置文件中仅仅只配置了sentinel
节点信息,并没有配置redis
相关地址信息,客户端是如何知道redis
地址信息,当redis
发送主从切换,客户端又是如何知道新主
的地址信息?
根据官方文档High availability with Redis Sentinel中Obtaining the address of the current master的介绍,客户端可以通过SENTINEL get-master-addr-by-name mymaster
命令获取当前主
的地址信息
结合客户端源码
分析
我们可以得知客户端是通过向sentinel
发送get-master-addr-by-name mymaster
命令来获取redis主
的连接地址
4.7 总结
- sentinel之间通过
redis
的pub/sub
机制发现彼此的存在 redis主从
切换后,sentinel
向从
和旧主
发送slaveof host port
命令来连接新主
- 客户端通过向
sentinel
发送get-master-addr-by-name mymaster
命令来获取redis
主的连接地址
5.集群
redis主从
模式在大多数场景下都是比较适用,在面对需要持久化大量数据的场景下会变得比之前慢。慢的主要原因是:在进行rdb
持久化会fork
出一个子进程,fork
操作会阻塞主线程
并且阻塞时间与内存中的数据成正相关。因此,在面对大量数据需要持久化的场景时,可以考虑选择redis-cluster
来进行应对。
在使用集群时,需要考虑下面问题:
key/value
在集群中如何分布- 想对某个
key/value
进行操作时,该连接那个节点 - 集群节点数量发生变化,还用原来的地址操作
key/value
是否可行?
为了弄明白这些问题,按照惯例,先来搭建redis集群
5.1 集群搭建
5.1.1 创建conf目录
➜ redis-stable mkdir conf
5.1.2 创建配置文件
在conf
目录下创建redis-7001.conf
配置文件,内容如下:
port 7001
cluster-enabled yes
cluster-config-file nodes-7001.conf
cluster-node-timeout 5000
appendonly yes
5.1.3 拷贝集群配置文件
➜ redis-stable sed 's/7001/7002/g' conf/redis-7001.conf > conf/redis-7002.conf
sed 's/7001/7003/g' conf/redis-7001.conf > conf/redis-7003.conf
sed 's/7001/7004/g' conf/redis-7001.conf > conf/redis-7004.conf
sed 's/7001/7005/g' conf/redis-7001.conf > conf/redis-7005.conf
sed 's/7001/7006/g' conf/redis-7001.conf > conf/redis-7006.conf
5.1.4 分别启动redis服务
➜ redis-stable redis-server conf/redis-7001.conf
➜ redis-stable redis-server conf/redis-7002.conf
➜ redis-stable redis-server conf/redis-7003.conf
➜ redis-stable redis-server conf/redis-7004.conf
➜ redis-stable redis-server conf/redis-7005.conf
➜ redis-stable redis-server conf/redis-7006.conf
5.1.5 创建集群
redis-cli --cluster create 127.0.0.1:7001 127.0.0.1:7002 \
127.0.0.1:7003 127.0.0.1:7004 127.0.0.1:7005 127.0.0.1:7006 \
--cluster-replicas 1
输入完命令后,可以看到如下分配方案,针对分配方案输入yes
即可
为了方便理解,可以看下图
到此,redis集群
就搭建完成了
5.2 客户端连接集群
5.2.1 缓存slot与实例映射关系
搭建完redis
集群后,可以通过客户端
来连接集群
,为了能更清晰了解客户端
连接集群
都做了些什么,这里选择使用spring boot
应用作为客户端
。通过源码调试,可以看到如下结果:
客户端应用在启动的时候会向redis集群实例
发送cluster slots
命令获取slot
分配信息,然后在本地缓存slot
与redis实例
的映射关系
5.2.2 根据key计算对应slot
客户端在对key/value
进行操作时,会对key
进行CRC
计算并和cluster slot
数量进行与
运算得到最终的slot
值
5.2.3 根据slot获取实例
5.2.1
中提到客户端会缓存slot
与实例
映射关系,5.2.2
根据计算得到对应的slot
,若想与redis实例
交互,此时只需要从映射缓存中获取对应的实例
既可
5.2.4 刷新slot与实例映射缓存关系
集群
环境往往会伴随着实例
的上线与下线,不管是上线还是下线,都会使得slot
重新分配,原本在某个实例上的slot
会被分配到新的实例上,针对这种情况,客户端如果还是请求旧实例会发生什么?客户端又如何知道slot
对应的实例发生了变化?
为了演示该场景,会再运行两台redis
实例,分别为:redis7007
和redis7008
,redis
实例启动完成之后,通过redis-cli --cluster add-node 127.0.0.1:7007 127.0.0.1:7001
命令将redis7007
以master
的方式加入到已存在的集群
中,再通过redis-cli --cluster add-node 127.0.0.1:7008 127.0.0.1:7000 --cluster-slave
命令将redis7008
以slave
的方式加入到已存在的集群
中。
redis
实例加入集群后,可以通过redis-cli --cluster check 127.0.0.1:7001
命令来查看集群的当前情况
根据输出内容可以看到新加入的redis7007
实例还没有分配slot
,可以通过redis-cli --cluster reshard 127.0.0.1:7001
命令来分配slot
,分配成功之后原本在redis7001
、redis7002
、redis7003
实例上的slot
会移动到redis7007
上。
找一个原本在redis7001
实例上后来被移动到redis7007
实例上的key,执行get
命令
执行完get
命令后,可以看到服务端返回了错误,并告诉我们当前key已经移动到redis7007
实例上,这是redis-cli
客户端执行后的结果,应用客户端会如何处理呢?
通过源码分析,可以得知当出现JedisMovedDataException
异常后,应用客户端会重新发送cluster slots
命令来刷新本地缓存
5.3 总结
- 集群部署后,
16384
个slot
会散落在各个redis master
实例上 - 客户端通过发送
cluster slots
命令在本地缓存slot
与redis
实例的映射关系 - 对
key/value
进行操作时,key
通过CRC
计算并和slot
数量 - 1进行与
运算后得到对应slot
,得到slot
,又知道slot
与redis
实例的映射关系,就可以对redis
实例进行访问 slot
迁移后,客户端还用原来的实例执行命令,会出现异常,针对异常客户端会通过发送cluster slots
命令来刷新本地slot
与redis
实例的映射关系