集群

予早 2024-10-19 16:10:35
Categories: Tags:

Redis集群能否保证强一致性?
Redis集群本身不能保证强一致性,因为它采用的是分片(sharding)的方式来水平扩展,数据被分布存储在不同的节点上。在Redis集群中,数据的写入是通过将数据分散存储在不同节点上来实现水平扩展的,每个节点只负责管理部分数据。这种设计使得Redis集群在某些情况下可能会出现数据不一致的情况,即可能存在数据写入一个节点成功,但由于某些节点未及时同步或者网络延迟等原因,导致其他节点上的数据并未更新的情况。

哈希算法

哈希取余算法

一致性哈希算法

哈希槽算法

单节点Redis的问题

  1. 数据丢失问题
    • 描述:Redis是内存存储,服务重启可能会丢失数据
    • 解决:实现Redis数据持久化
  2. 并发能力问题
    • 描述:单节点Redis并发能力虽然不错,但也无法满足如618这样的高并发场景
    • 解决:搭建主从集群,实现读写分离
  3. 存储能力问题
    • 描述:Redis基于内存,单节点能存储的数据量难以满足海量数据需求
    • 解决:搭建分片集群,利用插槽机制实现动态扩容
  4. 故障恢复问题
    • 描述:如果Redis宕机,则服务不可用,需要一种自动的故障恢复手段
    • 解决:利用Redis哨兵,实现健康检测和自动恢复

Redis 持久化

以下命令触发持久化

save
bgsave

RDB snapshotting

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

快照文件称为RDB文件,默认是保存在当前运行目录。

Redis停机时会执行一次RDB。

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

# 文件保存的路径目录
dir ./dumpfiles

# 每过900s进行一次检查,若至少有1个key被修改,则执行bgsave,如果是save "" 则表示禁用自动RDB
save 900 1  
save 300 10  
save 60 10000 

# 是否压缩 ,建议不开启,压缩也会消耗cpu,磁盘的话不值钱 yes no
rdbcompression no

# RDB文件名称,推荐使用dump
dbfilename dump6379.rdb  

# bgsave发生错误时停止Redis写入操作
stop-writes-on-bgsave-error yes

# rdb生成后进行校验,需要一定性能牺牲
rdbchecksum no

# 控制在删除 RDB 文件时是否使用同步操作
rdb-del-sync-files no

bgsave开始时会fork主进程得到子进程,子进程共享主进程的内存数据。完成fork后读取内存数据并写入 RDB 文件。
fork采用的是copy-on-write技术:

RDB方式bgsave的基本流程?

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

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

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

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

默认是服务停止时。

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

RDB的缺点?

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

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

什么时候会触发快照保存(下面任意情形)

  1. 配置文件中默认的快照配置
  2. 手动save/bgsave命令
  3. 执行flushall/flushdb命令也会产生dump.rdb文件,但里面是空的,无意义,两者是特殊的写操作
  4. 执行shutdown且没有设置开启AOF持久化
  5. 主从复制时,主节点自动触发

AOF

AOF全称为Append Only File(追加文件)。Redis处理的每一个写命令都会记录在AOF文件,可以看做是命令日志文件。

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

# 是否开启自动AOF功能,默认是no
appendonly yes
# AOF文件的名称
appendfilename "appendonly.aof"

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

# 表示每执行一次写命令,立即记录到AOF文件
appendfsync always 
# 写命令执行完先放入AOF缓冲区,然后表示每隔1秒将缓冲区数据写到AOF文件,是默认方案
appendfsync everysec 
# 写命令执行完先放入AOF缓冲区,由操作系统决定何时将缓冲区内容写回磁盘
appendfsync no
配置项 刷盘时机 优点 缺点
Always 同步刷盘 可靠性高,几乎不丢数据 性能影响大
everysec 每秒刷盘 性能适中 最多丢失1秒数据
no 操作系统控制 性能最好 可靠性较差,可能丢失大量数据

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

AOF重写

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

# 两者为且的条件
# AOF文件比上次文件 增长超过多少百分比则触发重写
auto-aof-rewrite-percentage 100
# AOF文件体积最小多大以上才触发重写
auto-aof-rewrite-min-size 64mb 

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

RDB AOF
持久化方式 定时对整个内存做快照 记录每一次执行的命令
数据完整性 不完整,两次备份之间会丢失 相对完整,取决于刷盘策略
文件大小 会有压缩,文件体积小 记录命令,文件体积很大
宕机恢复速度 很快
数据恢复优先级 低,因为数据完整性不如AOF 高,因为数据完整性更高
系统资源占用 高,大量CPU和内存消耗 低,主要是磁盘IO资源但AOF重写时会占用大量CPU和内存资源
使用场景 可以容忍数分钟的数据丢失,追求更快的启动速度 对数据安全性要求较高常见

触发机制

自动触发

手动触发

bgrewriteaof

重写原理
Redis 7中AOF文件重写的内部执行流程是一个旨在优化AOF文件大小和提高Redis性能的过程。这个过程不仅减少了磁盘空间的占用,还能在不影响Redis服务的前提下进行。以下是AOF文件重写的详细内部执行流程:

触发重写:AOF重写可以通过执行BGREWRITEAOF命令手动触发,或者根据配置文件中的规则自动触发。自动触发通常基于文件大小的增长比例。

创建子进程:Redis通过创建一个子进程来执行AOF重写操作,这样做的好处是避免阻塞主进程,确保Redis服务的响应性。主进程继续处理客户端请求。

子进程重写AOF文件:子进程开始根据当前内存中的数据状态构建一个新的AOF文件。这个过程不会读取旧的AOF文件,而是将当前内存中的数据状态作为源数据。这意味着新的AOF文件将只包含达到当前状态所需的最小操作集。

记录增量写操作:在子进程重写AOF文件的同时,主进程会继续处理客户端请求,并将这期间的所有写操作同时记录到旧的AOF文件和一个内存缓冲区中。这保证了在AOF重写过程中,任何新的更改都不会丢失。

同步新的AOF文件:子进程完成AOF重写后,它会向主进程发送一个信号。主进程在接到这个信号后,会将步骤4中记录在内存缓冲区的增量写操作追加到新的AOF文件中。这一步确保了新的AOF文件反映了最新的数据状态。

原子替换AOF文件:一旦新的AOF文件包含了所有最新的数据,Redis会原子性地将旧的AOF文件替换为新的AOF文件。这个替换操作是通过重命名文件来完成的,确保了操作的原子性。

完成重写过程:新的AOF文件替换旧文件后,AOF重写过程完成。此时,Redis会继续使用新的AOF文件来记录后续的写操作。

    这个过程优化了AOF文件的大小,提高了Redis的性能,同时确保了数据的完整性和一致性。通过在子进程中进行大部分重写工作,Redis能够保持高性能和高可用性,即使在进行数据持久化优化操作时也是如此。

Redis集群最大节点个数是多少?

16384个,$2^{14}$,不会超过哈希槽数量

Redis集群如何选择数据库?

Redis集群目前无法做数据库选择,默认在0数据库。

你知道有哪些Redis分区实现方案?

客户端分区就是在客户端就已经决定数据会被存储到哪个redis节点或者从哪个

redis节点读取。大多数客户端已经实现了客户端分区。

代理分区 意味着客户端将请求发送给代理,然后代理决定去哪个节点写数据或

者读数据。代理根据分区规则决定请求哪些Redis实例,然后根据Redis的响应结果返

回给客户端。redis和memcached的一种代理实现就是Twemproxy

查询路由(Query routing) 的意思是客户端随机地请求任意一个redis实例,然后

由Redis将请求转发给正确的Redis节点。Redis Cluster实现了一种混合形式的查询

路由,但并不是直接将请求从一个redis节点转发到另一个redis节点,而是在客户端

的帮助下直接redirected到正确的redis节点。

并发竞争redis的key问题

多个线程并发操作,先判断某个key的值,若符合条件就+1,由于两者不是原子操作,会造成问题

主从

单节点Redis并发能力有限,当业务需要满足更高并发请求时,首先考虑搭建主从集群,基于读写分离提高Redis并发能力。

主节点可写可读,从节点只读,从节点从主节点同步数据。

主从集群中主节点宕机,从节点可以继续提供只读服务,随后主节点恢复,从节点可重连主节点并进行数据同步。

主从同步机制

解释主从复制

https://blog.csdn.net/a1076067274/article/details/109294208

假设有A、B两个Redis实例,如何让B作为A的slave节点?

在B节点执行命令:slaveof A的IP A的port

主从之前第一次同步为全量同步

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

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

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

更加详细的增量同步和全量同步解析

https://blog.csdn.net/a1076067274/article/details/109294208

全量同步

增量同步

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

正常同步

异常同步

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

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

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

什么时候执行全量同步?

什么时候执行增量同步?

slave节点宕机恢复后可以找master节点同步数据,那master节点宕机怎么办?

哨兵

Redis提供了哨兵(Sentinel)机制来实现主从集群的自动故障恢复。

监控

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

选举

一旦发现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大小,越小优先级越高。

自动故障恢复

当选中了其中一个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,当故障节点恢复后会自动成为新的master的slave节点

在一主两从的主从集群基础上增加由三个节点构成的哨兵集群。

Redis主节点选举机制

  1. 正常运行阶段

哨兵会定期查询主节点的健康状态,即为周期性的发送ping命令对主节点进行心跳检测,若每次检测都正常,则整个系统处于正常状态

  1. 主观下线阶段,Subjectively Down (SDown)

当某一个哨兵检测时发现主节点心跳断开,且断开时间超过了某个阈值,该哨兵主观认为主节点已下线。时间阈值由配置sentinel down-after-milliseconds <masterName> <timeout>设置。

  1. 客观下线阶段,Objectively Down (ODown)

当主观认为主节点已下线的哨兵数量超过某个阈值,则哨兵集群认为主节点客观下线。该阈值由配置sentinel monitor 主机名称 主机ip 主机端口 下线条件设置。

  1. 哨兵leader选举

当哨兵集群已确认主节点客观下线时,哨兵集群需要选举一个哨兵leader负责此次故障切换。选举算法采用Raft。

  1. 哨兵leader选择主节点

  2. 过滤故障的节点

  3. 选择优先级slave-priority最大的从节点作为主节点,如不存在则继续

  4. 选择复制偏移量(数据写入量的字节,记录写了多少数据。主服务器会把偏移量同步给从服务器,当主从的偏移量一致,则数据是完全同步)最大的从节点作为主节点,如不存在则继续

  5. 选择runid(redis每次启动的时候生成随机的runid作为redis的标识)最小的从节点作为主节点

从节点升为主节点后,sentinel.conf会被动态修改,主节点信息和从节点信息会被重写

哨兵配置sentinel.conf

# sentinel端口,默认值26379
port 26379

# 是否为守护进程,默认为no,可选yes、no
daemonize yes

# 工作目录设置
dir "/home/centos/sentinel03"

# 日志文件名称,会放在工作目录下,空字符串表示日志输出到标准输出
logfile ""

# sentinel monitor <master-name> <ip> <redis-port> <quorum>
# 设置sentinel监听master的名称、IP、端口,以及指定至少有quorum个sentinel认为master离线时,master便算作真正离线
sentinel monitor mymaster 192.168.1.9 7001 2


# sentinel auth-pass <master-name> <password>
# 设置连接master和replica的密码,sentinel不支持master和replica的密码不同,因此需要保证master和replica密码相同
sentinel auth-pass mymaster 123456


# sentinel down-after-milliseconds <master-name> <milliseconds>
# 指定一个master与一个sentinel断开连接多长时间,sentinel会认为master离线。单位为毫秒,默认为3秒。
sentinel down-after-milliseconds mymaster 30000


# sentinel parallel-syncs <master-name> <numslaves>
# 指定从节点升为主节点后,最多同时可以有几个从节点进行从新的master同步数据,也意味着最多同时会有几个从节点由于全量数据同步而不可用。
# 默认值为1
sentinel parallel-syncs mymaster 1

Sentinel的三个作用是什么?
监控
故障转移
通知
Sentinel如何判断一个redis实例是否健康?
每隔1秒发送一次ping命令,如果超过一定时间没有相向则认为是主观下线
如果大多数sentinel都认为实例主观下线,则判定服务下线
故障转移步骤有哪些?
首先选定一个slave作为新的master,执行slaveof no one
然后让所有节点都执行slaveof 新master
修改故障节点,执行slaveof 新master

客户端切换

RedisTemplate的哨兵模式

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

  1. 在pom文件中引入redis的starter依赖:

    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-data-redis</artifactId>
    </dependency>
    
  2. 然后在配置文件application.yml中指定sentinel相关信息

    spring:
        redis:
            sentinel:
                master: mymaster # 指定master名称
                    nodes: # 指定redis-sentinel集群信息
                    - 192.168.150.101:27001
                    - 192.168.150.101:27002
                    - 192.168.150.101:27003
    
  3. 配置主从读写分离

    @Bean
    public LettuceClientConfigurationBuilderCustomizer configurationBuilderCustomizer(){
        return configBuilder -> configBuilder.readFrom(ReadFrom.REPLICA_PREFERRED);
    }
    

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

    • MASTER:从主节点读取
    • MASTER_PREFERRED:优先从master节点读取,master不可用才读取replica
    • REPLICA:从slave(replica)节点读取
    • REPLICA _PREFERRED:优先从slave(replica)节点读取,所有的slave都不可用才读取master

分片

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

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

Redis.conf

bind 0.0.0.0
port 6379

save 3600 1
save 300 100
save 60 10000
dbfilename dump.rdb
dir ./
replicaof <masterip> <masterport>
replica-read-only yes
appendonly no
appendfilename "appendonly.aof"

repl-diskless-sync yes

分片集群redis.conf配置

bind 0.0.0.0
port 6379
daemonize yes
protected-mode no

databases 1

requirepass 123456
masterauth 123456

pidfile redis.pid
logfile "redis.log"

dir /usr/local/redis_cluster/redis01

dbfilename dump.rdb

appendonly yes
appendfilename "dump.aof"

# 开启集群功能
cluster-enabled yes
# 集群的配置文件名称,不需要我们创建,由redis自己维护
cluster-config-file /usr/local/redis_cluster/redis01/nodes.conf
# 节点心跳失败的超时时间
cluster-node-timeout 5000
集群

redis哨兵结点间自动发现机制

自动重写哨兵节点的配置文件

sentinel.conf,#Generated by CONFIG REWRITE

sentinel.conf中不需要手动配置其余哨兵结点和从结点的地址,哨兵会通过master获知其余哨兵结点和从结点(那链式结点呢)

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

1.引入springdataredis依赖

2.配置application.yaml哨兵集群

3.配置读写策略

散列插槽

铁打的slot,流水的master

key与插槽绑定、重定向

CLUSTER COUNTKEYSINSLOT 4567
    1,该槽位被占用
        0,该槽位没占用

带通识符的set get命令

mset k1{x} v1 k2{x} v2 ...



mget k1{x} k2{x}  ...

不在同一个slot槽位下的键值无法使用mset、mget等多键操作。

可以通过{}来定义同一个组的概念,使key中{}内相同内容的键值对放到一个slot槽位去,对照下图类似k1k2k3都映射为x,自然槽位一样。

这也就解释了为什么CRC16 算法源码 cluster.c的KeyHashSlot方法 中有对 “{” 和 “}” 的判断

哈希函数采用CRC16

hash_slot = CRC16(key)mod 16384

redis-cli -c -p 7001

尽量避免跨结点查询,将同类数据放在同一结点上,将{keyId}…为前缀,控制key的有效部分

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

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

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

key中不包含“{}”,整个key都是有效部分

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

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

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

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

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

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

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

集群伸缩

添加结点,移动插槽

移动插槽,删除结点

添加一个节点到集群

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

比如,添加节点的命令:

向集群中添加一个新的master节点,并向其中存储 num = 10

求:
启动一个新的redis实例,端口为7004
添加7004到之前的集群,并作为一个master节点
给7004节点分配插槽,使得num这个key可以存储到7004实例

删除集群中的一个节点

删除7004这个实例

故障转移

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

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

数据迁移

手动变更结点角色,手动故障转移

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

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

RedisTemplate访问分片集群

RedisTemplate底层同样基于lettuce实现了分片集群的支持,而使用的步骤与哨兵模式基本一致:
引入redis的starter依赖
配置分片集群地址
配置读写分离
与哨兵模式相比,其中只有分片集群的配置方式略有差异,如下:

spring:
    redis:
        cluster:
            nodes: # 指定分片集群的每一个节点信息
            - 192.168.150.101:7001
            - 192.168.150.101:7002
            - 192.168.150.101:7003
            - 192.168.150.101:8001
            - 192.168.150.101:8002
            - 192.168.150.101:8003