分布式缓存
1.1 当前单点结构的问题
数据存储问题:Redis基于内存存储,服务重启可能会丢失数据
并发能力问题:单节点Redis并发能力虽然不错,但也无法满足618这样的高并发场景
故障恢复问题(容灾):Redis宕机,则服务不可用
存储能力问题:Redis基于内存,单节点能存储的数据量难以满足海量数据需求

1.2 Redis 持久化
1.2.1 RDB(全量备份)
RDB 全称 Redis Database Backup file(Redis 数据备份文件),也被叫做 Redis 数据快照。简单来说就是把内存中的所有数据都记录到磁盘中。当 Redis 实例故障重启后,从磁盘读取快照文件,恢复数据。快照文件称为 RDB 文件,默认是保存在当前运行目录。
两个命令
save:由Redis主进程来执行RDB,会阻塞所有命令,生产模式禁用。bgsave:开启子线程执行RDB,避免主进程受到影响。Redis在停机时会执行一次RDB,但是Redis突然宕机(进程崩溃、断电、硬件故障等)是不会执行RDB的。
配置
配置项 示例值 详细说明 save <m> <n>save 300 10触发规则:在 $m$ 秒内如果有 $n$ 个键发生改变,则自动触发 BGSAVE。可以配置多条。dbfilenamedump.rdb文件名:指定生成的快照文件的名称。 dir./存储目录:RDB 文件和 AOF 文件的存放路径。 stop-writes-on-bgsave-erroryes容错机制:如果后台快照保存失败,Redis 是否停止接收写命令(默认开启,起到预警作用)。 rdbcompressionyes压缩开关:采用 LZF 算法对 RDB 文件进行压缩。开启会节省磁盘空间,但会稍微增加 CPU 开销。 rdbchecksumyes数据校验:在 RDB 文件末尾添加 CRC64 校验码。开启会增加约 10% 的性能损耗,但数据更安全。 rdb-del-sync-filesno同步删除:在没有开启持久化的实例中,是否删除同步拉取的 RDB 文件(通常用于主从同步)。 运行时命令
如果你不想重启 Redis,可以通过客户端(
redis-cli)直接操作:SAVE:前台阻塞式保存。在生产环境严禁使用,因为它会阻塞主线程,导致 Redis 无法处理其他请求。BGSAVE:后台异步保存。Redis 会fork出一个子进程来处理快照,主线程继续响应请求。LASTSAVE:返回最近一次成功执行快照的时间戳(Unix timestamp)。CONFIG SET save "60 10000":动态修改保存规则,无需重启。
底层原理
- 执行RDB 的bgsave的时候,主线程会执行 fork 操作,将页表(虚拟内存和内存的映射关系,Redis就是通过虚拟内存来操作内存的)共享给子进程,子进程会对目标内存进行读操作,写新RDB文件,替换掉旧的RDB文件,最终的RDB文件是主进程 fork 那一刻的数据。主进程的写操作不会影响到子进程。
- 主进程在执行 bgsave 的时候,如果有用户==写==那部分只读数据,主进程会将那部分数据进行拷贝,之后的读写操作都在拷贝数据上进行,因此极端情况下,如果数据全部被拷贝一遍,Redis会占用原本的2倍内存,所以要预留一部分空间给Redis。

总结

1.2.2 AOF(增量备份)
AOF 全称 Append Only File(追加文件)。与 RDB 不同,它不是记录数据快照,而是记录 Redis 接受到的每一个写命令。当 Redis 重启时,会通过重新执行这些命令来恢复数据,就像“重放”录像带一样。
AOF 默认是关闭的,需要修改
redis.conf配置文件来开启:appendonly yes:开启 AOF 功能。appendfilename "appendonly.aof":指定 AOF 文件名。
三种写回策略(appendfsync
策略) 由于磁盘 IO 慢,Redis 写入命令时会先写入内存缓存区(OS Cache),何时刷入磁盘由以下配置决定:
策略 刷盘时机 优点 缺点 Always 每执行一个写命令立即同步刷盘 数据安全性最高,几乎不丢失 性能最差,受限于磁盘 IO 瓶颈 Everysec 每秒同步一次(默认) 性能与安全折中,兼顾吞吐量 宕机时可能丢失 1 秒左右的数据 No 由操作系统控制刷盘时机 性能最高 安全性最低,丢失数据量不可控 AOF 重写(Optimization)
随着命令越来越多,AOF 文件会变得非常臃肿(比如给同一个 key 执行了 100 次
INCR,AOF 会记录 100 条命令,但其实只需要记录 1 条SET结果)。- bgrewriteaof:Redis 会执行该命令异步重写 AOF,剔除冗余命令,减小文件体积。
- 自动重写配置:
auto-aof-rewrite-percentage 100:对比上次重写后的体积,增长 100% 时触发。auto-aof-rewrite-min-size 64mb:文件体积至少达到 64MB 才触发。
底层原理
AOF 的工作流程主要分为四个步骤:命令追加 (Append)、文件写入 (Write)、文件同步 (Sync)、文件重写 (Rewrite)。
- 命令追加:主线程在执行写命令后,将命令以 Redis 通讯协议 (RESP) 格式追加到
aof_buf缓冲区。 - 异步刷盘:根据
appendfsync的策略,由后台线程或操作系统将缓冲区内容写入磁盘。 - 重写机制 (Rewriting):
- 主进程
fork出一个子进程。 - 子进程读取内存中的当前最新数据(不是读取旧 AOF 文件),将其转换为最简便的命令写入临时文件。
- 一致性保证:在重写期间,主进程的新命令会同时写入
aof_buf和aof_rewrite_buf(重写缓冲区)。 - 替换:子进程写完新 AOF 后,主进程将重写缓冲区的内容追加到新文件,最后原子替换旧 AOF。
- 主进程
- 命令追加:主线程在执行写命令后,将命令以 Redis 通讯协议 (RESP) 格式追加到
1.2.3 RDB 和 AOF 的优缺点

1.3 Redis 主从
1.3.1 搭建主从结构
主从架构基本结构

即搭建多个节点,由于Redis使用中大多是读多写少的场景,所以我们选择读写分离的策略,由主节点负责写操作,从节点负责读操作,再有主节点数据同步给从节点。
架构的要在于如何进行集群的配置以及如何进行数据同步,以及之后的容灾(可用性),容灾之后我们会使用哨兵。、
Redis集群搭建
参考:Redis集群
开启主从模式,有临时和永久两种模式:
修改配置文件(永久生效)
- 在redis.conf中添加一行配置:
slaveof <masterip> <masterport>
- 在redis.conf中添加一行配置:
使用redis-cli客户端连接到redis服务,执行slaveof命令(重启后失效):
shellslaveof <masterip> <masterport>
INFO replication:查看集群信息
使用Docker搭建Redis集群
编写配置文件(配置文件的位置要和docker-compose.yml 文件中挂载卷的位置保持一致)
master:
yml# 允许所有 IP 连接 bind 0.0.0.0 # 端口(容器内固定为 6379) port 6379 # 关闭保护模式 protected-mode no # 开启 AOF 持久化(建议开启,保证数据安全) appendonly yes # 主节点密码(如果需要安全,建议设置) requirepass 123456 # 从节点连接主节点所需的密码(主从密码建议设为一致) masterauth 123456slave:
ymlbind 0.0.0.0 port 6379 protected-mode no appendonly yes requirepass 123456 masterauth 123456 # 【核心:指明主节点是谁】 # 使用 docker-compose 时,可以直接写服务名 redis-master replicaof redis-master 6379 # 【核心:声明 IP 和端口】 # 告诉主节点,我的宿主机 IP 是这个 replica-announce-ip 192.168.134.128 # 告诉主节点,我在宿主机映射的端口是 7002(slave2 改为 7003) replica-announce-port 7002
编写 docker-compose.yml
ymlservices: master: image: redis:6.2.6 container_name: redis-master volumes: - ./conf/master/redis.conf:/etc/redis/redis.conf - ./data/master:/data command: redis-server /etc/redis/redis.conf ports: - "7001:6379" networks: redis-net: ipv4_address: 172.20.0.10 slave1: image: redis:6.2.6 container_name: redis-slave1 depends_on: - master volumes: - ./conf/slave1/redis.conf:/etc/redis/redis.conf - ./data/slave1:/data command: redis-server /etc/redis/redis.conf ports: - "7002:6379" networks: redis-net: ipv4_address: 172.20.0.11 slave2: image: redis:6.2.6 container_name: redis-slave2 depends_on: - master volumes: - ./conf/slave2/redis.conf:/etc/redis/redis.conf - ./data/slave2:/data command: redis-server /etc/redis/redis.conf ports: - "7003:6379" networks: redis-net: ipv4_address: 172.20.0.12 networks: redis-net: driver: bridge ipam: config: - subnet: 172.20.0.0/16
1.3.2 主从数据同步原理
1.3.2.1 全量同步
主从第一次同步是全量同步:

Master 如何判断 slave 是不是第一次来同步数据?这里有两个概念:
Replication ld: 简称 replid, 是数据集的标记, id 一致则说明是同一数据集。每一个 master 都有唯一的 replid,slave 则会继承 master 节点的 replid
offset :偏移量,随着记录在 repl_baklog 中的数据增多而逐渐增大。 slave 完成同步时也会记录当前同步的 offset 。如果 slave 的 offset 小于 master 的 offset, 说明 slave 数据落后于 master, 需要更新。
因此salve做数据同步,必须向master 声明自己的 replication id 和 offset, master 才可以判断到底需要同步哪些数据。
所以master通过判断slave节点的 replication Id 是否和自己一致来判断 slave 节点是不是第一次来做数据同步。

总结,全量同步的流程:
- 从节点向主节点发送增量同步请求
- 主节点判断从节点的 replication ID 是否和自己的一致,判断是否是第一次同步
- 如果是第一次同步,主节点执行 bgsave ,把 RDB 文件发送给从节点
- 从节点清理掉本地数据,加载RDB文件
- 主节点将执行bgsave期间完成的写操作命令记录在repl_baklog中,发送给从节点
- 从节点执行这些命令,保持与 master 之间的同步
1.3.2.2 增量同步
增量同步过程

- 主节点会判断 replication id 是否和自己的一样,如果一样,则根据 offset 进行增量同步。
- repl_baklog 底层是实现是数组,master 和 slave 都有一个 offset,repl_baklog 大小有上限,写满后会覆盖最早的数据。进行增量同步时,主节点会判断当前自己的 offset 是否大于 slave 的offset,如果小于等于,则将 slave offset之后的命令发给 slave;如果大于,则进行全量同步。
可以从以下几个方面优化Redis主从集群:
优化全量同步
在 master 中配置 rep-diskless-sync yes 启用无磁盘复制,此时RDB文件就不会写入磁盘中,直接通过网络流发送给从节点,减少了磁盘IO时间。使用场景:磁盘IO比较慢,但是网络带宽比较快。
Redis单节点所占用内存不要过大,减少RDB导致的过多磁盘IO
减少全量同步
适当提高repl_baklog的大小(允许主从节点更大的数据差异),以及发现slave宕机时,尽快实现故障恢复(减少主从节点的数据差异),尽可能避免全量同步
限制一个 master 上的 slave 节点数量,如果实在是太多 slave ,则可以采用主一从一从链式结构,减少 master 压力,实现这种架构,只需要后面的从节点指定前面的从节点为主节点即可

1.3.2.3 全量同步与增量同步的总结
简述全量同步和增量同步区别?
全量同步:master 将完整内存数据生成 RDB, 发送 RDB 到 slave。后续命令则记录在 repl_baklog ,逐个发送给 slave。
增量同步:slave 提交自己的 offset 到 master, master 获取repl-baklog 中从 offset 之后的命令给 slave。
什么时候执行全量同步?
slave 节点第一次连接 master 节点时
slave 节点断开时间太久,repl-baklog 中的 offset 已经被覆盖时
什么时候执行增量同步?
- slave 节点断开又恢复,并且在 repl_baklog 中能找到 offset 时
1.4 Redis 哨兵
1.4.1 问题
slave节点宕机回复后可以找master节点同步数据,但是master节点宕机后,这期间的写操作就丢失了。
Redis提供了哨兵(Sentinel)机制来实现主从集群的自动故障恢复。
1.4.2 哨兵的作用和原理
哨兵的结构如下:

由哨兵集群监控Redis主从节点的状态,在Redis主节点出现故障后,自动选举新节点为主节点,并将状态的变化通知给客户端(使用者)
哨兵的作用:
- ==监控==:SentineI 会不断检查您的 master 和 slave是否按预期工作
- ==自动故障恢复==:如果 master 故障, Sentinel 会将一个 slave 提升为 master。当故障实例恢复后也以新的 master 为主
- ==通知==,Sentinel 充当 Redis 客户端的服务发现来源,当集群发生故障转移时,会将最新信息推送给 Redis 的客户端
哨兵==如何监控节点的服务状态==呢?
sentinel 基于心跳机制监测服务状态,每隔 1 秒向集群的每个实例发送 ping 命令
- 主观下线:如果某 sentinel 节点发现某实例未在规定时间响应,则认为该实例主观下线。
- 客观下线:若超过指定数量 (quorum) 的 sentinel 都认为该实例主观下线,则该实例客观下线。quorum 值最好超过 sentinel 实例数量的一半。
哨兵(sentinel)==以什么为依据选择新的 master== 呢?
一旦发现 master 故障,sentinel 需要在 salve 中选择一个作为新的 master, 选择依据是这样的:
- 首先会判断 slave 节点与 master 节点断开时间长短,如果超过指定值 (down-after-milliseconds *10 )则会排除该slave 节点
- 然后判断 slave 节点的 slave-priority 值,越小优先级越高,如果是 0 则永不参与选举
- 如果 slave-prority 一样,则判断 slave 节点的 offset 值,越大说明数据越新,优先级越高
- 最后是判断 slave 节点的运行 id 大小,越小优先级越高。
即选择的大体原则是:选择数据最新的slave节点作为主节点,优先保证数据的一致性。
如何实现==故障转移==?
当选中了其中一个 slave 为新的 master 后(例如 slave1),故障的转移的步骤如下:
- sentinel 给备选的 slave1 节点发送 slaveof no one 命令,让该节点成为 master。
- sentinel 给所有其它 slave 发送 slaveof 192.168.134.128 7002命令,让这些 slave 成为新 master 的从节点,开始从新的 master上同步数据。
- 最后,sentinel 将故障节点标记为 slave(修改该节点的配置文件), 当故障节点恢复后会自动成为新的 master 的 slave 节点。

1.4.3 搭建哨兵集群
参考:Redis集群
1.4.4 RedisTemplate的哨兵模式
配置哨兵
javaspring: redis: # 哨兵模式配置 sentinel: master: mymaster # 对应哨兵配置文件中的 master 名称 nodes: - 192.168.134.128:26379 # 哨兵1号的IP和端口 - 192.168.134.128:26380 # 哨兵2号 - 192.168.134.128:26381 # 哨兵3号 password: yourpassword # 如果设置了密码,主从和哨兵通常共用一个 database: 0 lettuce: # SpringBoot 2.x 默认使用 Lettuce 驱动 pool: max-active: 8 max-wait: -1ms配置读写分离
java@Bean public LettuceClientConfigurationBuilderCustomizer clientConfigurationBuilderCustomizer() { // REPLICA_PREFERRED: 优先从从节点读,如果从节点挂了再读主节点 return builder -> builder.readFrom(ReadFrom.REPLICA_PREFERRED); }这里的 ReadFrom 是配置 Redis 的读取策略, 是一个枚举,包括下面选择:
- MASTER :从主节点读取
- MASTER_PREFERRED: 优先从 master 节点读取, master 不可用才读取 replica
- REPLICA : 从 slave (replica) 节点读取
- REPLICA_REPLICA:优先从 slave (replica) 节点读取,所有的 slave 都不可用才读取 master
1.5 Redis分片集群
1.5.1 概念
分片集群(Cluster) 可以看作是 主从复制 + 哨兵(Sentinel) 的高级进化版。
分片集群解决数据量大的问题,传统的主从复制 + 哨兵的模式在数据量不断增大时,主节点的容量成为了瓶颈。分片集群通过插槽机制,将所有的数据分片存储到不同 Master 节点中,访问时,路由到指定节点。
为什么分片集群不需要额外的哨兵?
在“主从+哨兵”模式下,哨兵是一个独立的第三方代理,负责监控和拨乱反正。但在 分片集群(Redis Cluster) 中,这些功能被集成到了每一个数据节点中:
- 节点互监(去中心化监控):集群中的每个节点都通过 Gossip 协议 互相交换信息。它们不再需要专门的哨兵来告诉自己“谁挂了”,节点之间会互相检测对方的心跳。
- 自动故障转移:如果一个 Master 挂了,集群中其他的 Master 会共同投票,从挂掉节点的 Slave 中选出一个新的 Master。这套选举逻辑和哨兵非常相似,只是执行者变成了 Master 节点本身。
- 配置中心功能:哨兵模式下,客户端通过哨兵获取 Master 地址;而在分片集群中,客户端可以连接任意节点,如果数据不在该节点,它会返回一个
MOVED错误告诉客户端正确的去向。
客户端访问分片集群的流程
当一个客户端(比如你的 Spring Boot 应用)想要读写一个 Key 时,流程如下:
第一步:计算槽位 (Key -> Slot):客户端先对自己要操作的 Key 进行 CRC16 循环冗余校验,然后对 16384 取模,算出这个 Key 属于哪一个槽位。slot = CRC16(key) \pmod{16384}$$
第二步:查找节点 (Slot -> Node):客户端查看本地缓存的“映射表”,寻找这个槽位对应的节点 IP 和端口。
第三步:发送请求:客户端向目标节点发送请求。
第四步:节点判断与响应 (核心环节)
当节点收到请求后,它会先检查这个 Key 所属的槽位是否由自己负责:
- 如果在自己这里:直接执行命令,返回结果。
- 如果不在自己这里:
- 节点会查询自己内部记录的集群状态,找到真正负责该槽位的节点信息。
- 节点给客户端回一个特殊的错误响应:
MOVED [slot] [target_ip:port]。
第五步:客户端重定向
客户端收到
MOVED错误后,意识到自己本地的“映射表”过时了。它会做两件事:- 更新本地缓存:把该槽位与新节点的对应关系记下来。
- 重新发送:向
MOVED指向的新地址重新发送请求。
分片集群的代价
多 Key 操作受限 (Multi-key Operations)
这是开发中最头疼的地方。Redis 要求所有参与操作的 Key 必须在同一个哈希槽(Slot)中。
- 事务与批处理:如果你想用
MSET同时设置多个 Key,或者在MULTI/EXEC事务中操作多个 Key,如果这些 Key 被分配到了不同节点,操作会直接报错。 - Lua 脚本:同样,Lua 脚本中涉及的所有 Key 也必须在同一个节点上。
解决方案: 使用 Hash Tag。例如设置 Key 为
{user:100}info和{user:100}order,Redis 只会对{}里的内容哈希,确保它们落到同一个节点。- 事务与批处理:如果你想用
如果客户端访问的 Key 不在当前连接的节点上,Redis 会返回
MOVED或ASK错误。客户端必须能够解析这些错误,并自动重定向到正确的节点。
参考:Redis分片集群
1.5.2 散列插槽
1.5.2.1.插槽原理
Redis会把每一个master节点映射到0~16383共16384个插槽(hash slot)上,查看集群信息时就能看到:

数据key不是与节点绑定,而是与插槽绑定。redis会根据key的有效部分计算插槽值,分两种情况:
- key中包含"{}",且“{}”中至少包含1个字符,“{}”中的部分是有效部分
- key中不包含“{}”,整个key都是有效部分
例如:key是num,那么就根据num计算,如果是{itcast}num,则根据itcast计算。计算方式是利用CRC16算法得到一个hash值,然后对16384取余,得到的结果就是slot值。

如图,在7001这个节点执行set a 1时,对a做hash运算,对16384取余,得到的结果是15495,因此要存储到103节点。
到了7003后,执行get num时,对num做hash运算,对16384取余,得到的结果是2765,因此需要切换到7001节点
1.5.2.1.小结
Redis如何判断某个key应该在哪个实例?
- 将16384个插槽分配到不同的实例
- 根据key的有效部分计算哈希值,对16384取余
- 余数作为插槽,寻找插槽所在实例即可
如何将同一类数据固定的保存在同一个Redis实例?
- 这一类数据使用相同的有效部分,例如key都以{typeId}为前缀
1.5.3 集群伸缩
redis-cli --cluster提供了很多操作集群的命令,可以通过下面方式查看:

比如,添加节点的命令:

1.5.3.1.需求分析
需求:向集群中添加一个新的master节点,并向其中存储 num = 10
- 启动一个新的redis实例,端口为7004
- 添加7004到之前的集群,并作为一个master节点
- 给7004节点分配插槽,使得num这个key可以存储到7004实例
这里需要两个新的功能:
- 添加一个节点到集群中
- 将部分插槽分配到新插槽
1.5.3.2创建新的redis实例
创建一个文件夹:
mkdir 7004拷贝配置文件:
cp redis.conf /7004修改配置文件:
sed /s/6379/7004/g 7004/redis.conf启动
redis-server 7004/redis.conf1.5.3.3.添加新节点到redis
添加节点的语法如下:

执行命令:
redis-cli --cluster add-node 192.168.134.128:7004 192.168.134.128:7001通过命令查看集群状态:
redis-cli -p 7001 cluster nodes如图,7004加入了集群,并且默认是一个master节点:

但是,可以看到7004节点的插槽数量为0,因此没有任何数据可以存储到7004上
1.5.3.4.转移插槽
我们要将num存储到7004节点,因此需要先看看num的插槽是多少:

如上图所示,num的插槽为2765.
我们可以将0~3000的插槽从7001转移到7004,命令格式如下:

具体命令如下:
建立连接:

得到下面的反馈:

询问要移动多少个插槽,我们计划是3000个:
新的问题来了:

那个node来接收这些插槽??
显然是7004,那么7004节点的id是多少呢?

复制这个id,然后拷贝到刚才的控制台后:

这里询问,你的插槽是从哪里移动过来的?
- all:代表全部,也就是三个节点各转移一部分
- 具体的id:目标节点的id
- done:没有了
这里我们要从7001获取,因此填写7001的id:

填完后,点击done,这样插槽转移就准备好了:

确认要转移吗?输入yes:
然后,通过命令查看结果:

可以看到:

目的达成。
1.5.4 故障转移
集群初识状态是这样的:

其中7001、7002、7003都是master,我们计划让7002宕机。
1.5.4.1.自动故障转移
当集群中有一个master宕机会发生什么呢?
直接停止一个redis实例,例如7002:
redis-cli -p 7002 shutdown1)首先是该实例与其它实例失去连接
2)然后是疑似宕机:

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

4)当7002再次启动,就会变为一个slave节点了:

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

这种failover命令可以指定三种模式:
- 缺省:默认的流程,如图1~6歩
- force:省略了对offset的一致性校验
- takeover:直接执行第5歩,忽略数据一致性、忽略master状态和其它master的意见
案例需求:在7002这个slave节点执行手动故障转移,重新夺回master地位
步骤如下:
1)利用redis-cli连接7002这个节点
2)执行cluster failover命令
如图:

效果:

1.5.5 RedisTemplate访问分片集群
RedisTemplate底层同样基于lettuce实现了分片集群的支持,而使用的步骤与哨兵模式基本一致:
1)引入redis的starter依赖
2)配置分片集群地址
3)配置读写分离
与哨兵模式相比,其中只有分片集群的配置方式略有差异,如下:
spring:
redis:
cluster:
nodes:
- 192.168.134.128:7001
- 192.168.134.128:7002
- 192.168.134.128:7003
- 192.168.134.128:8001
- 192.168.134.128:8002
- 192.168.134.128:8003