🌇个人主页:平凡的小苏
📚学习格言:命运给你一个低的起点,是想看你精彩的翻盘,而不是让你自甘堕落,脚下的路虽然难走,但我还能走,比起向阳而生,我更想尝试逆风翻盘 。🛸Redis :Redis
> 家人们更新不易,你们的👍点赞👍和⭐关注⭐真的对我真重要,各位路 过的友友麻烦多多点赞关注。 欢迎你们的私信提问,感谢你们的转发! 关注我,关注我,关注我,你们将会看到更多的优质内容!!
一、持久化
redis
⽀持 RDB
和 AOF
两种持久化机制,持久化功能有效地避免因进程退出造成数据丢失问题,当下次重启时利⽤之前持久化的⽂件即可实现数据恢复。
1、RDB
RDB 持久化是把当前进程数据⽣成快照保存到硬盘的过程,触发 RDB 持久化过程分为⼿动触发和⾃动触发。
1.1、触发机制
⼿动触发分别对应 save
和 bgsave
命令:
• save
命令:阻塞当前 Redis 服务器,直到 RDB 过程完成为⽌,对于内存⽐较⼤的实例造成⻓时间阻塞,基本不采⽤。
• bgsave
命令:Redis 进程执⾏ fork 操作创建⼦进程,RDB 持久化过程由⼦进程负责,完成后⾃动结束。阻塞只发⽣在 fork 阶段,⼀般时间很短。
Redis 内部的所有涉及 RDB 的操作都采⽤类似 bgsave 的⽅式。
除了⼿动触发之外,Redis 运⾏⾃动触发 RDB 持久化机制,这个触发机制才是在实战中有价值的。
使⽤ save 配置。如 "save m n" 表⽰ m 秒内数据集发⽣了 n 次修改,⾃动 RDB 持久化。
从节点进⾏全量复制操作时,主节点⾃动进⾏ RDB 持久化,随后将 RDB ⽂件内容发送给从结点。
执⾏ shutdown 命令关闭 Redis 时,执⾏ RDB 持久化。
1.2、bgsave执行流程
-
执⾏ bgsave 命令,Redis ⽗进程判断当前进是否存在其他正在执⾏的⼦进程,如 RDB/AOF ⼦进程,如果存在 bgsave 命令直接返回。
-
⽗进程执⾏ fork 创建⼦进程,fork 过程中⽗进程会阻塞,通过 info stats 命令查看latest_fork_usec 选项,可以获取最近⼀次 fork 操作的耗时,单位为微秒。
-
⽗进程 fork 完成后,bgsave 命令返回 "Background saving started" 信息并不再阻塞⽗进程,可以继续响应其他命令。
-
⼦进程创建 RDB ⽂件,根据⽗进程内存⽣成临时快照⽂件,完成后对原有⽂件进⾏原⼦替换。执⾏ lastsave 命令可以获取最后⼀次⽣成 RDB 的时间,对应 info 统计的 rdb_last_save_time 选项。
-
进程发送信号给⽗进程表⽰完成,⽗进程更新统计信息。
1.3、RDB文件的处理
保存:RDB ⽂件保存再 dir 配置指定的⽬录(默认 /var/lib/redis/)下,⽂件名通过 dbfilename配置(默认 dump.rdb)指定。可以通过执⾏ config set dir {newDir} 和 config set dbfilename{newFilename} 运⾏期间动态执⾏,当下次运⾏时 RDB ⽂件会保存到新⽬录。
压缩:Redis 默认采⽤ LZF 算法对⽣成的 RDB ⽂件做压缩处理,压缩后的⽂件远远⼩于内存⼤⼩,默认开启,可以通过参数 config set rdbcompression {yes|no} 动态修改。
校验:如果 Redis 启动时加载到损坏的 RDB ⽂件会拒绝启动。这时可以使⽤ Redis 提供的 redis-check-dump ⼯具检测 RDB ⽂件并获取对应的错误报告。
1.4、RDB机制优缺点
-
RDB 是⼀个紧凑压缩的⼆进制⽂件,代表 Redis 在某个时间点上的数据快照。⾮常适⽤于备份,全量复制等场景。⽐如每 6 ⼩时执⾏ bgsave 备份,并把 RDB ⽂件复制到远程机器或者⽂件系统中(如 hdfs)⽤于灾备。
-
Redis 加载 RDB 恢复数据远远快于 AOF 的⽅式。
-
RDB ⽅式数据没办法做到实时持久化 / 秒级持久化。因为 bgsave 每次运⾏都要执⾏ fork 创建⼦进程,属于重量级操作,频繁执⾏成本过⾼。
-
RDB ⽂件使⽤特定⼆进制格式保存,Redis 版本演进过程中有多个 RDB 版本,兼容性可能有⻛险。
2、AOF
AOF(Append Only File)持久化:以独⽴⽇志的⽅式记录每次写命令,重启时再重新执⾏ AOF⽂件中的命令达到恢复数据的⽬的。AOF 的主要作⽤是解决了数据持久化的实时性,⽬前已经是Redis 持久化的主流⽅式。理解掌握好 AOF 持久化机制对我们兼顾数据安全性和性能⾮常有帮助。
2.1、使用AOF
开启 AOF
功能需要设置配置:appendonly yes
,默认不开启。AOF
⽂件名通过appendfilename
配置(默认是 appendonly.aof
)设置。保存⽬录同 RDB 持久化⽅式⼀致,通过 dir
配置指定。AOF
的⼯作流程操作:命令写⼊(append
)、⽂件同步(sync
)、⽂件重写(rewrite
)、重启加载(load
)。
工作流程
所有的写⼊命令会追加到 aof_buf(缓冲区)中。
AOF 缓冲区根据对应的策略向硬盘做同步操作。
随着 AOF ⽂件越来越⼤,需要定期对 AOF ⽂件进⾏重写,达到压缩的⽬的。
当 Redis 服务器启动时,可以加载 AOF ⽂件进⾏数据恢复
2.2、AOF是否会影响性能
1、AOF机制并非是直接让工作线程把数据直接写入硬盘,而是先写入一个内存中的缓冲区,再统一写入硬盘。
2、硬盘上读写数据,顺序读写的速度是比较快的。AOF是进行顺序写入的,所以对性能没有多大的影响
2.3、AOF的刷新策略
可配置值 | 说明 |
---|---|
always | 命令写入aof_buf后调用fsync同步,完成后返回 |
eyerysec | 命令写⼊aof_buf 后只执⾏ write 操作,不进⾏fsync。每秒由同步线程进⾏ fsync。 |
no | 命令写⼊ aof_buf 后只执⾏ write 操作,由 OS 控制fsync频率 |
系统调⽤ write 和 fsync 说明:
write 操作会触发延迟写(delayed write)机制。Linux 在内核提供⻚缓冲区⽤来提供硬盘 IO 性 能。write 操作在写⼊系统缓冲区后⽴即返回。同步硬盘操作依赖于系统调度机制,例如:缓冲区⻚空间写满或达到特定时间周期。同步⽂件之前,如果此时系统故障宕机,缓冲区内数据将丢失。
Fsync 针对单个⽂件操作,做强制硬盘同步,fsync 将阻塞直到数据写⼊到硬盘。
配置为 always 时,每次写⼊都要同步 AOF ⽂件,性能很差,在⼀般的 SATA 硬盘上,只能⽀持⼤约⼏百 TPS 写⼊。除⾮是⾮常重要的数据,否则不建议配置。
配置为 no 时,由于操作系统同步策略不可控,虽然提⾼了性能,但数据丢失⻛险⼤增,除⾮数据 重要程度很低,⼀般不建议配置。
配置为 everysec,是默认配置,也是推荐配置,兼顾了数据安全性和性能。理论上最多丢失 1 秒的数据。
2.4、AOF重写机制
随着命令不断写⼊ AOF,⽂件会越来越⼤。
重写后的 AOF 为什么可以变⼩?有如下原因:
• 进程内已超时的数据不再写⼊⽂件。
• 旧的 AOF 中的⽆效命令,例如 del、hdel、srem 等重写后将会删除,只需要保留数据的最终版本。
• 多条写操作合并为⼀条,例如 lpush list a、lpush list b、lpush list 从可以合并为 lpush list a b c。
较⼩的 AOF ⽂件⼀⽅⾯降低了硬盘空间占⽤,⼀⽅⾯可以提升启动 Redis 时数据恢复的速度。
AOF 重写过程可以⼿动触发和⾃动触发
-
⼿动触发:调⽤ bgrewriteaof 命令。
-
⾃动触发 :根据
auto-aof-rewrite-min-size
和auto-aof-rewrite-percentage
参数确定⾃动触发时机。auto-aof-rewrite-min-size
:表⽰触发重写时 AOF 的最⼩⽂件⼤⼩,默认为 64MB。auto-aof-rewrite-percentage
:代表当前 AOF 占⽤⼤⼩相⽐较上次重写时增加的⽐例。
AOF重写执行流程:
执⾏ AOF 重写请求。
如果当前进程正在执⾏ AOF 重写,请求不执⾏。如果当前进程正在执⾏ bgsave 操作,重写命令
延迟到 bgsave 完成之后再执⾏。
⽗进程执⾏ fork 创建⼦进程。
重写
a. 主进程 fork 之后,继续响应其他命令。所有修改操作写⼊ AOF 缓冲区并根据 appendfsync 策
略同步到硬盘,保证旧 AOF ⽂件机制正确。
b. ⼦进程只有 fork 之前的所有内存信息,⽗进程中需要将 fork 之后这段时间的修改操作写⼊
AOF 重写缓冲区中。
⼦进程根据内存快照,将命令合并到新的 AOF ⽂件中。
⼦进程完成重写
a. 新⽂件写⼊后,⼦进程发送信号给⽗进程。
b. ⽗进程把 AOF重写缓冲区内临时保存的命令追加到新 AOF ⽂件中。
c. ⽤新 AOF ⽂件替换⽼ AOF ⽂件。
问题:
1、如果,再执行bgrewriteaof的时候,此时redis已经正在进行aof重写了,会怎么样?
此时,不会再次执行哦aof重写,会直接返回了
2、如果,再执行bgrewriteaof的时候,发现redis再生成rdb文件的快照,会怎么样?
此时,aof重写操作就会等待,等待rdb的快照生成完毕之后,再次执行aof重写
二、事务
mysqy与redis事物的区别
• 弱化的原⼦性: redis 没有 "回滚机制". 只能做到这些操作 "批量执⾏". 不能做到 "⼀个失败就恢复到初始状态".
• 不保证⼀致性: 不涉及 "约束". 也没有回滚. MySQL 的⼀致性体现的是运⾏事务前和运⾏后 , 结果都是合理有效的, 不会出现中间⾮法状态.
• 不需要隔离性: 也没有隔离级别, 因为不会并发执⾏事务 (redis 单线程处理请求) .
• 不需要持久性: 是保存在内存的. 是否开启持久化, 是redis-server ⾃⼰的事情, 和事务⽆关.
Redis的事物:
Redis 事务本质上是在服务器上搞了⼀个 "事务队列". 每次客⼾端在事务中进⾏⼀个操作, 都会把命令先发给服务器, 放到 "事务队列" 中(但是并不会⽴即执⾏)⽽是会在真正收到 EXEC 命令之后, 才真正执⾏队列中的所有操作.
因此, Redis 的事务的功能相⽐于 MySQL 来说, 是弱化很多的. 只能保证事务中的这⼏个操作是 "连续的", 不会被别的客⼾端 "加塞", 仅此⽽已.
事务操作
1、multi
开启⼀个事务. 执⾏成功返回 OK
实例:
127.0.0.1:6379> MULTI
OK
2、exec
真正执⾏事务
127.0.0.1:6379> MULTI
OK
127.0.0.1:6379> set k1 1
QUEUED
127.0.0.1:6379> set k2 2
QUEUED
127.0.0.1:6379> set k3 3
QUEUED
127.0.0.1:6379> EXEC
1) OK
2) OK
3) OK
每次添加⼀个操作, 都会提⽰ "QUEUED", 说明命令已经进⼊客⼾端的队列了.真正执⾏ EXEC 的时候, 客⼾端才会真正把上述操作发送给服务器.此时就可以获取到上述 key 的值了
127.0.0.1:6379> get k1
"1"
127.0.0.1:6379> get k2
"2"
127.0.0.1:6379> get k3
"3"
3、discard
放弃当前事务. 此时直接清空事务队列. 之前的操作都不会真正执⾏到.
127.0.0.1:6379> MULTI
OK
127.0.0.1:6379> set k1 1
QUEUED
127.0.0.1:6379> set k2 2
QUEUED
127.0.0.1:6379> DISCARD
OK
127.0.0.1:6379> get k1
(nil)
127.0.0.1:6379> get k2
(nil)
4、watch
在执⾏事务的时候, 如果某个事务中修改的值, 被别的客⼾端修改了, 此时就容易出现数据不⼀致的问题.
实例
# 客⼾端1 先执⾏
127.0.0.1:6379> MULTI
OK
127.0.0.1:6379> set key 100
QUEUED
# 客⼾端2 再执⾏
127.0.0.1:6379> set key 200
OK
# 客⼾端1 最后执⾏
127.0.0.1:6379> EXEC
1) OK
此时, key 的值是多少呢??
从输⼊命令的时间看, 是客⼾端1 先执⾏的 set key 100. 客⼾端2 后执⾏的 set key 200.但是从实际的执⾏时间看, 是客⼾端2 先执⾏的, 客⼾端1 后执⾏的.
127.0.0.1:6379> get key
"100"
这个时候, 其实就容易引起歧义.
因此, 即使不保证严格的隔离性, ⾄少也要告诉⽤⼾, 当前的操作可能存在⻛险.
watch 命令就是⽤来解决这个问题的. watch 在该客⼾端上监控⼀组具体的 key.
• 当开启事务的时候, 如果对 watch 的 key 进⾏修改, 就会记录当前 key 的 "版本号". (版本号是个简单的整数, 每次修改都会使版本变⼤. 服务器来维护每个 key 的版本号情况)
• 在真正提交事务的时候, 如果发现当前服务器上的 key 的版本号已经超过了事务开始时的版本号, 就会让事务执⾏失败. (事务中的所有操作都不执⾏)。
实例:
客⼾端1 先执⾏
127.0.0.1:6379> watch k1 # 开始监控 k1
OK
127.0.0.1:6379> MULTI
OK
127.0.0.1:6379> set k1 100 # 进⾏修改, 从服务器获取 k1 的版本号是 0. 记录 k1 的版
QUEUED
127.0.0.1:6379> set k2 1000
QUEUED
只是⼊队列, 但是不提交事务执⾏.
客⼾端2 再执⾏
127.0.0.1:6379> set k1 200 # 修改成功, 使服务器端的 k1 的版本号 0 -> 1
OK
客⼾端1 再执⾏
127.0.0.1:6379> EXEC # 真正执⾏修改操作, 此时对⽐版本发现, 客⼾端的 k1 的版本
(nil)
127.0.0.1:6379> get k1
"200"
127.0.0.1:6379> get k2
(nil)
此时说明事务已经被取消了. 这次提交的所有命令都没有执⾏
5、unwatch
取消对 key 的监控.相当于 WATCH 的逆操作.
三、主从复制
在分布式系统中为了解决单点问题,通常会把数据复制多个副本部署到其他服务器,满⾜故障恢复和负载均衡等需求。Redis 也是如此,它为我们提供了复制的功能,实现了相同数据的多个 Redis 副本。复制功能是⾼可⽤ Redis 的基础,哨兵和集群都是在复制的基础上构建的。
1、配置
建立复制
参与复制的 Redis 实例划分为主节点(master)和从节点(slave)。每个从结点只能有⼀个主节点,⽽⼀个主节点可以同时具有多个从结点。复制的数据流是单向的,只能由主节点到从节点。配置复制的⽅式有以下三种:
-
在配置⽂件中加⼊ slaveof {masterHost} {masterPort} 随 Redis 启动⽣效。
-
在 redis-server 启动命令时加⼊ --slaveof {masterHost} {masterPort} ⽣效。
-
直接使⽤ redis 命令:slaveof {masterHost} {masterPort} ⽣效。
注意:redis配置文件修改完成之后,要重启服务器才能生效
第一种重启方式:使用kill命令,这种停止方式是和我们搭配之前直接运行redis-server的方式搭配的
第二种重启方式:使用service redis-server start这种方式启动,就必须使用service redis-server stop来进行停止
接下来,我们将 redis.conf 配置⽂件复制⼀份 redis-slave.conf,并且修改其 daemonize 为 yes。
# By default Redis does not run as a daemon. Use 'yes' if you need it.
# Note that Redis will write a pid file in /var/run/redis.pid when daemonized.
daemonize yes
接下来,默认启动的 redis 作为主 Redis,重新通过命令⾏启动⼀个 Redis 实例作为从 Redis:
观察主从关系
从节点只能读取数据,而不能写入数据,如果我们尝试使用从节点数据写入数据则会报错
2、Redis 主从节点复制过程
可以通过 info replication 命令查看复制相关状态。
3、断开主从关系
slaveof 命令不但可以建⽴复制,还可以在从节点执⾏ slaveof no one 来断开与主节点复制关系。
断开复制主要流程:
1)断开与主节点复制关系。
2)从节点晋升为主节点。
从节点断开复制后并不会抛弃原有数据,只是⽆法再获取主节点上的数据变化。
注意:这样的修改是临时的,如果重启服务器,它们还会是原来的主从结构。想要永久修改,还是需要修改配置文件
通过 slaveof 命令还可以实现切主操作,将当前从节点的数据源切换到另⼀个主节点。执⾏slaveof {newMasterIp} {newMasterPort} 命令即可。
切主操作主要流程:
1)断开与旧主节点复制关系。
2)与新主节点建⽴复制关系。
3)删除从节点当前所有数据。
4)从新主节点进⾏复制操作。
4、安全性
对于数据⽐较重要的节点,主节点会通过设置 requirepass
参数进⾏密码验证,这时所有的客⼾端访问必须使⽤ auth 命令实⾏校验。从节点与主节点的复制连接是通过⼀个特殊标识的客⼾端来完成,因此需要配置从节点的masterauth 参数与主节点密码保持⼀致,这样从节点才可以正确地连接到主节点并发起复制流程。
5、只读
默认情况下,从节点使⽤ slave-read-only=yes
配置为只读模式。由于复制只能从主节点到从节点,对于从节点的任何修改主节点都⽆法感知,修改从节点会造成主从数据不⼀致。所以建议线上不要修改从节点的只读模式。
6、传输延迟
主从节点⼀般部署在不同机器上,复制时的⽹络延迟就成为需要考虑的问题,Redis 为我们提供了 repl-disable-tcp-nodelay 参数⽤于控制是否关闭 TCP_NODELAY,默认为 no,即开启 tcpnodelay 功能,说明如下:
-
当关闭时,主节点产⽣的命令数据⽆论⼤⼩都会及时地发送给从节点,这样主从之间延迟会变⼩,但增加了⽹络带宽的消耗。适⽤于主从之间的⽹络环境良好的场景,如同机房部署。
-
当开启时,主节点会合并较⼩的 TCP 数据包从⽽节省带宽。默认发送时间间隔取决于 Linux 的内核,⼀般默认为 40 毫秒。这种配置节省了带宽但增⼤主从之间的延迟。适⽤于主从⽹络环境复杂的场景,如跨机房部署。
四、主从复制拓扑结构
Redis 的复制拓扑结构可以⽀持单层或多层复制关系,根据拓扑复杂性可以分为以下三种:⼀主⼀从、⼀主多从、树状主从结构。
1、一主一从结构
⼀主⼀从结构是最简单的复制拓扑结构,⽤于主节点出现宕机时从节点提供故障转移⽀持 ,如图所⽰。当应⽤写命令并发量较⾼且需要持久化 时,可以只在从节点上开启 AOF ,这样既可以保证数据安全性同时也避免了持久化对主节点的性能⼲扰。但需要注意的是,当主节点关闭持久化功能时,如果主节点宕机要避免⾃动重启操作。
2、一主多从结构
⼀主多从结构(星形结构)使得应⽤端可以利⽤多个从节点实现读写分离,如图 5-3 所⽰。对于读⽐重较⼤的场景,可以把读命令负载均衡到不同的从节点上来分担压⼒。同时⼀些耗时的读命令可以指定⼀台专⻔的从节点执⾏,避免破坏整体的稳定性。对于写并发量较⾼的场景,多个从节点会导致主节点写命令的多次发送从⽽加重主节点的负载
3、树形主从结构
树形主从结构(分层结构)使得从节点不但可以复制主节点数据,同时可以作为其他从节点的主节点继续向下层复制。通过引⼊复制中间层,可以有效降低住系欸按负载和需要传送给从节点的数据量,如图所⽰。数据写⼊节点 A 之后会同步给 B 和 C 节点,B 节点进⼀步把数据同步给 D 和 E 节点。当主节点需要挂载等多个从节点时为了避免对主节点的性能⼲扰,可以采⽤这种拓扑结构。
五、主从复制原理
1)保存主节点(master)的信息。
2)从节点(slave)内部通过每秒运⾏的定时任务维护复制相关逻辑,当定时任务发现存在新的主节点后,会尝试与主节点建⽴基于 TCP 的⽹络连接。如果从节点⽆法建⽴连接,定时任务会⽆限重试直到连接成功或者⽤⼾停⽌主从复制。
3)发送 ping 命令。连接建⽴成功之后,从节点通过 ping 命令确认主节点在应⽤层上是⼯作良好的。如果 ping 命令的结果 pong 回复超时,从节点会断开 TCP 连接,等待定时任务下次重新建⽴连接。
4)权限验证。如果主节点设置了 requirepass 参数,则需要密码验证,从节点通过配置 masterauth参数来设置密码。如果验证失败,则从节点的复制将会停⽌。
5)同步数据集。对于⾸次建⽴复制的场景,主节点会把当前持有的所有数据全部发送给从节点,这步操作基本是耗时最⻓的,所以⼜划分称两种情况:全量同步和部分同步,下⼀节重点介绍。
6)命令持续复制。当从节点复制了主节点的所有数据之后,针对之后的修改命令,主节点会持续的把命令发送给从节点,从节点执⾏修改命令,保证主从数据的⼀致性。
1、数据同步psync
Redis 使⽤ psync 命令完成主从数据同步,同步过程分为:全量复制和部分复制。
• 全量复制:⼀般⽤于初次复制场景,Redis 早期⽀持的复制功能只有全量复制,它会把主节点全部数据⼀次性发送给从节点,当数据量较⼤时,会对主从节点和⽹络造成很⼤的开销。
• 部分复制:⽤于处理在主从复制中因⽹络闪断等原因造成的数据丢失场景,当从节点再次连上主节点后,如果条件允许,主节点会补发数据给从节点。因为补发的数据远⼩于全量数据,可以有效避免全量复制的过⾼开销。
PSYNC 的语法格式
PSYNC replicationid offset
如果 replicationid 设为 ? 并且 offset 设为 -1 此时就是在尝试进⾏全量复制.如果 replicationid offset 设为了具体的数值, 则是尝试进⾏部分复制.
- replicationid/replid (复制id)
主节点的复制 id. 主节点重新启动, 或者从节点晋级成主节点, 都会⽣成⼀个 replicationid. (同⼀个节点, 每次重启, ⽣成的 replicationid 也会变化).从节点在和主节点建⽴连接之后, 就会获取到主节点的 replicationid.通过 info replication 即可看到 replicationid
127.0.0.1:6379> info replication
# Replication
role:master
connected_slaves:0
master_replid:1da596acecf5a34b4b2aae45bd35be785691ae69
master_replid2:0000000000000000000000000000000000000000
master_repl_offset:0
second_repl_offset:-1
repl_backlog_active:0
repl_backlog_size:1048576
repl_backlog_first_byte_offset:0
repl_backlog_histlen:0
关于 master_replid 和 master_replid2
每个节点需要记录两组 master_replid . 这个设定解决的问题场景是这样的:
⽐如当前有两个节点 A 和 B, A 为 master, B 为 slave.
此时 B 就会记录 A 的 master_replid.
如果⽹络出现抖动, B 以为 A 挂了, B ⾃⼰就会成为主节点. 于是 B 给⾃⼰分配了新的 master_replid.
此时就会使⽤ master_replid2 来保存之前 A 的 master_replid.
• 后续如果⽹络恢复了, B 就可以根据 master_replid2 找回之前的主节点.
• 后续如果⽹络没有恢复, B 就按照新的 master_replid ⾃成⼀派, 继续处理后续的数据.
- offset (偏移量)
参与复制的主从节点都会维护⾃⾝复制偏移量。通过对⽐主从节点的复制偏移量,可以判断主从节点数据是否⼀致。
2、psync 运⾏流程
1)从节点发送 psync 命令给主节点,replid 和 offset 的默认值分别是 ? 和 -1.
2)主节点根据 psync 参数和⾃⾝数据情况决定响应结果:
• 如果回复 +FULLRESYNC replid offset,则从节点需要进⾏全量复制流程。
• 如果回复 +CONTINEU,从节点进⾏部分复制流程。
• 如果回复 -ERR,说明 Redis 主节点版本过低,不⽀持 psync 命令。从节点可以使⽤ sync 命令进⾏全量复制。
• psync ⼀般不需要⼿动执⾏. Redis 会在主从复制模式下⾃动调⽤执⾏.
• sync 会阻塞 redis server 处理其他请求. psync 则不会.
3、全量复制
全量复制是 Redis 最早⽀持的复制⽅式,也是主从第⼀次建⽴复制时必须经历的阶段。全量复制的运⾏流程如图所⽰。
1)从节点发送 psync 命令给主节点进⾏数据同步,由于是第⼀次进⾏复制,从节点没有主节点的运⾏ ID 和复制偏移量,所以发送 psync ? -1。
2)主节点根据命令,解析出要进⾏全量复制,回复 +FULLRESYNC 响应。
3)从节点接收主节点的运⾏信息进⾏保存。
4)主节点执⾏ bgsave 进⾏ RDB ⽂件的持久化。
5)从节点发送 RDB ⽂件给从节点,从节点保存 RDB 数据到本地硬盘。
6)主节点将从⽣成 RDB 到接收完成期间执⾏的写命令,写⼊缓冲区中,等从节点保存完 RDB ⽂件后,主节点再将缓冲区内的数据补发给从节点,补发的数据仍然按照 rdb 的⼆进制格式追加写⼊到收到的 rdb ⽂件中. 保持主从⼀致性。
7)从节点清空⾃⾝原有旧数据。
8)从节点加载 RDB ⽂件得到与主节点⼀致的数据。
9)如果从节点加载 RDB 完成之后,并且开启了 AOF 持久化功能,它会进⾏ bgrewrite 操作,得到最近的 AOF ⽂件。
4、部分复制
部分复制主要是 Redis 针对全量复制的过⾼开销做出的⼀种优化措施,使⽤ psync replicationId offset 命令实现。当从节点正在复制主节点时,如果出现⽹络闪断或者命令丢失等异常情况时,从节点会向主节点要求补发丢失的命令数据,如果主节点的复制积压缓冲区存在数据则直接发送给从节点,这样就可以保持主从节点复制的⼀致性。补发的这部分数据⼀般远远⼩于全量数据,所以开销很⼩。整体流程如图所⽰。
1)当主从节点之间出现⽹络中断时,如果超过 repl-timeout 时间,主节点会认为从节点故障并终端复制连接。
2)主从连接中断期间主节点依然响应命令,但这些复制命令都因⽹络中断⽆法及时发送给从节点,所以暂时将这些命令滞留在复制积压缓冲区中。
3)当主从节点⽹络恢复后,从节点再次连上主节点。
4)从节点将之前保存的 replicationId 和 复制偏移量作为 psync 的参数发送给主节点,请求进⾏部分复制。
5)主节点接到 psync 请求后,进⾏必要的验证。随后根据 offset 去复制积压缓冲区查找合适的数据,并响应 +CONTINUE 给从节点。
6)主节点将需要从节点同步的数据发送给从节点,最终完成⼀致性。
5、实时复制
主从节点在建⽴复制连接后,主节点会把⾃⼰收到的 修改操作 , 通过 tcp ⻓连接的⽅式, 源源不断的传输给从节点. 从节点就会根据这些请求来同时修改⾃⾝的数据. 从⽽保持和主节点数据的⼀致性.
另外, 这样的⻓连接, 需要通过⼼跳包的⽅式来维护连接状态. (这⾥的⼼跳是指应⽤层⾃⼰实现的⼼跳,⽽不是 TCP ⾃带的⼼跳).
1)主从节点彼此都有⼼跳检测机制,各⾃模拟成对⽅的客⼾端进⾏通信。
2)主节点默认每隔 10 秒对从节点发送 ping 命令,判断从节点的存活性和连接状态。
3)从节点默认每隔 1 秒向主节点发送 replconf ack {offset} 命令,给主节点上报⾃⾝当前的复制偏移量。如果主节点发现从节点通信延迟超过 repl-timeout 配置的值(默认 60 秒),则判定从节点下线,断开复制客⼾端连接。从节点恢复连接后,⼼跳机制继续进⾏。
6、关于redis主节点无法重启的问题
我们曾经redis server用的是同一个aof文件(不太科学)
之前最开始创建从节点的配置文件没有改appendfilename,导致生成aof的文件路劲和文件名都是同一个
解决方案:
把三个redis服务器生成的文件,给区分开更靠谱的是,直接把三个redis服务器的工作目录区分开(修改配置文件的dir)
1.停止之前的redis服务器
2.删除之前工作目录下已经生成的aof文件,或者也可以通过chown命令修改aof 文件所属的用户
3.给从节点创建新的目录
六、哨兵
Redis 的主从复制模式下,⼀旦主节点由于故障不能提供服务,需要⼈⼯进⾏主从切换,同时⼤量的客⼾端需要被通知切换到新的主节点上,对于上了⼀定规模的应⽤来说,这种⽅案是⽆法接受的,于是 Redis 从 2.8 开始提供了 Redis Sentinel(哨兵)加个来解决这个问题。
1、基本概念
由于对 Redis 的许多概念都有不同的名词解释,所以在介绍 Redis Sentinel 之前,先对⼏个名词概念进⾏必要的说明。
2、主从复制的问题
Redis 的主从复制模式可以将主节点的数据改变同步给从节点,这样从节点就可以起到两个作⽤:
第⼀,作为主节点的⼀个备份,⼀旦主节点出了故障不可达的情况,从节点可以作为后备 "顶" 上来,并且保证数据尽量不丢失(主从复制表现为最终⼀致性)。第⼆,从节点可以分担主节点上的读压⼒,让主节点只承担写请求的处理,将所有的读请求负载均衡到各个从节点上。但是主从复制模式并不是万能的,它同样遗留下以下⼏个问题:
-
主节点发⽣故障时,进⾏主备切换的过程是复杂的,需要完全的⼈⼯参与,导致故障恢复时间⽆法保障。
-
主节点可以将读压⼒分散出去,但写压⼒/存储压⼒是⽆法被分担的,还是受到单机的限制。
其中第⼀个问题是⾼可⽤问题,即 Redis 哨兵主要解决的问题。第⼆个问题是属于存储分布式的问题,留给 Redis 集群去解决,本章我们集中讨论第⼀个问题。
3、哨兵自动恢复主节点故障
当主节点出现故障时,Redis Sentinel 能⾃动完成故障发现和故障转移,并通知应⽤⽅,从⽽实现真正的⾼可⽤。Redis Sentinel 是⼀个分布式架构,其中包含若⼲个 Sentinel 节点和 Redis 数据节点,每个Sentinel 节点会对数据节点和其余 Sentinel 节点进⾏监控,当它发现节点不可达时,会对节点做下线表⽰。如果下线的是主节点,它还会和其他的 Sentinel 节点进⾏ "协商",当⼤多数 Sentinel 节点对主节点不可达这个结论达成共识之后,它们会在内部 "选举" 出⼀个领导节点来完成⾃动故障转移的⼯作,同时将这个变化实时通知给 Redis 应⽤⽅。整个过程是完全⾃动的,不需要⼈⼯介⼊。
Redis Sentinel 相⽐于主从复制模式是多了若⼲(建议保持奇数)Sentinel 节点⽤于实现监控数据节点,哨兵节点会定期监控所有节点(包含数据节点和其他哨兵节点)。
针对主节点故障的情况,故障转移流程⼤致如下:
1)主节点故障,从节点同步连接中断,主从复制停⽌。
2)哨兵节点通过定期监控发现主节点出现故障。哨兵节点与其他哨兵节点进⾏协商,达成多数认同主节点故障的共识。这步主要是防⽌该情况:出故障的不是主节点,⽽是发现故障的哨兵节点,该情况经常发⽣于哨兵节点的⽹络被孤⽴的场景下。
3)哨兵节点之间使⽤ Raft 算法选举出⼀个领导⻆⾊,由该节点负责后续的故障转移⼯作。
4)哨兵领导者开始执⾏故障转移:从节点中选择⼀个作为新主节点;让其他从节点同步新主节点;通知应⽤层转移到新主节点。
可以看出 Redis Sentinel 具有以下⼏个功能
• 监控: Sentinel 节点会定期检测 Redis 数据节点、其余哨兵节点是否可达。
• 故障转移: 实现从节点晋升(promotion)为主节点并维护后续正确的主从关系。
• 通知: Sentinel 节点会将故障转移的结果通知给应⽤⽅。
4、安装部署(基于docker)
- 安装 docker 和 docker-compose(有docker笔记)
docker-compose 的安装
yum install docker-compose
-
停⽌之前的 redis-server
停⽌ redis-server
service redis-server stop
停⽌ redis-sentinel 如果已经有的话.
service redis-sentinel stop
-
使⽤ docker 获取 redis 镜像
docker pull redis:5.0.9
5、编排redis主节点
1) 编写 docker-compose.yml
创建 /root/redis/docker-compose.yml
, 同时 cd 到 yml 所在⽬录中.
注意: docker 中可以通过容器名字, 作为 ip 地址, 进⾏相互之间的访问.
redis
version: '3.3'
services:
master:
image: 'redis:5.0.9'
container_name: redis-master
restart: always
command: redis-server --appendonly yes
ports:
- 6379:6379
slave1:
image: 'redis:5.0.9'
container_name: redis-slave1
restart: always
command: redis-server --appendonly yes --slaveof redis-master 6379
ports:
- 6380:6379
slave2:
image: 'redis:5.0.9'
container_name: redis-slave2
restart: always
command: redis-server --appendonly yes --slaveof redis-master 6379
ports:
- 6381:6379
2) 启动所有容器
docker-compose up -d
报错信息:
ERROR: In file './docker-compose.yml', service 'networks' must be a mapping not an array.
ERROR: In file './docker-compose.yml', service 'ports' must be a mapping not an array.
解决方法:
这类错误 一般是docker-compose.yml文件中的缩进格式不正确(个人建议缩进长度不要超过四个空格)
3) 查看运⾏⽇志
docker-compose logs
上述操作必须保证⼯作⽬录在 yml 的同级⽬录中, 才能⼯作
6、编排 redis-sentinel 节点
version: '3.3'
services:
sentinel1:
image: 'redis:5.0.9'
container_name: redis-sentinel-1
restart: always
command: redis-sentinel /etc/redis/sentinel.conf
volumes:
- ./sentinel1.conf:/etc/redis/sentinel.conf
ports:
- 26379:26379
sentinel2:
image: 'redis:5.0.9'
container_name: redis-sentinel-2
restart: always
command: redis-sentinel /etc/redis/sentinel.conf
volumes:
- ./sentinel2.conf:/etc/redis/sentinel.conf
ports:
- 26380:26379
sentinel3:
image: 'redis:5.0.9'
container_name: redis-sentinel-3
restart: always
command: redis-sentinel /etc/redis/sentinel.conf
volumes:
- ./sentinel3.conf:/etc/redis/sentinel.conf
ports:
- 26381:26379
networks:
default:
external:
name: redisdata_default
2) 创建配置⽂件
创建 sentinel1.conf sentinel2.conf sentinel3.conf . 三份⽂件的内容是完全相同的.
bind 0.0.0.0
port 26379
sentinel monitor redis-master redis-master 6379 2
sentinel down-after-milliseconds redis-master 1000
注意:此处,我们查看docker-compose logs时候,不认识redis-master。
redis-master相当于一个域名,docker会进行域名解析
我们开启docker时,三个redis-server是一个局域网
三个哨兵节点是另一个局域网,这两网络,不互通,所以报错
解决方案:可以使用docker-compose把此处的两组服务放到同一个局域网中
docker network ls列出当前docker中的局域网
我们再yml文件中添加如图所示:
注意:centos可能对中横杠敏感,所以我选择没有中横杠的来添加局域网
7、理解 sentinel monitor
sentinel monitor 主节点名 主节点ip 主节点端⼝ 法定票数
• 主节点名, 这个是哨兵内部⾃⼰起的名字.
• 主节点 ip, 部署 redis-master 的设备 ip. 此处由于是使⽤ docker, 可以直接写 docker 的容器名, 会被⾃动 DNS 成对应的容器 ip
• 主节点端⼝, 不解释.
• 法定票数, 哨兵需要判定主节点是否挂了. 但是有的时候可能因为特殊情况, ⽐如主节点仍然⼯作正常, 但是哨兵节点⾃⼰⽹络出问题了, ⽆法访问到主节点了. 此时就可能会使该哨兵节点认为主节点下线, 出现误判. 使⽤投票的⽅式来确定主节点是否真的挂了是更稳妥的做法. 需要多个哨兵都认为
主节点挂了, 票数 >= 法定票数 之后, 才会真的认为主节点是挂了.
8、重新选举
⼿动把 redis-master ⼲掉
docker stop redis-master
观察哨兵的⽇志
可以看到哨兵发现了主节点 sdown, 进⼀步的由于主节点宕机得票达到 3/2 , 达到法定得票, 于是master 被判定为 odown.
• 主观下线 (Subjectively Down, SDown): 哨兵感知到主节点没⼼跳了. 判定为主观下线.
• 客观下线 (Objectively Down, ODown): 多个哨兵达成⼀致意⻅, 才能认为 master 确实下线了.
注意:即使重启redis-master他也只会是从节点了,而不能重新变回主节点。因为主节点已经重新选举
9、结论
• Redis 主节点如果宕机, 哨兵会把其中的⼀个从节点, 提拔成主节点.
• 当之前的 Redis 主节点重启之后, 这个主节点被加⼊到哨兵的监控中, 但是只会被作为从节点使⽤.
七、哨兵重新选举原理(面试)
1) 主观下线
当 redis-master 宕机, 此时 redis-master 和三个哨兵之间的⼼跳包就没有了.此时, 站在三个哨兵的⻆度来看, redis-master 出现严重故障. 因此三个哨兵均会把 redis-master 判定为主观下线 (SDown)
2) 客观下线
此时, 哨兵 sentenal1, sentenal2, sentenal3 均会对主节点故障这件事情进⾏投票. 当故障得票数 >= 配置的法定票数之后,
3) 选举出哨兵的 leader
接下来需要哨兵把剩余的 slave 中挑选出⼀个新的 master. 这个⼯作不需要所有的哨兵都参与. 只需要选出个代表 (称为 leader), 由 leader 负责进⾏ slave 升级到 master 的提拔过程.
4)leader选举主节点
1.优先级 每个redis数据节点,都会在配置文件中,有一个优先级的设置,slave-priority优先级高的从节点,就会胜出
2.offset 最大,就胜出,offset从节点从主节点这边同步数据的进度,数值越大,说明从节点的数据和主节点越接近
3.runid 每个redis节点启动等的时候就会随机生成一串数字(大小全凭缘分,选谁都可以了)
把新的主节点指定好了之后,leader就会控制这个节点,执行slave no one,成为主节点在控制其他节点,执行slave of,让这些其他节点,以新的master作为主节点
1、注意事项
• 哨兵节点不能只有⼀个. 否则哨兵节点挂了也会影响系统可⽤性.
• 哨兵节点最好是奇数个. ⽅便选举 leader, 得票更容易超过半数.
• 哨兵节点不负责存储数据. 仍然是 redis 主从节点负责存储.
• 哨兵 + 主从复制解决的问题是 "提⾼可⽤性", 不能解决 "数据极端情况下写丢失" 的问题.
• 哨兵 + 主从复制不能提⾼数据的存储容量. 当我们需要存的数据接近或者超过机器的物理内存, 这样的结构就难以胜任了.为了能存储更多的数据, 就引⼊了集群.
八、集群
广义的集群,只要你是多个机器,构成了分布式系统,都可以称为一个"集群"。前面的主从结构,哨兵模式,可以称为"广义的集群"
狭义的集群,redis提供的集群模式,这个集群模式之下,主要是解决,存储空间不足的问题(拓展存储空间)
Redis 的集群就是在上述的思路之下, 引⼊多组 Master / Slave , 每⼀组 Master / Slave 存储数据全集的⼀部分, 从⽽构成⼀个更⼤的整体, 称为 Redis 集群 (Cluster).
假定整个数据全集是 1 TB, 引⼊三组 Master / Slave 来存储. 那么每⼀组机器只需要存储整个数据全集的 1/3 即可.
在上述图中,
• Master1 和 Slave11 和 Slave12 保存的是同样的数据. 占总数据的 1/3
• Master2 和 Slave21 和 Slave22 保存的是同样的数据. 占总数据的 1/3
• Master3 和 Slave31 和 Slave32 保存的是同样的数据. 占总数据的 1/3
这三组机器存储的数据都是不同的.
每个 Slave 都是对应 Master 的备份(当 Master 挂了, 对应的 Slave 会补位成 Master).
每个红框部分都可以称为是⼀个 分⽚ (Sharding).
如果全量数据进⼀步增加, 只要再增加更多的分⽚, 即可解决.
1、数据分片算法
1.1、哈希求余法
设有 N 个分⽚, 使⽤ [0, N-1] 这样序号进⾏编号.
针对某个给定的 key, 先计算 hash 值, 再把得到的结果 % N, 得到的结果即为分⽚编号.
例如, N 为 3. 给定 key 为 hello, 对 hello 计算 hash 值(⽐如使⽤ md5 算法), 得到的结果为bc4b2a76b9719d91 , 再把这个结果 % 3, 结果为 0, 那么就把 hello 这个 key 放到 0 号分⽚上.当然, 实际⼯作中涉及到的系统, 计算 hash 的⽅式不⼀定是 md5, 但是思想是⼀致的
后续如果要取某个 key 的记录, 也是针对 key 计算 hash , 再对 N 求余, 就可以找到对应的分⽚编号了.
优点: 简单⾼效, 数据分配均匀.
缺点: ⼀旦需要进⾏扩容, N 改变了, 原有的映射规则被破坏, 就需要让节点之间的数据相互传输, 重新排列, 以满⾜新的映射规则. 此时需要搬运的数据量是⽐较多的, 开销较⼤.
N 为 3 的时候, [100, 120] 这 21 个 hash 值的分布 (此处假定计算出的 hash 值是⼀个简单的整数, ⽅便⾁眼观察)
当引⼊⼀个新的分⽚, N 从 3 => 4 时, ⼤量的 key 都需要重新映射. (某个key % 3 和 % 4 的结果不⼀样,就映射到不同机器上了).
1.2、哈希槽分区算法 (Redis 使⽤)
为了解决上述问题 (搬运成本⾼ 和 数据分配不均匀), Redis cluster 引⼊了哈希槽 (hash slots) 算法.
hash_slot = crc16(key) % 16384
其中 crc16 也是⼀种 hash 算法.
16384 其实是 16 * 1024, 也就是 2^14.
相当于是把整个哈希值, 映射到 16384 个槽位上, 也就是 [0, 16383].
然后再把这些槽位⽐较均匀的分配给每个分⽚. 每个分⽚的节点都需要记录⾃⼰持有哪些分⽚.
假设当前有三个分⽚, ⼀种可能的分配⽅式:
• 0 号分⽚: [0, 5461], 共 5462 个槽位
• 1 号分⽚: [5462, 10923], 共 5462 个槽位
• 2 号分⽚: [10924, 16383], 共 5460 个槽位
如果需要进⾏扩容, ⽐如新增⼀个 3 号分⽚, 就可以针对原有的槽位进⾏重新分配.
⽐如可以把之前每个分⽚持有的槽位, 各拿出⼀点, 分给新分⽚.
⼀种可能的分配⽅式:
• 0 号分⽚: [0, 4095], 共 4096 个槽位
• 1 号分⽚: [5462, 9557], 共 4096 个槽位
• 2 号分⽚: [10924, 15019], 共 4096 个槽位
• 3 号分⽚: [4096, 5461] + [9558, 10923] + [15019, 16383], 共 4096 个槽位
1.3、问题(面试)
问题⼀: Redis 集群是最多有 16384 个分⽚吗?
并⾮如此. 如果⼀个分⽚只有⼀个槽位, 这对于集群的数据均匀其实是难以保证的.
实际上 Redis 的作者建议集群分⽚数不应该超过 1000.
⽽且, 16000 这么⼤规模的集群, 本⾝的可⽤性也是⼀个⼤问题. ⼀个系统越复杂, 出现故障的概率是越⾼的.
问题⼆: 为什么是 16384 个槽位?
• 节点之间通过⼼跳包通信. ⼼跳包中包含了该节点持有哪些 slots. 这个是使⽤位图这样的数据结构表⽰的. 表⽰ 16384 (16k) 个 slots, 需要的位图⼤⼩是 2KB. 如果给定的 slots 数更多了, ⽐如 65536个了, 此时就需要消耗更多的空间, 8 KB 位图表⽰了. 8 KB, 对于内存来说不算什么, 但是在频繁的⽹络⼼跳包中, 还是⼀个不⼩的开销的.
• 另⼀⽅⾯, Redis 集群⼀般不建议超过 1000 个分⽚. 所以 16k 对于最⼤ 1000 个分⽚来说是⾜够⽤的, 同时也会使对应的槽位配置位图体积不⾄于很⼤.
2、基于docker搭建集群
2.1、拓扑结构
2.2、创建目录和配置
创建 redis-cluster ⽬录. 内部创建两个⽂件
redis-cluster/
├── docker-compose.yml
└── generate.sh
generate.sh 内容如下
for port in $(seq 1 9); \
do \
mkdir -p redis${port}/
touch redis${port}/redis.conf
cat << EOF > redis${port}/redis.conf
port 6379
bind 0.0.0.0
protected-mode no
appendonly yes
cluster-enabled yes
cluster-config-file nodes.conf
cluster-node-timeout 5000
cluster-announce-ip 172.30.0.10${port}
cluster-announce-port 6379
cluster-announce-bus-port 16379
EOF
done
# 注意 cluster-announce-ip 的值有变化.
for port in $(seq 10 11); \
do \
mkdir -p redis${port}/
touch redis${port}/redis.conf
cat << EOF > redis${port}/redis.conf
port 6379
bind 0.0.0.0
protected-mode no
appendonly yes
cluster-enabled yes
cluster-config-file nodes.conf
cluster-node-timeout 5000
cluster-announce-ip 172.30.0.1${port}
cluster-announce-port 6379
cluster-announce-bus-port 16379
EOF
done
执⾏命令
bash generate.sh
配置说明
• cluster-enabled yes 开启集群.
• cluster-config-file nodes.conf 集群节点⽣成的配置.
• cluster-node-timeout 5000 节点失联的超时时间.
• cluster-announce-ip 172.30.0.101 节点⾃⾝ ip.
• cluster-announce-port 6379 节点⾃⾝的业务端⼝.
• cluster-announce-bus-port 16379 节点⾃⾝的总线端⼝. 集群管理的信息交互是通过这个端⼝进⾏的.
编写 docker-compose.yml
• 先创建 networks, 并分配⽹段为 172.30.0.0/24
• 配置每个节点. 注意配置⽂件映射, 端⼝映射, 以及容器的 ip 地址. 设定成固定 ip ⽅便后续的观察和操作.
此处的端⼝映射不配置也可以, 配置的⽬的是为了可以通过宿主机 ip + 映射的端⼝进⾏访问. 通过 容器⾃⾝ ip:6379 的⽅式也可以访问.
version: '3.3'
networks:
mynet:
ipam:
config:
- subnet: 172.30.0.0/24
services:
redis1:
image: 'redis:5.0.9'
container_name: redis1
restart: always
volumes:
- ./redis1/:/etc/redis/
ports:
- 6371:6379
- 16371:16379
command:
redis-server /etc/redis/redis.conf
networks:
mynet:
ipv4_address: 172.30.0.101
redis2:
image: 'redis:5.0.9'
container_name: redis2
restart: always
volumes:
- ./redis2/:/etc/redis/
ports:
- 6372:6379
- 16372:16379
command:
redis-server /etc/redis/redis.conf
networks:
mynet:
ipv4_address: 172.30.0.102
redis3:
image: 'redis:5.0.9'
container_name: redis3
restart: always
volumes:
- ./redis3/:/etc/redis/
ports:
- 6373:6379
- 16373:16379
command:
redis-server /etc/redis/redis.conf
networks:
mynet:
ipv4_address: 172.30.0.103
redis4:
image: 'redis:5.0.9'
container_name: redis4
restart: always
volumes:
- ./redis4/:/etc/redis/
ports:
- 6374:6379
- 16374:16379
command:
redis-server /etc/redis/redis.conf
networks:
mynet:
ipv4_address: 172.30.0.104
redis5:
image: 'redis:5.0.9'
container_name: redis5
restart: always
volumes:
- ./redis5/:/etc/redis/
ports:
- 6375:6379
- 16375:16379
command:
redis-server /etc/redis/redis.conf
networks:
mynet:
ipv4_address: 172.30.0.105
redis6:
image: 'redis:5.0.9'
container_name: redis6
restart: always
volumes:
- ./redis6/:/etc/redis/
ports:
- 6376:6379
- 16376:16379
command:
redis-server /etc/redis/redis.conf
networks:
mynet:
ipv4_address: 172.30.0.106
redis7:
image: 'redis:5.0.9'
container_name: redis7
restart: always
volumes:
- ./redis7/:/etc/redis/
ports:
- 6377:6379
- 16377:16379
command:
redis-server /etc/redis/redis.conf
networks:
mynet:
ipv4_address: 172.30.0.107
redis8:
image: 'redis:5.0.9'
container_name: redis8
restart: always
volumes:
- ./redis8/:/etc/redis/
ports:
- 6378:6379
- 16378:16379
command:
redis-server /etc/redis/redis.conf
networks:
mynet:
ipv4_address: 172.30.0.108
redis9:
image: 'redis:5.0.9'
container_name: redis9
restart: always
volumes:
- ./redis9/:/etc/redis/
ports:
- 6379:6379
- 16379:16379
command:
redis-server /etc/redis/redis.conf
networks:
mynet:
ipv4_address: 172.30.0.109
redis10:
image: 'redis:5.0.9'
container_name: redis10
restart: always
volumes:
- ./redis10/:/etc/redis/
ports:
- 6380:6379
- 16380:16379
command:
redis-server /etc/redis/redis.conf
networks:
mynet:
ipv4_address: 172.30.0.110
redis11:
image: 'redis:5.0.9'
container_name: redis11
restart: always
volumes:
- ./redis11/:/etc/redis/
ports:
- 6381:6379
- 16381:16379
command:
redis-server /etc/redis/redis.conf
networks:
mynet:
ipv4_address: 172.30.0.111
第三步: 启动容器
docker-compose up -d
注意:
启动之前,一定要把之前已经运行的redis都干掉!!!
否则就可能因为端口冲突等原因,导致启动失败
第四步: 构建集群
redis-cli --cluster create 172.30.0.101:6379 172.30.0.102:6379 172.30.0.103:6379 172.30.0.104:6379 172.30.0.105:6379 172.30.0.106:6379 172.30.0.107:6379 172.30.0.108:6379 172.30.0.109:6379 --cluster-replicas 2
解释:create表示创建集群,列出每个参与构建集群的ip和端口,端口都是写容器内部的端口号 --cluster replicas 2 描述集群的每个节点,应该是有2个从节点
此时, 使⽤客⼾端连上集群中的任何⼀个节点, 都相当于连上了整个集群.
• 客⼾端后⾯要加上 -c 选项, 否则如果 key 没有落到当前节点上, 是不能操作的. -c 会⾃动把请求重定向到对应节点.
• 使⽤ cluster nodes 可以查看到整个集群的情况
主节点宕机
⼿动停⽌⼀个 master 节点, 观察效果.
⽐如上述拓扑结构中, 可以看到 redis1 redis2 redis3 是主节点, 随便挑⼀个停掉.
docker stop redis1
连上 redis2 , 观察结果.
可以看到, 101 已经提⽰ fail, 然后 原本是 slave 的 105 成了新的 master.
如果重新启动 redis1
docker start redis1
再次观察结果. 可以看到 101 启动了, 仍然是 slave.
可以使⽤ cluster failover 进⾏集群恢复. 也就是把 101 重新设定成 master. (登录到 101 上执⾏)
3、处理流程
故障判定
集群中的所有节点, 都会周期性的使⽤⼼跳包进⾏通信.
节点 A 给 节点 B 发送 ping 包, B 就会给 A 返回⼀个 pong 包. ping 和 pong 除了 message type属性之外, 其他部分都是⼀样的. 这⾥包含了集群的配置信息(该节点的id, 该节点从属于哪个分⽚,是主节点还是从节点, 从属于谁, 持有哪些 slots 的位图...).
每个节点, 每秒钟, 都会给⼀些随机的节点发起 ping 包, ⽽不是全发⼀遍. 这样设定是为了避免在节点很多的时候, ⼼跳包也⾮常多(⽐如有 9 个节点, 如果全发, 就是 9 * 8 有 72 组⼼跳了, ⽽且这是按照 N^2 这样的级别增⻓的).
当节点 A 给节点 B 发起 ping 包, B 不能如期回应的时候, 此时 A 就会尝试重置和 B 的 tcp 连接, 看能否连接成功. 如果仍然连接失败, A 就会把 B 设为 PFAIL 状态(相当于主观下线).
A 判定 B 为 PFAIL 之后, 会通过 redis 内置的 Gossip 协议, 和其他节点进⾏沟通, 向其他节点确认 B的状态. (每个节点都会维护⼀个⾃⼰的 "下线列表", 由于视⻆不同, 每个节点的下线列表也不⼀定相同).
此时 A 发现其他很多节点, 也认为 B 为 PFAIL, 并且数⽬超过总集群个数的⼀半, 那么 A 就会把 B 标记成 FAIL (相当于客观下线), 并且把这个消息同步给其他节点(其他节点收到之后, 也会把 B 标记成FAIL).
⾄此, B 就彻底被判定为故障节点了
某个或者某些节点宕机, 有的时候会引起整个集群都宕机 (称为 fail 状态).
以下三种情况会出现集群宕机:
• 某个分⽚, 所有的主节点和从节点都挂了.
• 某个分⽚, 主节点挂了, 但是没有从节点.
• 超过半数的 master 节点都挂了.
故障迁移
上述例⼦中, B 故障, 并且 A 把 B FAIL 的消息告知集群中的其他节点.
• 如果 B 是从节点, 那么不需要进⾏故障迁移.
• 如果 B 是主节点, 那么就会由 B 的从节点 (⽐如 C 和 D) 触发故障迁移了.
所谓故障迁移, 就是指把从节点提拔成主节点, 继续给整个 redis 集群提供⽀持.
具体流程如下:
从节点判定⾃⼰是否具有参选资格. 如果从节点和主节点已经太久没通信(此时认为从节点的数据和主节点差异太⼤了), 时间超过阈值, 就失去竞选资格.
具有资格的节点, ⽐如 C 和 D, 就会先休眠⼀定时间. 休眠时间 = 500ms 基础时间 + [0, 500ms] 随机时间 + 排名 * 1000ms. offset 的值越⼤, 则排名越靠前(越⼩).
⽐如 C 的休眠时间到了, C 就会给其他所有集群中的节点, 进⾏拉票操作. 但是只有主节点才有投票资格.
主节点就会把⾃⼰的票投给 C (每个主节点只有 1 票). 当 C 收到的票数超过主节点数⽬的⼀半, C 就会晋升成主节点. (C ⾃⼰负责执⾏ slaveof no one, 并且让 D 执⾏ slaveof C).
同时, C 还会把⾃⼰成为主节点的消息, 同步给其他集群的节点. ⼤家也都会更新⾃⼰保存的集群结构信息.
4、集群扩容
扩容是⼀个在开发中⽐较常遇到的场景.
随着业务的发展, 现有集群很可能⽆法容纳⽇益增⻓的数据. 此时给集群中加⼊更多新的机器, 就可以使存储的空间更⼤了.
第⼀步: 把新的主节点加⼊到集群
上⾯已经把 redis1 - redis9 重新构成了集群. 接下来把 redis10 和 redis11 也加⼊集群.
此处我们把 redis10 作为主机, redis11 作为从机.
redis-cli --cluster add-node 172.30.0.110:6379 172.30.0.101:6379
第⼆步: 重新分配 slots
redis-cli --cluster reshard 172.30.0.101:6379
reshard 后的地址是集群中的任意节点地址.
另外, 注意单词拼写, 是 reshard (重新切分), 不是 reshared (重新分享) , 不要多写个 e.
执⾏之后, 会进⼊交互式操作, redis 会提⽰⽤⼾输⼊以下内容:
• 多少个 slots 要进⾏ reshard ? (此处我们填写 4096)
• 哪个节点来接收这些 slots ? (此处我们填写 172.30.0.110 这个节点的集群节点 id)
• 这些 slots 从哪些节点搬运过来? (此处我们填写 all, 表⽰从其他所有的节点都进⾏搬运)
执⾏结果如下
How many slots do you want to move (from 1 to 16384)? 4096
What is the receiving node ID? 522a1bd88a1a9084e6919fa88f4bf1c3655ad837
Please enter all the source node IDs.
Type 'all' to use all the nodes as source nodes for the hash slots.
Type 'done' once you entered all the source nodes IDs.
Source node #1: all
第三步: 给新的主节点添加从节点
光有主节点了, 此时扩容的⽬标已经初步达成. 但是为了保证集群可⽤性, 还需要给这个新的主节点添加从节点, 保证该主节点宕机之后, 有从节点能够顶上
redis-cli --cluster add-node 172.30.0.111:6379 172.30.0.101:6379 --cluster-slave
执⾏完毕后, 从节点就已经被添加完成了
注意:如果在搬运过程中,客户端能否访问咱们的redis集群?
搬运key,大部分的key是不用搬运的,针对这些未搬运的key,此时是可以正常访问的。针对这些正在搬运中的key,是有可能会出现访问出错的情况
假设客户端访问k1,集群通过分片算法,得到k1的第一个分片的数据。就会重定向给到第一个分片的节点,, 就可能在重定向过去之后,正好k1被搬走了,自然就无法访问了。
想要追求更高的可用性,让扩容对用户影响更小,就需要搞一组新的机器,重新搭建集群,并且把数据导入过来,使用新集群代替旧集群(成本最高)
九、缓存
缓存 (cache) 是计算机中的⼀个经典的概念. 在很多场景中都会涉及到.
核⼼思路就是把⼀些常⽤的数据放到触⼿可及(访问速度更快)的地⽅, ⽅便随时读取。
9.1、使用redis作为缓存
在⼀个⽹站中, 我们经常会使⽤关系型数据库 (⽐如 MySQL) 来存储数据.
关系型数据库虽然功能强⼤, 但是有⼀个很⼤的缺陷, 就是性能不⾼. (换⽽⾔之, 进⾏⼀次查询操作消耗的系统资源较多).
因此, 如果访问数据库的并发量⽐较⾼, 对于数据库的压⼒是很⼤的, 很容易就会使数据库服务器宕机.
如何让数据库能够承担更⼤的并发量呢? 核⼼思路主要是两个:
• 开源: 引⼊更多的机器, 部署更多的数据库实例, 构成数据库集群. (主从复制, 分库分表等...)
• 节流: 引⼊缓存, 使⽤其他的⽅式保存经常访问的热点数据, 从⽽降低直接访问数据库的请求数量.
9.2、缓存更新策略
接下来还有⼀个重要的问题, 到底哪些数据才是 "热点数据" 呢?
1) 定期⽣成
每隔⼀定的周期(⽐如⼀天/⼀周/⼀个⽉), 对于访问的数据频次进⾏统计. 挑选出访问频次最⾼的前 N%的数据.
这种做法实时性较低. 对于⼀些突然情况应对的并不好.
2) 实时⽣成
先给缓存设定容量上限(可以通过 Redis 配置⽂件的 maxmemory 参数设定).
接下来把⽤⼾每次查询:
• 如果在 Redis 查到了, 就直接返回.
• 如果 Redis 中不存在, 就从数据库查, 把查到的结果同时也写⼊ Redis.
如果缓存已经满了(达到上限), 就触发缓存淘汰策略, 把⼀些 "相对不那么热⻔" 的数据淘汰掉.
按照上述过程, 持续⼀段时间之后 Redis 内部的数据⾃然就是 "热⻔数据" 了.
通⽤的淘汰策略主要有以下⼏种:
FIFO (First In First Out) 先进先出
把缓存中存在时间最久的 (也就是先来的数据) 淘汰掉.
LRU (Least Recently Used) 淘汰最久未使⽤的
记录每个 key 的最近访问时间. 把最近访问时间最⽼的 key 淘汰掉.
LFU (Least Frequently Used) 淘汰访问次数最少的
记录每个 key 最近⼀段时间的访问次数. 把访问次数最少的淘汰掉.
Random 随机淘汰
从所有的 key 中抽取幸运⼉被随机淘汰掉.
Redis 内置的淘汰策略如下:
• volatile-lru 当内存不⾜以容纳新写⼊数据时,从设置了过期时间的key中使⽤LRU(最近最少使⽤)算法进⾏淘汰
• allkeys-lru 当内存不⾜以容纳新写⼊数据时,从所有key中使⽤LRU(最近最少使⽤)算法进⾏淘汰.
• volatile-lfu 4.0版本新增,当内存不⾜以容纳新写⼊数据时,在过期的key中,使⽤LFU算法进⾏删除key.
• allkeys-lfu 4.0版本新增,当内存不⾜以容纳新写⼊数据时,从所有key中使⽤LFU算法进⾏淘汰.
• volatile-random 当内存不⾜以容纳新写⼊数据时,从设置了过期时间的key中,随机淘汰数据.
• allkeys-random 当内存不⾜以容纳新写⼊数据时,从所有key中随机淘汰数据.
• volatile-ttl 在设置了过期时间的key中,根据过期时间进⾏淘汰,越早过期的优先被淘汰.
(相当于 FIFO, 只不过是局限于过期的 key)
• noeviction 默认策略,当内存不⾜以容纳新写⼊数据时,新写⼊操作会报错.
9.3、缓存预热, 缓存穿透, 缓存雪崩 和 缓存击穿
缓存预热
缓存中的数据
1.定期生成(这种情况,不涉及"预热")
2.实时生成
使⽤ Redis 作为 MySQL 的缓存的时候, 当 Redis 刚刚启动, 或者 Redis ⼤批 key 失效之后, 此时由于Redis ⾃⾝相当于是空着的, 没啥缓存数据, 那么 MySQL 就可能直接被访问到, 从⽽造成较⼤的压⼒.因此就需要提前把热点数据准备好, 直接写⼊到 Redis 中. 使 Redis 可以尽快为 MySQL 撑起保护伞.热点数据可以基于之前介绍的统计的⽅式⽣成即可. 这份热点数据不⼀定⾮得那么 "准确", 只要能帮助MySQL 抵挡⼤部分请求即可. 随着程序运⾏的推移, 缓存的热点数据会逐渐⾃动调整, 来更适应当前情况.
缓存穿透
访问的 key 在 Redis 和 数据库中都不存在. 此时这样的 key 不会被放到缓存上, 后续如果仍然在访问该key, 依然会访问到数据库.这就会导致数据库承担的请求太多, 压⼒很⼤.这种情况称为 缓存穿透.
为何产生
原因可能有⼏种:
• 业务设计不合理. ⽐如缺少必要的参数校验环节, 导致⾮法的 key 也被进⾏查询了.
• 开发/运维误操作. 不⼩⼼把部分数据从数据库上误删了.
• ⿊客恶意攻击.
解决方案
• 针对要查询的参数进⾏严格的合法性校验. ⽐如要查询的 key 是⽤⼾的⼿机号, 那么就需要校验当前key 是否满⾜⼀个合法的⼿机号的格式.
• 针对数据库上也不存在的 key , 也存储到 Redis 中, ⽐如 value 就随便设成⼀个 "". 避免后续频繁访问数据库.
• 使⽤布隆过滤器先判定 key 是否存在, 再真正查询.
缓存雪崩
短时间内⼤量的 key 在缓存上失效, 导致数据库压⼒骤增, 甚⾄直接宕机。
本来 Redis 是 MySQL 的⼀个护盾, 帮 MySQL 抵挡了很多外部的压⼒. ⼀旦护盾突然失效了, MySQL⾃⾝承担的压⼒骤增, 就可能直接崩溃.
为何产生
⼤规模 key 失效, 可能性主要有两种:
• Redis 挂了.
• Redis 上的⼤量的 key 同时过期
为啥会出现⼤量的 key 同时过期?
这种和可能是短时间内在 Redis 上缓存了⼤量的 key, 并且设定了相同的过期时间.
解决方案
• 部署⾼可⽤的 Redis 集群, 并且完善监控报警体系.
• 不给 key 设置过期时间 或者 设置过期时间的时候添加随机时间因⼦.
缓存击穿
相当于缓存雪崩的特殊情况. 针对热点 key , 突然过期了, 导致⼤量的请求直接访问到数据库上, 甚⾄引起数据库宕机.
解决方案
• 基于统计的⽅式发现热点 key, 并设置永不过期.
• 进⾏必要的服务降级. 例如访问数据库的时候使⽤分布式锁, 限制同时请求数据库的并发数.
十、分布式锁
在⼀个分布式的系统中, 也会涉及到多个节点访问同⼀个公共资源的情况. 此时就需要通过 锁 来做互斥控制, 避免出现类似于 "线程安全" 的问题.
⽽ java 的 synchronized 或者 C++ 的 std::mutex, 这样的锁都是只能在当前进程中⽣效, 在分布式的这种多个进程多个主机的场景下就⽆能为⼒了.此时就需要使⽤到分布式锁.
10.1、分布式锁的基本实现
思路⾮常简单. 本质上就是通过⼀个键值对来标识锁的状态.
举个例⼦: 考虑买票的场景, 现在⻋站提供了若⼲个⻋次, 每个⻋次的票数都是固定的.现在存在多个服务器节点, 都可能需要处理这个买票的逻辑: 先查询指定⻋次的余票, 如果余票 > 0, 则设
置余票值 -= 1.
显然上述的场景是存在 "线程安全" 问题的, 需要使⽤锁来控制
否则就可能出现 "超卖" 的情况.
-
此时, 如果 买票服务器1 尝试买票, 就需要先访问 Redis, 在 Redis 上设置⼀个键值对. ⽐如 key 就是⻋次, value 随便设置个值 (⽐如 1).
-
如果这个操作设置成功, 就视为当前没有节点对该 001 ⻋次加锁, 就可以进⾏数据库的读写操作. 操作完成之后, 再把 Redis 上刚才的这个键值对给删除掉.
-
如果在 买票服务器1 操作数据库的过程中, 买票服务器2 也想买票, 也会尝试给 Redis 上写⼀个键值对,key 同样是⻋次. 但是此时设置的时候发现该⻋次的 key 已经存在了, 则认为已经有其他服务器正在持有锁, 此时 服务器2 就需要等待或者暂时放弃.
10.2、引入过期时间
当 服务器1 加锁之后, 开始处理买票的过程中, 如果 服务器1 意外宕机了, 就会导致解锁操作 (删除该key) 不能执⾏. 就可能引起其他服务器始终⽆法获取到锁的情况.
为了解决这个问题, 可以在设置 key 的同时引⼊过期时间. 即这个锁最多持有多久, 就应该被释放.
可以使⽤
set ex nx
的⽅式, 在设置锁的同时把过期时间设置进去.
注意! 此处的过期时间只能使⽤⼀个命令的⽅式设置.
如果分开多个操作, ⽐如 setnx 之后, 再来⼀个单独的 expire, 由于 Redis 的多个指令之间不存在关联, 并且即使使⽤了事务也不能保证这两个操作都⼀定成功, 因此就可能出现 setnx 成功, 但是 expire失败的情况。此时仍然会出现⽆法正确释放锁的问题.
10.3、引入校验id
对于 Redis 中写⼊的加锁键值对, 其他的节点也是可以删除的
⽐如 服务器1 写⼊⼀个 "001": 1 这样的键值对, 服务器2 是完全可以把 "001" 给删除掉的.当然, 服务器2 不会进⾏这样的 "恶意删除" 操作, 不过不能保证因为⼀些 bug 导致 服务器2 把锁误删除.
为了解决上述问题, 我们可以引⼊⼀个校验 id
⽐如可以把设置的键值对的值, 不再是简单的设为⼀个 1, ⽽是设成服务器的编号. 形如 "001": "服务器1".
这样就可以在删除 key (解锁)的时候, 先校验当前删除 key 的服务器是否是当初加锁的服务器, 如果是,才能真正删除; 不是, 则不能删除.
逻辑⽤伪代码描述如下
10.4、引入lua脚本
为了使解锁操作原⼦, 可以使⽤ Redis 的 Lua 脚本功能
10.5、引入watch dog看门狗
上述⽅案仍然存在⼀个重要问题. 当我们设置了 key 过期时间之后 (⽐如 10s), 仍然存在⼀定的可能性,当任务还没执⾏完, key 就先过期了. 这就导致锁提前失效.
把这个过期时间设置的⾜够⻓, ⽐如 30s, 是否能解决这个问题呢? 很明显, 设置多⻓时间合适, 是⽆⽌境的. 即使设置再⻓, 也不能完全保证就没有提前失效的情况.⽽且如果设置的太⻓了, 万⼀对应的服务器挂了, 此时其他服务器也不能及时的获取到锁.因此相⽐于设置⼀个固定的⻓时间, 不如动态的调整时间更合适.
所谓 watch dog, 本质上是加锁的服务器上的⼀个单独的线程, 通过这个线程来对锁过期时间进⾏ "续约".
注意,:这个线程是业务服务器上的, 不是 Redis 服务器的
例子
初始情况下设置过期时间为 10s. 同时设定看⻔狗线程每隔 3s 检测⼀次.
那么当 3s 时间到的时候, 看⻔狗就会判定当前任务是否完成.
• 如果任务已经完成, 则直接通过 lua 脚本的⽅式, 释放锁(删除 key).
• 如果任务未完成, 则把过期时间重写设置为 10s. (即 "续约")
10.6、引⼊ Redlock 算法
实践中的 Redis ⼀般是以集群的⽅式部署的 (⾄少是主从的形式, ⽽不是单机). 那么就可能出现以下⽐较极端的⼤冤种情况:
服务器1 向 master 节点进⾏加锁操作. 这个写⼊ key 的过程刚刚完成, master 挂了; slave 节点升级成了新的 master 节点. 但是由于刚才写⼊的这个 key 尚未来得及同步给 slave 呢, 此时就相当于 服务器1 的加锁操作形同虚设了, 服务器2 仍然可以进⾏加锁 (即给新的 master 写⼊ key. 因为新的 master 不包含刚才的 key).
解决方案
Redis 的作者提出了 Redlock 算法。
此处加锁,就是按照一定的顺序,针对这些组redis都进行加锁操作
如果某个节点挂了(某个节点加不上锁,没关系,可能是redis挂了)
继续给下一个节点加锁即可
如果写入key成功的节点个数超过总数的一啊不能,就视为加锁成功
同理,进行解锁的时候,也就会把上述节点都设置一遍