3

Redis持久化

什么是持久化

Redis所有数据保存在内存中,对数据的更新将异步地保存到磁盘上。

持久化的方式

快照

  • MySQL Dump
  • Redis RDB

日志

  • MySQL binlog
  • Redis AOF

RDB

什么是RDB

RDB持久化是指在指定的时间间隔内将内存中的数据集快照写入磁盘。

也是默认的持久化方式,这种方式是就是将内存中数据以快照的方式写入到二进制文件中,默认的文件名为dump.rdb。

在 Redis 运行时, RDB 程序将当前内存中的数据库快照保存到磁盘文件中, 在 Redis 重启动时, RDB 程序可以通过载入 RDB 文件来还原数据库的状态。

工作方式

当 Redis 需要保存 dump.rdb 文件时, 服务器执行以下操作:

  1. Redis 调用forks。同时拥有父进程和子进程。
  2. 子进程将数据集写入到一个临时 RDB 文件中。
  3. 当子进程完成对新 RDB 文件的写入时,Redis 用新 RDB 文件替换原来的 RDB 文件,并删除旧的 RDB 文件。

这种工作方式使得 Redis 可以从写时复制(copy-on-write)机制中获益。

三种触发机制

save命令

save 命令执行一个同步操作,以RDB文件的方式保存所有数据的快照。

127.0.0.1:6379> save
OK

需要注意的是save命令是同步命令。如果数据过多,会造成阻塞。

另外需要注意的是,执行save命令会覆盖之前的RDB文件。

bgsave命令

bgsave 命令执行一个异步操作,以RDB文件的方式保存所有数据的快照。

127.0.0.1:6379> bgsave
Background saving started

Redis使用Linux系统的fock()生成一个子进程来将DB数据保存到磁盘,主进程继续提供服务以供客户端调用。

如果操作成功,可以通过客户端命令LASTSAVE来检查操作结果。

LASTSAVE 将返回最近一次 Redis 成功将数据保存到磁盘上的时间,以 UNIX 时间戳格式表示。

127.0.0.1:6379> LASTSAVE
(integer) 1609294414

savebgsave对比

命令savebgsave
IO类型同步异步
阻塞是(阻塞发生在fock(),通常非常快)
复杂度O(n)O(n)
优点不会消耗额外的内存不阻塞客户端命令
缺点阻塞客户端命令需要fock子进程,消耗内存
自动保存

我们可以通过配置文件对 Redis 进行设置, 让它在“ N 秒内数据集至少有 M 个改动”这一条件被满足时, 自动进行数据集保存操作。

相关配置

# RDB自动持久化规则
# 当 900 秒内有至少有 1 个键被改动时,自动进行数据集保存操作
save 900 1
# 当 300 秒内有至少有 10 个键被改动时,自动进行数据集保存操作
save 300 10
# 当 60 秒内有至少有 10000 个键被改动时,自动进行数据集保存操作
save 60 10000

# RDB持久化文件名
dbfilename dump-<port>.rdb

# 数据持久化文件存储目录
dir /var/lib/redis

# bgsave发生错误时是否停止写入,默认为yes
stop-writes-on-bgsave-error yes

# rdb文件是否使用压缩格式
rdbcompression yes

# 是否对rdb文件进行校验和检验,默认为yes
rdbchecksum yes

优点

  • 适合大规模的数据恢复。
  • 如果业务对数据完整性和一致性要求不高,RDB是很好的选择。

缺点

  • 数据的完整性和一致性不高,因为RDB可能在最后一次备份时宕机了。
  • 备份时占用内存,因为Redis 在备份时会独立创建一个子进程,将数据写入到一个临时文件,最后再将临时文件替换之前的备份文件。所以要考虑到大概两倍的数据膨胀性。
  • 针对RDB不适合实时持久化的问题,Redis提供了AOF持久化方式来解决。

AOF

AOF(append only file)持久化:以独立日志的方式记录每次写命令,重启时再重新执行AOF文件中命令达到恢复数据的目的。AOF的主要作用是解决了数据持久化的实时性,目前已经是Redis持久化的主流方式。

AOF创建原理

AOF恢复原理

三种策略

always

每次有新命令追加到 AOF 文件时就执行一次 fsync ,非常慢,也非常安全。

everysec

每秒 fsync 一次:足够快,并且在故障时只会丢失 1 秒钟的数据。

推荐(并且也是默认)为每秒 fsync 一次, 这种 fsync 策略可以兼顾速度和安全性。

no

将数据交给操作系统来处理,由操作系统来决定什么时候同步数据。

三种对比

命令alwayseverysecno
优点不丢失数据每秒一次fsync,可能会丢失一秒数据省心
缺点IO开销较大可能会丢失一秒数据不可控

AOF重写

因为 AOF 的运作方式是不断地将命令追加到文件的末尾, 所以随着写入命令的不断增加, AOF 文件的体积也会变得越来越大。

所以Redis会将已经过期、重复的命令最终改写为生效的命令。

可以在不打断服务客户端的情况下, 对 AOF 文件进行重建(rebuild)。执行 bgrewriteaof 命令, Redis 将生成一个新的 AOF 文件, 这个文件包含重建当前数据集所需的最少命令。

AOF重写的作用

  • 减少磁盘占用量
  • 加速数据恢复

AOF重写实现的两种方式

BGREWRITEAOF

执行一个AOF文件重写操作。重写会创建一个当前 AOF 文件的体积优化版本。

即使BGREWRITEAOF执行失败,也不会有任何数据丢失,因为旧的 AOF 文件在BGREWRITEAOF成功之前不会被修改。

重写操作只会在没有其他持久化工作在后台执行时被触发,也就是说:

  • 如果 Redis 的子进程正在执行快照的保存工作,那么 AOF 重写的操作会被预定(scheduled),等到保存工作完成之后再执行 AOF 重写。在这种情况下, BGREWRITEAOF 的返回值仍然是 OK
  • 如果已经有别的 AOF 文件重写在执行,那么BGREWRITEAOF 返回一个错误,并且这个新的 BGREWRITEAOF 请求也不会被预定到下次执行。

从 Redis 2.4 开始, AOF 重写由 Redis 自行触发,BGREWRITEAOF 仅仅用于手动触发重写操作。

127.0.0.1:6379> BGREWRITEAOF
Background append only file rewriting started
AOF重写配置
配置名称含义
auto-aof-rewrite-min-sizeaof文件重写需要的大小
auto-aof-rewrite-percentageaof文件增长率
统计名称含义
aof_current_sizeAOF文件当前尺寸(字节)
aof_base_sizeAOF文件上次启动和重写时的尺寸(字节)

自动触发时机,同时满足的情况下:

  • aof_current_size > auto-aof-rewrite-min-size
  • (aof_current_size - aof_base_size) * 100 / aof_base_size > auto-aof-rewrite-percentage

AOF重写流程

AOF相关配置

# 开启AOF持久化方式
appendonly yes

# AOF持久化文件名
appendfilename appendonly-<port>.aof

# 每秒把缓冲区的数据同步到磁盘,同步策略
appendfsync everysec

# 数据持久化文件存储目录
dir /var/lib/redis

# 是否在执行重写时不同步数据到AOF文件
no-appendfsync-on-rewrite yes

# 触发AOF文件执行重写的最小尺寸
auto-aof-rewrite-min-size 64mb

# 触发AOF文件执行重写的增长率
auto-aof-rewrite-percentage 100

AOF的优点

  • AOF可以更好的保护数据不丢失,一般AOF会以每隔1秒,通过后台的一个线程去执行一次fsync操作,如果redis进程挂掉,最多丢失1秒的数据。
  • AOF以appen-only的模式写入,所以没有任何磁盘寻址的开销,写入性能非常高。
  • AOF日志文件的命令通过非常可读的方式进行记录,这个非常适合做灾难性的误删除紧急恢复。

AOF的缺点

  • 对于同一份文件AOF文件比RDB数据快照要大。
  • AOF开启后支持写的QPS会比RDB支持的写的QPS低,因为AOF一般会配置成每秒fsync操作,每秒的fsync操作还是很高的。
  • 数据恢复比较慢,不适合做冷备。

RDB和AOF

命令RDBAOF
启动优先级
体积
恢复速度
数据安全性丢数据根据策略决定
轻重

如何抉择

  • 不要仅仅使用RDB这样会丢失很多数据。
  • 也不要仅仅使用AOF,因为这会有两个问题,第一通过AOF做冷备没有RDB做冷备恢复的速度快;第二RDB每次简单粗暴生成数据快照,更加健壮。
  • 综合AOF和RDB两种持久化方式,用AOF来保证数据不丢失,作为恢复数据的第一选择;用RDB来做不同程度的冷备,在AOF文件都丢失或损坏不可用的时候,可以使用RDB进行快速的数据恢复。

主从复制

了解主从复制之前,我们先看看单机有什么问题?

  • 单机故障,比如CPU坏了,内存坏了,宕机。
  • 容量瓶颈。
  • QPS瓶颈,虽然Redis官网说可以达到10w QPS,如果我们想要达到 100w QPS,单机显然是无法做到的。

什么是主从复制

主从复制,是用来建立一个和主数据库完全一样的数据库环境,称为从数据库,主数据库一般是准实时的业务数据库。在最常用的mysql数据库中,支持单项、异步赋值。在赋值过程中,一个服务器充当主服务器,而另外一台服务器充当从服务器;此时主服务器会将更新信息写入到一个特定的二进制文件中。

并会维护文件的一个索引用来跟踪日志循环。这个日志可以记录并发送到从服务器的更新中去。当一台从服务器连接到主服务器时,从服务器会通知主服务器从服务器的日志文件中读取最后一次成功更新的位置。然后从服务器会接收从哪个时刻起发生的任何更新,然后锁住并等到主服务器通知新的更新。

主从复制的作用

  • 确保数据安全;做数据的热备,作为后备数据库,主数据库服务器故障后,可切换到从数据库继续工作,避免数据的丢失。
  • 提升I/O性能;随着日常生产中业务量越来越大,I/O访问频率越来越高,单机无法满足,此时做多库的存储,有效降低磁盘I/O访问的频率,提高了单个设备的I/O性能。
  • 读写分离;使数据库能支持更大的并发。

同样也支持一主多从。

简单演示:

小总结

  • 一个master可以有多个slave
  • 一个slave只能有一个master
  • 数据流向是单向的,master到slave

主从复制的两种实现方式

slaveof命令
127.0.0.1:6379>slaveof ip port

取消复制

需要注意的是断开主从复制后,仍然会保留master之前给它同步的数据。

127.0.0.1:6379>slaveof no one

配置文件
#配置主节点的ip和port
slaveof ip port
#从节点只读,避免主从数据不一致
slave-read-only yes
两种方式对比
方式命令配置
优点无需重启统一配置
缺点不便于管理需要重启

实操

首先准备两台centos,

我这里主节点是: 192.168.3.155

从节点:192.168.3.156

我们使用配置文件的方式,在redis.conf中配置 slaveof 192.168.3.155 6379

注意防火墙开启6379端口。

启动主和从节点后,我们可以使用 info replication查看。

192.168.3.155:6379> info replication
# Replication
role:master
connected_slaves:1
slave0:ip=192.168.3.156,port=6379,state=online,offset=15,lag=1
master_repl_offset:15
repl_backlog_active:1
repl_backlog_size:1048576
repl_backlog_first_byte_offset:2
repl_backlog_histlen:14
192.168.3.156:6379> info replication
# Replication
role:slave
master_host:192.168.3.155
master_port:6379
master_link_status:up
master_last_io_seconds_ago:1
master_sync_in_progress:0
slave_repl_offset:15
slave_priority:100
slave_read_only:1
connected_slaves:0
master_repl_offset:0
repl_backlog_active:0
repl_backlog_size:1048576
repl_backlog_first_byte_offset:0
repl_backlog_histlen:0

我们接着可以测试一下,在主节点加入测试数据,看看从节点是否可以获取。

192.168.3.155:6379> set jack hello
OK
192.168.3.156:6379> get jack
"hello"

全量复制

用于初次复制或其它无法进行部分复制的情况,将主节点中的所有数据都发送给从节点,是一个IE非常重型的操作,当数据量较大时,会对主从节点和网络造成很大的开销。

  1. Redis 内部会发出一个同步命令,刚开始是 psync 命令,psync ? -1表示要求 master 主机同步数据。
  2. 主机会向从机发送 runid (redis-cli info server)和 offset,因为 slave 并没有对应的 offset,所以是全量复制。
  3. 从机会保存主机的基本信息save masterinfo
  4. 主节点收到全量复制的命令后,执行bgsave(异步执行),在后台生成RDB文件(快照),并使用一个缓冲区(称为复制缓冲区)记录从现在开始执行的所有命令。
  5. 主机send RDB发送RDB文件给从机。
  6. 发送缓冲区数据。
  7. 刷新旧的数据,从节点在载入主节点的数据之前要先将老数据清除。
  8. 加载RDB文件将数据库状态更新至主节点执行bgsave时的数据库状态和缓冲区数据加载。

复制偏移量

从节点(slave)每秒钟上报自身的复制偏移量给主节点,因为主节点也会保存从节点的复制偏移量,slave_repl_offset 指标。统计指标如下:

192.168.3.156:6379> info replication
# Replication
role:slave
master_host:192.168.3.155
master_port:6379
master_link_status:up
master_last_io_seconds_ago:1
master_sync_in_progress:0
slave_repl_offset:15
slave_priority:100
slave_read_only:1
connected_slaves:0
master_repl_offset:0
repl_backlog_active:0
repl_backlog_size:1048576
repl_backlog_first_byte_offset:0
repl_backlog_histlen:0

参与复制的主从节点都会维护自身复制偏移量。主节点(master)在处理完写入命令后,会把命令的字节长度做累加记录,统计信息会在info replication 中的master_repl_offset指标中。slave0记录了从节点信息。

192.168.3.155:6379> info replication
# Replication
role:master
connected_slaves:1
slave0:ip=192.168.3.156,port=6379,state=online,offset=15,lag=1
master_repl_offset:15
repl_backlog_active:1
repl_backlog_size:1048576
repl_backlog_first_byte_offset:2
repl_backlog_histlen:14

从节点在接收到主节点发送的命令后,也会累加记录自身的偏移量。统计信息在info replication中的slave_repl_offset

部分复制

  1. 如果网络抖动(连接断开 connection lost)
  2. 主机master 还是会写 replbackbuffer(复制缓冲区)
  3. 从机slave 会继续尝试连接主机
  4. 从机slave 会把自己当前 runid 和偏移量传输给主机 master,并且执行 pysnc 命令同步
  5. 如果 master 发现你的偏移量是在缓冲区的范围内,就会返回 continue 命令
  6. 同步了 offset 的部分数据,所以部分复制的基础就是偏移量 offset。

如何选择

从节点将offset发送给主节点后,主节点根据offset和缓冲区大小决定能否执行部分复制。

如果offset偏移量之后的数据,仍然都在复制积压缓冲区里,则执行部分复制

如果offset偏移量之后的数据已不在复制积压缓冲区中(数据已被挤压),则执行全量复制

主从节点初次复制时,主节点将自己的runid发送给从节点,从节点将这个runid保存起来,当断线重连时,从节点会将这个runid发送给主节点,主节点根据runid判断能否进行部分复制。

如果从节点保存的runid与主节点现在的runid相同,说明主从节点之前同步过,主节点会继续尝试使用部分复制(到底能不能部分复制还要看offset和复制积压缓冲区的情况)。

如果从节点保存的runid与主节点现在的runid不同,说明从节点在断线前同步的Redis节点并不是当前的主节点,只能进行全量复制。

runid可以通过 info server命令来查看。

192.168.3.156:6379> info server
# Server
redis_version:3.0.7
redis_git_sha1:00000000
redis_git_dirty:0
redis_build_id:3fdf3aafcf586962
redis_mode:standalone
os:Linux 3.10.0-1127.el7.x86_64 x86_64
arch_bits:64
multiplexing_api:epoll
gcc_version:4.8.5
process_id:11306
run_id:116ef394d1999f8807f1d30d1bf0dc79aa8d865d
tcp_port:6379
uptime_in_seconds:3442
uptime_in_days:0
hz:10
lru_clock:14271160
config_file:/usr/local/redis-3.0.7/redis.conf

主从复制的问题

  • 主从复制,主挂掉后需要手工来操作麻烦。
  • 写能力和存储能力受限 (主从复制只是备份,单节点存储能力) 。

如果这个时候Master断掉了,那么主从复制也就断掉了。那么这个时候写就失败了。

在这个时候我们只能选择一个从节点执行slaveof no one。然后让其它从节点选择新的主节点。

Redis Sentinel 架构

在主从复制的基础上,增加了多个 Redis Sentinel 节点,这些Sentinel节点不存储数据。在Redis发生故障时候会自动进行故障转移处理,然后通知客户端。

一套Redis Sentinel集群可以监控多套Redis主从,每一套Redis主从通过master-name作为标识。客户端不直接连接Redis服务,而连接Redis Sentinel。

在Redis Sentinel中清楚哪个是master节点。

故障转移过程

  1. 多个Sentinel发现并确认master有问题。
  2. 选举出一个Sentinel作为领导。
  3. 选出一个slave作为master。
  4. 通知其余slave成为新的master的salve。
  5. 通知客户端主从变化。
  6. 等待老的master复活成为新的master的slave。

安装与配置

sentinel monitor mymaster 127.0.0.1 6379 2
sentinel down-after-milliseconds mymaster 60000
sentinel failover-timeout mymaster 180000
sentinel parallel-syncs mymaster 1

在master节点增加 daemonize yes配置后。我们进行启动

redis-sentinel sentinel.conf

我们执行查看命令是否已经启动。可以看到26379端口已经启动。

[root@localhost redis]# ps -ef | grep redis-sentinel
root     11056     1  0 18:38 ?        00:00:00 redis-sentinel *:26379 [sentinel]
root     11064  9916  0 18:41 pts/0    00:00:00 grep --color=auto redis-sentinel

随后我们进行连接,可以使用info命令查看信息

[root@localhost redis]# redis-cli -p 26379
127.0.0.1:26379> info
# Server
redis_version:3.0.7
redis_git_sha1:00000000
redis_git_dirty:0
redis_build_id:311215fe18f833b6
redis_mode:sentinel
os:Linux 3.10.0-1127.el7.x86_64 x86_64
arch_bits:64
multiplexing_api:epoll
gcc_version:4.8.5
process_id:11056
run_id:855df973568ff3604a9a373a799c24601b15822a
tcp_port:26379
uptime_in_seconds:260
uptime_in_days:0
hz:17
lru_clock:14279821
config_file:/usr/local/redis-3.0.7/sentinel.conf

# Sentinel
sentinel_masters:1  # 一个master
sentinel_tilt:0
sentinel_running_scripts:0
sentinel_scripts_queue_length:0
master0:name=mymaster,status=ok,address=127.0.0.1:6379,slaves=2,sentinels=1 #两个从节点

我们再去查看sentinel的配置文件,可以看到已经发生变化,从节点已经配置在里面。

sentinel monitor mymaster 127.0.0.1 6379 2
sentinel known-slave mymaster 192.168.3.156 6379
sentinel known-slave mymaster 192.168.3.157 6379

然后我们在其它的从节点上配置sentinel.conf文件

增加下面代码:

daemonize yes
# 配置master信息
sentinel monitor mymaster 192.168.3.155 6379 2

然后启动。

再执行info命令可以查看现在已经有三个sentinels节点。

master0:name=mymaster,status=ok,address=127.0.0.1:6379,slaves=2,sentinels=3

客户端连接

既然已经实现高可用为什么不直接直连?

高可用涉及的是服务高可用、完成自动的故障转移;故障转移后客户端无法感知将无法保证正常的使用。

需要保证的是服务高可用客户端高可用

客户端实现基本原理

  1. 获取所有的Sentinel的节点和MasterName,遍历Sentinel集合得到一个可用的Sentinel节点。

  2. 向可用的Sentinel节点发送sentinel的get-master-addr-by-name的请求,参数masterName,获取master节点信息。

  3. 客户端获取得到master节点后会执行一次role或者role replication来验证是否是master节点。

  4. master节点发生变化,sentinel是感知的(所有的故障发现、转移是由sentinel做的)。
    sentinel怎么通知client的呢?
    内部是一个发布订阅的模式,client订阅sentinel的某一个频道,该频道里由谁是master的信息,假如由变化sentinel就在频道里publish一条消息,client订阅就可以获取到信息,通过新的master信息进行连接。

完整流程图如下:

JedisSentinelPool sentinelPool = new JedisSentinelPool(masterName, sentinelSet, poolConfig, timeout);
Jedis jedis = null;
try {
    jedis = redisSentinelPool.getResource();
} catch(Exception e) {
    logger.error(e.getMessage(), e);
}finally {
    if(jedis != null) {
        jedis.close();
    }
}
jedis
  • JedisSentinelPool不是连接Sentinel节点集合的连接池。
  • 本质上还是连接master。
  • 只是跟JedisPool进行区分。

故障转移演练

目前还是ip 192.168.3.155主节点。

156和157是从节点,启动了三个sentinel。

/**
 * @author 又坏又迷人
 * 公众号: Java菜鸟程序员
 * @date 2020/12/31
 * @Description:
 */
public class RedisSentinelTest {

    private static Logger logger = LoggerFactory.getLogger(RedisSentinelTest.class);

    public static void main(String[] args) {
        String masterName = "mymaster";
        Set<String> sentinels = new HashSet<>();
        sentinels.add("192.168.3.155:26379");
        sentinels.add("192.168.3.156:26379");
        sentinels.add("192.168.3.157:26379");
        JedisSentinelPool jedisSentinelPool = new JedisSentinelPool(masterName, sentinels);
        int count = 0;
        while (true) {
            count++;
            Jedis jedis = null;
            try {
                jedis = jedisSentinelPool.getResource();
                int index = new Random().nextInt(10000);
                String key = "k-" + index;
                String value = "v-" + index;
                jedis.set(key, value);
                if (count % 100 == 0) {
                    logger.info("{} value is {}", key, jedis.get(key));
                }
                TimeUnit.MILLISECONDS.sleep(10);
            } catch (Exception e) {
                logger.error(e.getMessage(), e);
            } finally {
                if (jedis != null) {
                    jedis.close();
                }
            }
        }
    }
}

我们先正常的启动。

11:06:55.050 [main] INFO RedisSentinelTest - k-6041 value is v-6041
11:06:56.252 [main] INFO RedisSentinelTest - k-3086 value is v-3086
11:06:57.467 [main] INFO RedisSentinelTest - k-3355 value is v-3355
11:06:58.677 [main] INFO RedisSentinelTest - k-6767 value is v-6767

这个时候我们直接强制停止master节点,也就是155节点。

192.168.3.155:6379> info server
# Server
redis_version:3.0.7
redis_git_sha1:00000000
redis_git_dirty:0
redis_build_id:3fdf3aafcf586962
redis_mode:standalone
os:Linux 3.10.0-1127.el7.x86_64 x86_64
arch_bits:64
multiplexing_api:epoll
gcc_version:4.8.5
process_id:4129
run_id:3212ce346ed95794f31dc30d87ed2a4020d3b252
tcp_port:6379
uptime_in_seconds:1478
uptime_in_days:0
hz:10
lru_clock:15496249
config_file:/usr/local/redis-3.0.7/redis.conf

获得process_id:4129 我们直接kill -9 4129,杀掉此进程

之后我们查看是否还存在此进程。

ps -ef | grep redis-server | grep 6379

发现没有后,我们查看Java控制台。在一定时间后,就完成故障转移。程序还是可以正常执行。

Caused by: redis.clients.jedis.exceptions.JedisConnectionException: java.net.ConnectException: Connection refused (Connection refused)
    at redis.clients.jedis.Connection.connect(Connection.java:207)
    at redis.clients.jedis.BinaryClient.connect(BinaryClient.java:93)
    at redis.clients.jedis.BinaryJedis.connect(BinaryJedis.java:1767)
    at redis.clients.jedis.JedisFactory.makeObject(JedisFactory.java:106)
    at org.apache.commons.pool2.impl.GenericObjectPool.create(GenericObjectPool.java:868)
    at org.apache.commons.pool2.impl.GenericObjectPool.borrowObject(GenericObjectPool.java:435)
    at org.apache.commons.pool2.impl.GenericObjectPool.borrowObject(GenericObjectPool.java:363)
    at redis.clients.util.Pool.getResource(Pool.java:49)
    ... 2 common frames omitted
Caused by: java.net.ConnectException: Connection refused (Connection refused)
    at java.net.PlainSocketImpl.socketConnect(Native Method)
    at java.net.AbstractPlainSocketImpl.doConnect(AbstractPlainSocketImpl.java:350)
    at java.net.AbstractPlainSocketImpl.connectToAddress(AbstractPlainSocketImpl.java:206)
    at java.net.AbstractPlainSocketImpl.connect(AbstractPlainSocketImpl.java:188)
    at java.net.SocksSocketImpl.connect(SocksSocketImpl.java:392)
    at java.net.Socket.connect(Socket.java:589)
    at redis.clients.jedis.Connection.connect(Connection.java:184)
    ... 9 common frames omitted
十二月 31, 2020 11:14:13 上午 redis.clients.jedis.JedisSentinelPool initPool
信息: Created JedisPool to master at 192.168.3.156:6379
11:14:13.146 [main] INFO RedisSentinelTest - k-7996 value is v-7996
11:14:14.339 [main] INFO RedisSentinelTest - k-9125 value is v-9125
11:14:15.597 [main] INFO RedisSentinelTest - k-2589 value is v-2589

这样就自动完成故障转移了。

主观下线和客观下线

  • 主观下线:每个sentinel节点对redis节点失败的看法。

    • sentinel down-after-millseconds masterName timeout
    • 每个sentinel节点每秒会对redis节点进行ping,当超过timeout毫秒之后还没得到pong,则认为redis节点下线。
  • 客观下线:所有sentinel节点对redis节点失败达成共识。

    • sentinel monitor masterName ip port quorum
    • 大于等于quorum个sentinel主观认为redis节点失败下线。
    • 通过 sentinel is-master-down-by-addr提出自己认为redis master下线。

领导者选举

  • 原因:只有sentinel节点完成故障转移
  • 选举:通过sentinel is-master-down-by-addr命令希望成功领导者。

    • 每个主观下线的节点向其它的sentinel节点发送命令,要求将它设置为领导者。
    • 收到命令的sentinel节点如果没有同意过其它节点发送的命令,那么将同意该节点,否则拒绝。
    • 如果该sentinel节点发现自己的票数已经超过sentinel集合半数且超过quorum,那么将成为领导者。
    • 如果此过程多个sentinel成为领导者,那么将等待一段时间后重新进行选举。

故障转移

故障转移就是当master宕机,选择一个合适的slave节点来升级为master节点的操作,sentinel会自动完成这个,不需要手动实现。

具体步骤如下:

  1. 在从节点列表中选出一个节点作为新的主节点,选择方法如下:

    • 过滤不健康(主观下线、断线)、5秒内没有回复过Sentinel节点、与主节点失联超过down-after-milliseconds设置的。
    • 选择slave-priority(从节点优先级)最高的节点列表,如果存在则返回,不存在则继续。
    • 选择复制偏移量最大的从节点(复制的最完整),如果存在则返回,不存在则继续。
    • 选择runid最小的从节点。
  2. sentinel领导者节点会对第一步选出来的从节点执行slaveof no one命令让其成为主节点。
  3. sentinel领导者节点会向剩余的从节点发送命令,让它们成为新主节点的从节点,复制规则和parallel-syncs参数有关。
  4. sentinel节点集合会将原来的主节点更新为从节点,并保持着对其关注,当其恢复后命令它去复制新的主节点。

高可用读写分离

我们先了解一下目前从节点的作用:

  • 当主节点出现故障时,作为主节点的后备“顶”上来实现故障转移,Redis Sentinel已经实现了该功能的自动化,实现了真正的高可用。
  • 扩展主节点的读能力,尤其是在读多写少的场景非常适用。

但是目前模型中,从节点并不是高可用的

  • 如果slave-1节点出现故障,首先客户端client-1将与其失联,其次sentinel节点只会对该节点做主观下线,因为Redis Sentinel的故障转移是针对主节点的
  • 所以很多时候,Redis Sentinel中的从节点仅仅是作为主节点一个热备,不让它参与客户端的读操作,就是为了保证整体高可用性,但实际上这种使用方法还是有一些浪费,尤其是在有很多从节点或者确实需要读写分离的场景,所以如何实现从节点的高可用是非常有必要的。

思路

Redis Sentinel在对各个节点的监控中,如果有对应事件的发生,都会发出相应的事件消息

  • +switch-master:切换主节点(原来的从节点晋升为主节点)。
  • +convert-to-slave:切换从节点(原来的主节点降级为从节点)。
  • +sdown:主观下线,说明可能某个从节点可能不可用(因为对从节点不会做客观下线),所以在实现客户端时可以采用自身策略来实现类似主观下线的功能。
  • +reboot:重新启动了某个节点,如果它的角色是slave,那么说明添加了某个从节点。

所以在设计Redis Sentinel的从节点高可用时,只要能够实时掌握所有从节点的状态,把所有从节点看做一个资源池,无论是上线还是下线从节点,客户端都能及时感知到(将其从资源池中添加或者删除),这样从节点的高可用目标就达到了。


神秘杰克
765 声望382 粉丝

Be a good developer.