分布式缓存

之前学的都是单点Redis,而这种单点Redis存在一些问题:

  • 数据丢失
  • 并发能力差
  • 存储量小
  • 故障恢复能力弱

我们可以使用Redis分布式缓存解决以上问题。

Redis持久化解决数据丢失问题

RDB持久化

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

RDB持久化的执行条件有save命令bfsave命令Redis手动停机触发系统内置RDB条件时

save命令

使用redis-cli链接到redis中后使用save命令可以立刻执行一次RDB。save命令会导致主进程执行RDB,这个过程中其它所有命令都会被阻塞。只有在数据迁移时可能用到。

bgsave命令

save命令的执行步骤相同,但bgsave命令执行是异步的,执行后会开启独立进程完成RDB,主进程可以持续处理用户请求,不受影响。

停机

主动停机时会自动执行一次RDB

Redis内置RDB条件

Redis内部有触发RDB的机制,可以在redis.conf文件中找到(保持默认即可),格式如下:

# 900秒内,如果至少有1个key被修改,则执行bgsave, 如果是save "" 则表示禁用RDB
save 900 1
save 300 10
save 60 10000

# 是否压缩,建议不开启
rdbcompression yes

# RDB文件名称
dbfilename dump.rdb

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

RDB原理

当 bgsave 执行时,主进程会 fork(fork()是unix和linux这种操作系统的一个api)一个子进程,子进程共享主进程的内存数据,完成fork后子进程读取内存数据并写入 RDB 文件。

当子进程读取内存数据写入 RDB 文件时,主进程可以继续进行工作,依靠的是 copy-on-write 技术。

  • 当主进程执行读操作时,直接访问共享内存
  • 当主进程执行写操作时,则会在内存中拷贝一份数据,对拷贝的数据执行写操作,这样不会影响到子进程读取的内存数据

如果不使用copy-on-write会怎么样?

不使用copy-on-write,就意味着子进程在进行写RDB文件时,主进程可以修改子进程要读取的内存数据,那么就无法保证某一时刻数据的一致性。

RDB的缺点

  • RDB执行间隔时间长,两次RDB之间写入数据有丢失风险
  • fork子进程、压缩、写出RDB文件都比较耗时

AOF持久化

AOF全称为Append Only File(追加文件)。Redis处理的每一个写命令都会记录在AOF文件,可以看做是命令日志文件。(由主进程先写入到缓冲区,之后由后台线程将缓冲区中的数据写入到AOF文件

AOF配置

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

# 是否开启AOF功能,默认是no
appendonly yes
# AOF文件的名称
appendfilename "appendonly.aof"
# 表示每执行一次写命令,立即记录到AOF文件
appendfsync always
# 写命令执行完先放入AOF缓冲区,然后表示每隔1秒将缓冲区数据写到AOF文件,是默认方案
appendfsync everysec
# 写命令执行完先放入AOF缓冲区,由操作系统决定何时将缓冲区内容写回磁盘
appendfsync no

appendfsync三种策略对比:

配置项 刷盘实机 优点 缺点
always 同步刷盘 可靠性高,几乎不丢失数据 性能影响大
everysec 每秒刷盘 性能适中 可能会丢失1秒内的数据
no 操作系统控制 性能最好 可靠性较差,可能丢失大量数据

AOF文件重写

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

如果一直没执行此命令,Redis也会在触发阈值时自动重写AOF文件(异步执行)。阈值也可以在redis.conf中配置:

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

RDB与AOF对比

在实际开发中往往会结合两者来使用

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

总结

RDB

  • RDB是一种快照持久化方法,它会在指定的时间间隔内生成数据的完整快照
  • 适合于灾难恢复,可以很方便的被迁移到另一个数据中心
  • RDB在保存快照时速度快,恢复时也非常迅速,适合用作备份
  • 最后一次快照之后的数据可能会丢失,因为这部分数据还没有被写入快照

AOF

  • AOF记录每一条写命令
  • AOF提供了更好的数据安全性,可以配置为每秒同步一次,或者每写入一条命令就同步一次
  • AOF文件通常会比RDB文件更大,且恢复速度可能会更慢,但可以通过AOF文件重写进行压缩
  • AOF在系统崩溃时能最大化数据恢复,最多只丢失几秒钟的数据

如果需要快速恢复且可以接受少量数据丢失,RDB可能是更好的选择。如果注重数据完整性可以接受较慢的恢复速度,则应该使用AOF。在很多场景下,结合使用RDB和AOF能提供更为可靠的数据保护机制。

主从集群解决并发问题

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

主从集群可以是一台Redis服务器作为主节点(master),数台服务器作为从节点(slave),主节点只负责写数据,从节点只负责读数据。

数据同步原理

主从之间的第一次同步全量同步,既要加载RDB文件,又要读取并执行repl_baklog中的命令。

master&slave

如何判断是不是第一次同步?

  • 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才可以判断到底需要同步哪些数据

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

优化主从集群性能

  • 在master中配置repl-diskless-sync yes启用无磁盘复制,避免全量同步时的磁盘IO。
  • Redis单节点上的内存占用不要太大,减少RDB导致的过多磁盘IO
  • 适当提高repl_baklog的大小,发现slave宕机时尽快实现故障恢复,尽可能避免全量同步
  • 限制一个master上的slave节点数量,如果实在是太多slave,则可以采用主-从-从链式结构,减少master压力

Redis哨兵解决故障恢复问题

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

这就需要指定一个slave节点为新的master,执行写操作。这个操作不需要人工手动执行,因为Redis提供了哨兵(Sentinel)机制来实现主从集群的自动故障恢复。哨兵本身也是一个集群

哨兵的结构和作用

  • 监控:Sentinel会不断检查master和slave是否按预期工作
  • 自动故障恢复:如果master故障,Sentinel会将一个slave提升为master。当故障实例恢复后也以新的master为主
  • 通知:Sentinel充当Redis客户端的服务发现来源,当集群发生故障转移时,会将最新消息推送给Redis的客户端

服务状态监控

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

  • 主观下线:如果Sentinel节点发现某实例未在规定时间相应,则认为该实例主观下线
  • 客观下线:若超过指定数量(quorum)的Sentinel都认为该实例主观下线,则该实例客观下线。quorum的取值最好超过Sentinel数量的一半

选举新的master

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

  • 首先会判断slave节点与master节点断开时间长短,如果超过指定值(down-after-miliseconds*10)则会排除该slave节点
  • 然后判断slave节点的slave-priority值,越小优先级越高,如果是0则永不参与选举
  • 如果slave-prority一样,则判断slave节点的offset值,越大说明数据越新,优先级越高
  • 最后是判断slave节点的运行id大小,越小优先级越高

实现故障转移

当选中了其中一个slave为新的master后:

  • Sentinel给备选的slave节点发送slaveof no one命令,让该节点成为master
  • Sentinel给所有其它slave发送slaveof IP地址 端口命令,让这些slave成为新master的从节点,开始从新的master上同步数据
  • 最后,Sentinel将故障节点标记为slave,当故障节点恢复后会自动成为新的masterslave节点

脑裂问题与解决方案

脑裂问题是指在分布式系统中,由于网络分区导致集群被分割成多个独立的部分,每个部分都可能认为自己是唯一正常运行的集群,从而导致数据不一致或服务冲突。

原因:

  1. 网络分区:集群节点之间的网络通信中断,导致部分节点无法互相通信
  2. 主从切换:在网络分区后,部分节点可能选举出新的主节点,导致同一集群中出现多个主节点
  3. 数据不一致:不同主节点可能同时接收写请求,导致数据冲突或丢失

表现:

  • 集群被分割为多个独立子集群,每个子集群可能都有自己的主节点
  • 客户端可能连接到不同的子集群,读取或写入不一致的数据
  • 网络恢复后,集群可能无法自动合并,导致数据冲突

解决方案

  1. 集群配置参数
  • min-slaves-to-write:主节点至少需要多少个从节点才能接收写请求。如min-slaves-to-write 1表示主节点至少需要1个从节点才能写入数据。在网络分区后,如果主节点失去足够多的从节点,则停止写入,避免数据不一致
  • min-slaves-max-lag:从节点与主节点的最大延迟时间(秒)。如min-slaves-max-lag 10表示从节点的复制延迟不能超过10秒。如果从节点延迟过高,主节点会停止写入,确保数据同步
  1. 数据分片
  • Redis集群将数据划分为16384个哈希槽,每个主节点负责一部分槽
  • 在网络分区后,只有持有完整槽信息的分区才能提供服务,避免数据冲突

Java中使用RedisTemplate配置哨兵集群

引入依赖

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

配置Redis

logging:
level:
io.lettuce.core: debug
pattern:
dateformat: MM-dd HH:mm:ss:SSS
spring:
redis:
sentinel:
master: mymaster
nodes:
- IP地址 端口1
- IP地址 端口2
- IP地址 端口3

配置主从读写分离

@SpringBootApplication
public class RedisDemoApplication {

public static void main(String[] args) {
SpringApplication.run(RedisDemoApplication.class, args);
}

// 配置主从读写分离
@Bean
public LettuceClientConfigurationBuilderCustomizer configurationBuilderCustomizer(){
return clientConfigurationBuilder -> clientConfigurationBuilder.readFrom(ReadFrom.MASTER_PREFERRED);
}

}

ReadForm函数是配置Redis的读取策略,有以下选择:

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

Redis分片集群解决海量数据存储问题

分片集群特征:

  • 集群中有多个master,每个master保存不同数据
  • 每个master都可以有多个slave节点
  • master之间通过ping监测彼此健康状态
  • 客户端请求可以访问集群任意节点,最终都会被转发到正确节点

散列插槽

Redis会把每一个master节点映射到0~16384个插槽上,通过查看集群信息时即可看到每个节点插槽的区间

redis-cli -p 7001 cluster nodes

集群伸缩

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

redis-cli --cluster help

故障迁移

如果集群中某个master宕机了,则该master下优先级最高的slave节点会变成master,选举出的新master会接管原master的槽位(slot)和数据,继续对外提供服务。其他节点会更新自己的路由表,将请求转发到新的master。如果原master重新上线,它会成为新master的slave节点,开始同步数据。

failover

Redis访问分片集群

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

  1. 引入redis的starter依赖
  2. 配置分片集群地址
  3. 配置读写分离
  4. 与哨兵模式相比,其中只有分片集群的配置方式略有差异,如下:
spring:
redis:
cluster:
nodes:
- IP地址 端口1
- IP地址 端口2
- IP地址 端口3
- IP地址 端口4
- IP地址 端口5
- IP地址 端口6