基于 Redis 实现分布式缓存

一、单节点 Redis 的问题

1.1 存在的问题

1、 数据丢失问题:Redis是内存存储,服务重启可能会丢失数据。

2、 并发能力问题:单节点 Redis 并发能力虽然不错,但也无法满足如 618这样的高并发场景。

3、 故障恢复问题:如果 Redis宕机,则服务不可用,需要一种自动的故障恢复手段。

4、 存储能力问题:Redis基于内存,单节点能存储的数据量难以满足海量数据需求

1.2 解决方案

二、Redis 持久化

2.1 RDB 持久化

2.1.1 简介

RDB 全称Redis Database Backup fileRedis 数据备份文件),也被叫做 Redis 数据快照。简单来说就是把内存中的所有数据都记录到磁盘中。当 Redis 实例故障重启后,从磁盘读取快照文件,恢复数据。 快照文件称为 RDB文件,默认是保存在当前运行目录。

执行 save命令会阻塞其他的命令,如下图:

执行 bgsave命令时则会开启子进程执行备份任务,如下图:

注意:Redis 正常停机时会执行一次 RDB,突然断电啥的则不会执行。

2.1.2 触发机制

Redis 内部有触发 RDB 的机制,可以在 redis.conf文件中找到,格式如下:

RDB 的其它配置也可以在redis.conf文件中设置,如下图:

2.1.3 底层实现

bgsave 开始时会 fork 主进程得到子进程,子进程共享主进程的内存数据。完成 fork 后读取内存数据并写入 RDB文件。

如下图,首先数据是存在在物理内存上的,而 redis 的主进程如果想要实现对 redis 数据的读写就需要在物理内存上进行读写,而在 linux环境下,所有的进程都无法直接操作物理内存,而是由操作系统给每个进程分配一个虚拟内存,即主进程只能操作虚拟内存,而操作系统会维护一个虚拟内存和物理内存之间的映射关系表,又称为页表。所以主进程操作虚拟内存,而虚拟内存基于页表的映射关系操作我们的物理内存,这样就实现了对物理内存的读写了。

而当我们执行 fork 操作时,会创建一个子进程,fork的过程中会把页表做一份拷贝,也就是把映射关系拷贝给子进程,子进程有了和主进程一样的映射关系,当子进程在操作自己的虚拟内存时,一定会操作和主进程一样的物理内存区域,就实现了子进程和主进程之间的内存共享了。

此时,当子进程执行备份操作时,就可以放心的读取自己内存的数据,其实读取的就是主进程的数据。之后再把数据写入到磁盘中新的文件中去。到这为止,异步持久化就实现了。

如果子进程在写 RDB 的过程中,主进程同时在修改数据,此时读与写之间就会有冲突,甚至出现脏数据。为了避免这种事情的发生,fork 的底层采用的是copy-on-write 技术,当主进程要写的时候做一次拷贝,现在的内存是共享的,没有做拷贝的。此时 fork 会把共享内存标记成 read-only 只读模式,任何一个进程都只能来读数据而不能写。假设此时用户真的来写数据了。此时先拷贝一份数据,再对拷贝的数据完成一份写操作。一旦完成了拷贝,主进程再做读操作的时候也要从拷贝的数据里面读取了,也就是说主进程的页面的映射关系发生了变化。这个就是 copy-on-write技术。

但是在某些极端的情况下,假设 RDB 写的过程比较慢,耗时较长。就在它写的过程中,不断的有新的请求进来,不断的修改共享的数据,导致所有的数据都被修改了一遍。就会导致所有的数据都需要拷贝一份新的,也就意味着 redis 对内存的占用翻倍了,这是极端的情况,几乎不可能发生,但是理论上有可能。因此 redis 都需要预留一部分内存空间。假设服务器内存为32G ,不能把 32G 都交给 redis,还需要预留一部分。

2.1.4 优缺点

RDB 方式 bgsave的基本流程

1、fork主进程得到一个子进程,共享内存空间

2、 子进程读取内存数据并写入新的 RDB文件

3、 用新 RDB 文件替换旧的 RDB文件。

RDB 会在什么时候执行?save 60 1000代表什么含义?

**1、**默认是服务停止时。

2、 代表 60 秒内至少执行 1000 次修改则触发 RDB

RDB的缺点?

1、RDB 执行间隔时间长,两次 RDB之间写入数据有丢失的风险

2、fork 子进程、压缩、写出 RDB文件都比较耗时

2.2 AOF 持久化

2.2.1 简介

AOF 全称为Append Only File (追加文件)。Redis 处理的每一个写命令都会记录在 AOF 文件,可以看做是命令日志文件。其中**$3** 表示下面的字母的个数。

2.2.2 触发机制

AOF 默认是关闭的,需要修改 redis.conf 配置文件来开启 AOF

AOF 的命令记录的频率也可以通过redis.conf文件来配:

2.2.3 底层机制

因为是记录命令,AOF 文件会比 RDB 文件大的多。而且 AOF 会记录对同一个 key 的多次写操作,但只有最后一次写操作才有意义。可以通过执行 bgrewriteaof 命令,可以让 AOF文件执行重写功能,用最少的命令达到相同效果。

Redis 也会在触发阈值时自动去重写 AOF 文件。阈值也可以在redis.conf中配置:

2.3 RDB 和 AOF 对比

RDBAOF各有自己的优缺点,如果对数据安全性要求较高,在实际开发中往往会结合两者来使用。

三、Redis 主从

3.1 搭建主从架构

3.1.1 简介

单节点 Redis 的并发能力是有上限的,要进一步提高 Redis的并发能力,就需要搭建主从集群,实现读写分离。

3.1.2 集群架构

我们搭建的主从集群结构如图:

共包含三个节点,一个主节点,两个从节点。这里我们会在同一台虚拟机中开启 3redis实例,模拟主从集群,信息如下:

|-----------------|----------|--------|
| IP | PORT | 角色 |
| 192.168.229.175 | 7001 | master |
| 192.168.229.175 | 7002 | slave |
| 192.168.229.175 | 7003 | slave |

3.1.3 准备实例和配置

首先将 Redis 安装包上传到虚拟机的**/tmp**目录下,然后执行解压缩命令:

bash 复制代码
tar -xzf redis-6.2.4.tar.gz

然后进入redis目录:

bash 复制代码
cd redis-6.2.4

运行编译命令:

bash 复制代码
make && make install

如果没有出错,应该就安装成功了。

要在同一台虚拟机开启 3 个实例,必须准备三份不同的配置文件和目录,配置文件所在目录也就是工作目录。我们创建三个文件夹,名字分别叫 700170027003,如下图:

bash 复制代码
# 进入/tmp目录
cd /tmp
# 创建目录
mkdir 7001 7002 7003

然后将redis-6.2.4/redis.conf 文件拷贝到三个目录中(在 /tmp目录执行下列命令):

bash 复制代码
# 方式一:逐个拷贝
cp redis-6.2.4/redis.conf 7001
cp redis-6.2.4/redis.conf 7002
cp redis-6.2.4/redis.conf 7003

# 方式二:管道组合命令,一键拷贝
echo 7001 7002 7003 | xargs -t -n 1 cp redis-6.2.4/redis.conf

修改每个文件夹内的配置文件,将端口分别修改为 700170027003 ,将 rdb 文件保存位置都修改为自己所在目录(在**/tmp**目录执行下列命令):

bash 复制代码
sed -i -e 's/6379/7001/g' -e 's/dir .\//dir \/tmp\/7001\//g' 7001/redis.conf
sed -i -e 's/6379/7002/g' -e 's/dir .\//dir \/tmp\/7002\//g' 7002/redis.conf
sed -i -e 's/6379/7003/g' -e 's/dir .\//dir \/tmp\/7003\//g' 7003/redis.conf

虚拟机本身有多个 IP ,为了避免将来混乱,我们需要在redis.conf 文件中指定每一个实例的绑定ip 信息,每个目录都要改,我们一键完成修改(在 /tmp 目录执行下列命令):

bash 复制代码
# 逐一执行
sed -i '1a replica-announce-ip 192.168.229.175' 7001/redis.conf
sed -i '1a replica-announce-ip 192.168.229.175' 7002/redis.conf
sed -i '1a replica-announce-ip 192.168.229.175' 7003/redis.conf

# 或者一键修改
printf '%s\n' 7001 7002 7003 | xargs -I{} -t sed -i '1a replica-announce-ip 192.168.229.175' {}/redis.conf

3.1.4 启动

为了方便查看日志,我们打开3ssh 窗口,分别启动 3redis实例,启动命令:

bash 复制代码
# 第1个
redis-server 7001/redis.conf
# 第2个
redis-server 7002/redis.conf
# 第3个
redis-server 7003/redis.conf

如果要一键停止,可以运行下面命令:

bash 复制代码
printf '%s\n' 7001 7002 7003 | xargs -I{} -t redis-cli -p {} shutdown

3.1.5 开启主从关系

现在三个实例还没有任何关系,要配置主从可以使用 replicaof 或者 slaveof5.0以前)命令。有临时和永久两种模式:

如果想要永久生效,就需要修改配置文件,在 redis.conf中添加一行配置,内容如下:

bash 复制代码
# 在从节点上配置主节点的 ip 和端口号
slaveof <masterip> <masterport>

如果想要重启后失效,则使用 redis-cli 客户端连接到 redis 服务,执行 slaveof命令,如下:

bash 复制代码
# 在从节点上配置主节点的 ip 和端口号
slaveof <masterip> <masterport>

注意:在5.0 以后新增命令 replicaof ,与 salveof效果一样。这里我们为了演示方便,使用方式二。

通过 redis-cli 命令连接 7002,执行下面命令:

bash 复制代码
# 连接 7002
redis-cli -p 7002
# 执行slaveof
slaveof 192.168.150.101 7001

通过redis-cli 命令连接 7003,执行下面命令:

bash 复制代码
# 连接 7003
redis-cli -p 7003
# 执行slaveof
slaveof 192.168.150.101 7001

然后连接 7001节点,查看集群状态:

bash 复制代码
# 连接 7001
redis-cli -p 7001
# 查看状态
info replication

结果如下:

3.1.6 测试

1、 利用redis-cli 连接 7001 ,执行 set num 123

2、 利用 redis-cli 连接 7002 ,执行 get num ,再执行 set num 666

3、 利用redis-cli 连接 7003 ,执行 get num ,再执行 set num 888

可以发现,只有在 7001 这个 master 节点上可以执行写操作,70027003 这两个 slave节点只能执行读操作。

3.1.7 小结

假设有 AB 两个 Redis 实例,如何让B 作为Aslave节点?

B 节点执行命令:slaveof AIP Aport即可。

3.2 主从数据同步原理

当我们在主节点执行写操作的时候,从节点也可以看到写入的数据,那么这种数据同步是如何实现的呢?接下来来研究下。

3.2.1 全量同步

redis 的主从第一次同步是全量同步,以下图为例,当从节点和主节点第一次建立连接时,需要执行 replicaof 命令并指定主节点的 ip 和端口,这个就是和主节点建立连接的过程。建立连接之后,slave 就向 master 发送请求数据同步的请求,master 会对请求进行判断看其是否为第一次同步,如果是第一次来则先向 slave 节点发送 master 的数据版本信息,slave先保存下来,此时完成了第一阶段,如下图:

此时 master 节点执行 bgsave 命令,生成 RDB 文件,将此文件发送给 slave 节点,此时 slave 节点清空本地数据,加载新的 RDB 文件,此时就可以确保 masterslave 节点的数据基本一致。在生成 RDB 的过程中,master 将新写入的命令记录到 repl_baklog (内存缓冲区)文件中,此时第二阶段完成,如下图:

接下来就是将缓冲区里面的命令发送给 slave 节点,slave拿到命令后执行即可,此时第三阶段完成,如下图:

因为这种方式有个 RDB 的过程,所以叫全量同步,比较耗费性能,只在第一次建立连接的时候会同步。那么我们的 master 是如何知道 slave是第一次来呢?这里有两个重要的概念。

Replication Id :简称 replid ,是数据集的标记,id 一致则说明是同一数据集。每一个 master 都有唯一的 replidslave 则会继承 master 节点的 replid

offset :偏移量,随着记录在 repl_baklog 中的数据增多而逐渐增大。slave 完成同步时也会记录当前同步的 offset 。如果 slaveoffset 小于 masteroffset ,说明 slave 数据落后于 master,需要更新。

因此 slave 做数据同步,必须向 master 声明自己的replication idoffsetmaster 才可以判断到底需要同步哪些数据。

此时的全量同步第一阶段就变成了下面的样子,slave 节点发送同步请求时就需要携带 replidoffset ,每一个节点在成为 slave 之前都是 master ,所以他一定有自己的 replid ,带着自己的来,然后和 master 一比,判断是否一致,只要不一致一定是第一次,此时就需要返回主节点的 replidoffset ,此时 slave保存下来即可。

简述全量同步的流程?

1、slave节点请求增量同步

2、master 节点判断 replid,发现不一致,拒绝增量同步

3、master 将完整内存数据生成 RDB ,发送 RDBslave

4、slave 清空本地数据,加载 masterRDB

5、masterRDB 期间的命令记录在 repl_baklog ,并持续将 log 中的命令发送给 slave

6、slave 执行接收到的命令,保持与 master之间的同步。

3.2.2 增量同步

主从第一次同步是全量同步,但如果 slave重启后同步,则执行增量同步。

slave 节点重启之后,发送请求同步的命令,mater 判断请求的 replid 和自己一致,则回复给 slave 节点 continue ,此时 masterrepl_baklog 中获取 offset 后的数据,发送给 slaveslave执行相关的命令即可。

repl_baklog 本质是个数组,他比较特殊,大小固定,当他存储数据满了之后会覆盖以前的数据继续存储,只要 salvemaster之间的差距不超过这个数组的存储上限,那么永远可以在这个环里面找到需要的数据,如果超过了存储上限,就只能做全量同步。

repl_baklog 大小有上限,写满后会覆盖最早的数据。如果 slave 断开时间过久,导致尚未备份的数据被覆盖,则无法基于 log做增量同步,只能再次全量同步。

3.2.3 集群优化

可以从以下几个方面来优化 Redis主从就集群:

1、master 中配置repl-diskless-sync yes 启用无磁盘复制,避免全量同步时的磁盘 IO

2、Redis 单节点上的内存占用不要太大,减少 RDB 导致的过多磁盘 IO

3、 适当提高 repl_baklog 的大小,发现 slave宕机时尽快实现故障恢复,尽可能避免全量同步

4、 限制一个 master 上的 slave 节点数量,如果实在是太多 slave ,则可以采用主-从-从链式结构,减少 master压力,如下图:

3.2.4 小结

简述全量同步和增量同步区别?

1、 全量同步:master 将完整内存数据生成 RDB ,发送 RDBslave 。后续命令则记录在 repl_baklog ,逐个发送给 slave

2、 增量同步:slave 提交自己的 offsetmastermaster 获取 repl_baklog 中从 offset 之后的命令给 slave

什么时候执行全量同步?

1、slave 节点第一次连接 master节点时

2、slave 节点断开时间太久,repl_baklog 中的 offset已经被覆盖时

什么时候执行增量同步?

1、slave 节点断开又恢复,并且在 repl_baklog 中能找到 offset

四、Redis 哨兵

4.1 哨兵的作用和原理

4.1.1 作用

Redis 提供了哨兵(Sentinel)机制来实现主从集群的自动故障恢复。哨兵的结构和作用如下:

监控:Sentinel 会不断检查您的 masterslave是否按预期工作。

自动故障恢复:如果 master 故障,Sentinel 会将一个 slave 提升为 master 。当故障实例恢复后也以新的 master为主。

通知:Sentinel 充当 Redis 客户端的服务发现来源,当集群发生故障转移时,会将最新信息推送给 Redis 的客户端(java 这边不知道 master 节点挂了,sentinel 会通知 java这边)。

4.1.2 原理

Sentinel 基于心跳机制监测服务状态,每隔 1 秒向集群的每个实例发送 ping命令:

主观下线:如果某 sentinel节点发现某实例未在规定时间响应,则认为该实例主观下线。

客观下线:若超过指定数量(quorum )的 sentinel 都认为该实例主观下线,则该实例客观下线。quorum 值最好超过 Sentinel 实例数量的一半( 3 台服务器则设置为 2 )。

4.1.3 选举机制

一旦发现 master 故障,sentinel 需要在 salve 中选择一个作为新的 master,选择依据是这样的:

1、 首先会判断 slave 节点与 master 节点断开时间长短,如果超过指定值(down-after-milliseconds * 10 )则会排除该 slave节点。

2、 然后判断 slave 节点的 slave-priority 值,越小优先级越高,如果是0 则永不参与选举。

3、 如果 slave-prority 一样,则判断 slave 节点的 offset值,越大说明数据越新,优先级越高。

4、 最后是判断 slave 节点的运行id大小,越小优先级越高。

4.1.4 故障转移

当选中了其中一个 slave 为新的 master 后(例如 slave1),故障的转移的步骤如下:

1、sentinel 给备选的 slave1 节点发送 slaveof no one 命令,让该节点成为 master

2、sentinel 给所有其它 slave 发送slaveof 192.168.150.101 7002 命令,让这些 slave 成为新 master 的从节点,开始从新的 master 上同步数据。

3、 最后,sentinel 将故障节点标记为 slave ,当故障节点恢复后会自动成为新的 masterslave节点。

4.2 搭建哨兵集群

4.2.1 集群架构

这里我们搭建一个三节点形成的 Sentinel 集群,来监管之前的 Redis主从集群。如图:

三个 sentinel实例信息如下:

|--------|-----------------|----------|
| 节点 | IP | PORT |
| s1 | 192.168.229.175 | 27001 |
| s2 | 192.168.229.175 | 27002 |
| s3 | 192.168.229.175 | 27003 |

4.2.2 准备实例和配置

要在同一台虚拟机开启3 个实例,必须准备三份不同的配置文件和目录,配置文件所在目录也就是工作目录。我们创建三个文件夹,名字分别叫 s1s2s3

bash 复制代码
# 进入/tmp目录
cd /tmp
# 创建目录
mkdir s1 s2 s3

然后我们在 s1 目录创建一个 sentinel.conf文件,添加下面的内容:

bash 复制代码
# 是当前 sentinel 实例的端口
port 27001
sentinel announce-ip 192.168.229.176

# 指定主节点信息
# mymaster:主节点名称,自定义,任意写
# 192.168.150.101 7001:主节点的ip和端口
# 2:选举master时的quorum值
sentinel monitor mymaster 192.168.229.176 7001 2
sentinel down-after-milliseconds mymaster 5000
sentinel failover-timeout mymaster 60000
dir "/tmp/s1"

然后将 s1/sentinel.conf 文件拷贝到 s2s3 两个目录中(在 /tmp 目录执行下列命令):

bash 复制代码
# 方式一:逐个拷贝
cp s1/sentinel.conf s2
cp s1/sentinel.conf s3

# 方式二:管道组合命令,一键拷贝
echo s2 s3 | xargs -t -n 1 cp s1/sentinel.conf

修改s2、s3 两个文件夹内的配置文件,将端口分别修改为 2700227003

bash 复制代码
sed -i -e 's/27001/27002/g' -e 's/s1/s2/g' s2/sentinel.conf
sed -i -e 's/27001/27003/g' -e 's/s1/s3/g' s3/sentinel.conf

4.2.3 启动

为了方便查看日志,我们打开3ssh 窗口,分别启动 3redis实例,启动命令:

bash 复制代码
# 第1个
redis-sentinel s1/sentinel.conf
# 第2个
redis-sentinel s2/sentinel.conf
# 第3个
redis-sentinel s3/sentinel.conf

启动后:

4.2.4 测试

尝试让 master 节点 7001 宕机,查看 sentinel日志:

查看 7003的日志:

查看 7002的日志:

4.3 RedisTemplate 的哨兵模式

4.3.1 简介

Sentinel 集群监管下的 Redis 主从集群,其节点会因为自动故障转移而发生变化,Redis 的客户端必须感知这种变化,及时更新连接信息。SpringRedisTemplate 底层利用 lettuce实现了节点的感知和自动切换。

4.3.2 引入步骤

1、pom 文件中引入 redisstarter依赖:

XML 复制代码
<dependency>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

2、 然后在配置文件application.yml 中指定 sentinel相关信息:

bash 复制代码
spring:
  redis:
    sentinel:
      master: mymaster   # 指定master名称
      nodes:             # 指定redis-sentinel集群信息
          - 192.168.229.175:27001
          - 192.168.229.175:27002
          - 192.168.229.175:27003

**3、**配置主从读写分离

java 复制代码
@Bean
public LettuceClientConfigurationBuilderCustomizer configurationBuilderCustomizer(){
   return configBuilder -> configBuilder.readFrom(ReadFrom.REPLICA_PREFERRED);
}

这里的 ReadFrom 是配置 Redis的读取策略,是一个枚举,包括下面选择:

MASTER:从主节点读取

MASTER_PREFERRED :优先从 master 节点读取,master 不可用才读取 replica

REPLICA :从 slavereplica)节点读取

REPLICA _PREFERRED :优先从 slavereplica )节点读取,所有的 slave 都不可用才读取 master

五、Redis 分片集群

主从集群可以应对高并发读的问题,但是 redis 主从之间需要做同步,为了提高主从同步之间的性能,单节点 redis 的内存不能设置太高,如果过大那么在做 redis 的持久化或全量同步的时候会导致大量的 IO,性能会有一定的下降。

如果说单节点 redis 的内存降低了,那么如果我有海量的数据需要存储该怎么办呢?现在解决了高并发读的问题,那么高并发写的问题又该如何解决呢?此时就需要 redis的分片集群来解决了。

主从和哨兵可以解决高可用、高并发读的问题。但是依然有两个问题没有解决:海量数据存储问题和高并发写的问题。

使用分片集群可以解决上述问题,分片集群特征:

1、 集群中有多个 master ,每个 master保存不同数据

2、 每个 master 都可以有多个 slave节点

3、master 之间通过 ping监测彼此健康状态

**4、**客户端请求可以访问集群任意节点,最终都会被转发到正确节点。

5.1 搭建分片集群

5.1.1 集群架构

分片集群需要的节点数量较多,这里我们搭建一个最小的分片集群,包含 3master 节点,每个 master 包含一个 slave节点,结构如下:

这里我们会在同一台虚拟机中开启6redis实例,模拟分片集群,信息如下:

|-----------------|----------|--------|
| IP | PORT | 角色 |
| 192.168.229.176 | 7001 | master |
| 192.168.229.176 | 7002 | master |
| 192.168.229.176 | 7003 | master |
| 192.168.229.176 | 8001 | slave |
| 192.168.229.176 | 8002 | slave |
| 192.168.229.176 | 8003 | slave |

5.1.2 准备实例和配置

删除之前的 700170027003 这几个目录,重新创建出 700170027003800180028003目录:

bash 复制代码
# 进入/tmp目录
cd /tmp
# 删除旧的,避免配置干扰
rm -rf 7001 7002 7003
# 创建目录
mkdir 7001 7002 7003 8001 8002 8003

在**/tmp** 目录下准备一个新的redis.conf文件,内容如下:

bash 复制代码
port 6379
# 开启集群功能
cluster-enabled yes
# 集群的配置文件名称,不需要我们创建,由redis自己维护
cluster-config-file /tmp/6379/nodes.conf
# 节点心跳失败的超时时间
cluster-node-timeout 5000
# 持久化文件存放目录
dir /tmp/6379
# 绑定地址
bind 0.0.0.0
# 让redis后台运行
daemonize yes
# 注册的实例ip
replica-announce-ip 192.168.229.176
# 保护模式
protected-mode no
# 数据库数量
databases 1
# 日志
logfile /tmp/6379/run.log

将这个文件拷贝到每个目录下:

bash 复制代码
# 进入/tmp目录
cd /tmp
# 执行拷贝
echo 7001 7002 7003 8001 8002 8003 | xargs -t -n 1 cp redis.conf

修改每个目录下的 redis.conf ,将其中的 6379修改为与所在目录一致:

bash 复制代码
# 进入/tmp目录
cd /tmp
# 修改配置文件
printf '%s\n' 7001 7002 7003 8001 8002 8003 | xargs -I{} -t sed -i 's/6379/{}/g' {}/redis.conf

5.1.3 启动

因为已经配置了后台启动模式,所以可以直接启动服务:

bash 复制代码
# 进入/tmp目录
cd /tmp
# 一键启动所有服务
printf '%s\n' 7001 7002 7003 8001 8002 8003 | xargs -I{} -t redis-server {}/redis.conf

查看集群状态,发现服务都已经正常启动,如下图:

如果要关闭所有进程,可以执行命令:

bash 复制代码
printf '%s\n' 7001 7002 7003 8001 8002 8003 | xargs -I{} -t redis-cli -p {} shutdown

5.1.4 创建集群

虽然服务启动了,但是目前每个服务之间都是独立的,没有任何关联。我们需要执行命令来创建集群,命令如下:

bash 复制代码
# redis-cli --cluste:代表集群操作命令
# create:代表是创建集群
# --replicas 1 或者 --cluster-replicas 1 :指定集群中每个 master 的副本个数为1,此时节点总数 ÷ (replicas + 1)` 得到的就是 master 的数量。
# 因此节点列表中的前n个就是master,其它节点都是slave节点,随机分配到不同master
redis-cli --cluster create --cluster-replicas 1 192.168.229.176:7001 192.168.229.176:7002 192.168.229.176:7003 192.168.229.176:8001 192.168.229.176:8002 192.168.229.176:8003

运行后的样子:

通过命令可以查看集群状态:

bash 复制代码
redis-cli -p 7001 cluster nodes

5.1.5 测试

尝试连接 7001节点,存储一个数据:

bash 复制代码
# 连接
# 集群操作时,需要给 redis-cli 加上 -c 参数才可以
redis-cli -c -p 7001
# 存储数据
set num 123
# 读取数据
get num
# 再次存储
set a 1

5.2 散列插槽

5.2.1 简介

Redis 会把每一个 master 节点映射到 0~1638316384 个插槽(hash slot)上,查看集群信息时就能看到:

假设此时要存储一个数据到集群上,比如 set num 123 ,那这个 num 应该存储到哪一个 master 上呢?既然每一个 master都可以存储数据,那应该存到哪里呢?如果随便挑一个去存,那么以后取的时候又该如何确定去哪里取呢?插槽就是来解决这个问题的。

redis 中数据 key 不是与节点绑定,而是与插槽绑定。redis 会根据 key的有效部分计算插槽值,分两种情况:

1、key 中包含 "{}",且 "{}" 中至少包含 1个字符,"{}" 中的部分是有效部分

2、key中不包含 "{}",整个 key 都是有效部分。

例如 keynum ,那么就根据 num 计算,如果是 {itcast}num ,则根据 itcast 计算。计算方式是利用 CRC16 算法得到一个 hash 值,然后对 16384 取余,得到的结果就是 slot值。

为什么我们的 key 要和插槽绑定而不和节点绑定?是因为 redis的主节点是可能出现宕机的情况的,也有可能出现集群增加节点或删除节点的情况,如果节点删除了,数据可能就丢了。如果和插槽绑定,当节点删除时,我们可以将对应的插槽转移到活着的节点上。

5.2.2 小结

Redis 如何判断某个 key应该在哪个实例?

1、16384个插槽分配到不同的实例

2、 根据 key 的有效部分计算哈希值,对 16384取余

**3、**余数作为插槽,寻找插槽所在实例即可

如何将同一类数据固定的保存在同一个 Redis实例?

1、 这一类数据使用相同的有效部分,例如 key 都以**{typeId}**为前缀

5.3 集群伸缩

5.3.1 简介

作为一个分片集群,有一个非常重要的功能就是集群伸缩,也就是说这个集群必须可以动态的增加或者删除节点。

redis-cli --cluster提供了很多操作集群的命令,可以通过下面方式查看:

比如添加节点的命令,后面需要添加新的节点 ip 和端口号,后面还要添加一个已经存在的集群的ip 和端口号,因为向集群中添加节点需要通知集群中每一个角色。后面还可以加一些选项,如果不加后面的选项,新增的节点默认就是 master节点。

5.3.2 添加节点

需求描述:向集群中添加一个新的 master 节点,并向其中存储 num = 10,详细要求如下:

1、 启动一个新的 redis 实例,端口为 7004

2、 添加 7004 到之前的集群,并作为一个 master节点

3、7004 节点分配插槽,使得 num 这个 key 可以存储到 7004实例

首先启动一个新的 redis实例,如下:

bash 复制代码
[root@localhost tmp]# mkdir 7004
[root@localhost tmp]# cp redis.conf 7004
[root@localhost tmp]# sed -i s/6379/7004/g 7004/redis.conf 
[root@localhost tmp]# 
[root@localhost tmp]# redis-server 7004/redis.conf 
[root@localhost tmp]# ps -ef | grep redis
root      86414      1  0 01:00 ?        00:00:09 redis-server 0.0.0.0:7001 [cluster]
root      86416      1  0 01:00 ?        00:00:08 redis-server 0.0.0.0:7002 [cluster]
root      86422      1  0 01:00 ?        00:00:08 redis-server 0.0.0.0:7003 [cluster]
root      86428      1  0 01:00 ?        00:00:08 redis-server 0.0.0.0:8001 [cluster]
root      86434      1  0 01:00 ?        00:00:08 redis-server 0.0.0.0:8002 [cluster]
root      86440      1  0 01:00 ?        00:00:08 redis-server 0.0.0.0:8003 [cluster]
root      87356      1  0 02:00 ?        00:00:00 redis-server 0.0.0.0:7004 [cluster]
root      87369  84683  0 02:00 pts/1    00:00:00 grep --color=auto redis
[root@localhost tmp]# 

执行下面的命令,将 7004加入到集群之中

bash 复制代码
redis-cli --cluster add-node 192.168.229.176:7004 192.168.229.176:7001

但是,此时的 7004上是没有任何的插槽的,如下图:

因为我们要set num7004 上面,此时就需要将 7001 上面的插槽分配到 7004 上一部分,我们先看下 numslot值是多少,如下图:

此时可以看到 num 的插槽值为 2765 ,也就是说只需要把 2765 这个插槽分配到 7004上面即可,所以此时就需要做插槽分配,执行下面的命令:

bash 复制代码
redis-cli --cluster reshard 192.168.229.176:7001

此时输入下面的命令,再次查看节点的信息,可以看到 7004上面也有插槽信息了

bash 复制代码
​redis-cli -p 7001 cluster nodes

此时再次查看 num 的信息,可以看到,存储到了 7004节点上了

5.3.3 删除节点

接下来我们将刚才添加的 7004 节点删除掉,执行下面的命令执行删除操作

没有删除成功,首先我们先转移 7004的插槽,命令如下:

转移完成后,再次尝试删除节点信息,可以到了,删除成功了。

5.4 故障转移

5.4.1 自动故障转移

当集群有一个 master宕机会发生什么呢?首先是该实例与其它实例失去连接,然后是疑似宕机,如下图:

最后是确定下线,自动提升一个 slave 为新的 master

这种方式成为自动故障转移,一台 master 意外宕机了,自动的去选择一台新的主机

5.4.2 手动故障转移

有时候我们还需要做手动的故障转移,比如说设备升级等。

利用cluster failover 命令可以手动让集群中的某个 master 宕机,切换到执行 cluster failover 命令的这个 slave节点,实现无感知的数据迁移。其流程如下:

手动的 Failover支持三种不同模式:

1、 缺省:默认的流程,如图 1~6

2、force :省略了对 offset的一致性校验

3、takeover :直接执行第5 歩,忽略数据一致性、忽略 master 状态和其它 master 的意见

5.5 RedisTemplate 访问分片集群

RedisTemplate 底层同样基于 lettuce实现了分片集群的支持,而使用的步骤与哨兵模式基本一致:

1、pom 文件中引入 redisstarter依赖:

XML 复制代码
<dependency>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

2、 然后在配置文件application.yml 中指定 cluster相关信息:

bash 复制代码
spring:
  redis:
    cluster:
      nodes:             # 指定分片集群的每一个节点信息
          - 192.168.229.176:7001
          - 192.168.229.176:7002
          - 192.168.229.176:7003
		  - 192.168.229.176:8001
		  - 192.168.229.176:8002
		  - 192.168.229.176:8003

**3、**配置读写分离

java 复制代码
@Bean
public LettuceClientConfigurationBuilderCustomizer configurationBuilderCustomizer(){
   return configBuilder -> configBuilder.readFrom(ReadFrom.REPLICA_PREFERRED);
}

这里的 ReadFrom 是配置 Redis的读取策略,是一个枚举,包括下面选择:

MASTER:从主节点读取

MASTER_PREFERRED :优先从 master 节点读取,master 不可用才读取 replica

REPLICA :从 slavereplica)节点读取

REPLICA _PREFERRED :优先从 slavereplica )节点读取,所有的 slave 都不可用才读取 master

相关推荐
CoderIsArt31 分钟前
Redis的三种模式:主从模式,哨兵与集群模式
数据库·redis·缓存
ketil275 小时前
Redis - String 字符串
数据库·redis·缓存
王佑辉7 小时前
【redis】延迟双删策略
redis
生命几十年3万天7 小时前
redis时间优化
数据库·redis·缓存
Shenqi Lotus8 小时前
Redis-“自动分片、一定程度的高可用性”(sharding水平拆分、failover故障转移)特性(Sentinel、Cluster)
redis·sentinel·cluster·failover·sharding·自动分片·水平拆分
YMY哈12 小时前
Redis常见面试题(二)
redis
元气满满的热码式12 小时前
Redis常用的五大数据类型(列表List,集合set)
数据库·redis·缓存
学习路漫长14 小时前
Redis 的使⽤和原理
redis·缓存
-273K14 小时前
33.Redis多线程
数据库·redis·缓存
KKTT0115 小时前
Redis数据库测试和缓存穿透、雪崩、击穿
数据库·redis·缓存