redis的数据全部在内存中,如果突然宕机,数据就会全部丢失,因此需要持久化来保证redis 的数据不会因为故障而丢失,redis 重启的时候可以重新加载持久化文件来恢复数据,基本上,就是数据写入磁盘的形式。
为了有个整体的认识,先贴出配置文件redis.conf中有关持久化的部分。
###### aof ######
appendonly no
appendfilename "appendonly.aof"
# appendfsync always
appendfsync everysec
# appendfsync no
# auto-aof-rewrite-percentage 为 0 则关闭 aof 复写
auto-aof-rewrite-percentage 100
auto-aof-rewrite-min-size 64mb
# yes 如果 aof 数据不完整,尽量读取最多的格式正确的数据,换句话说,就是把不完整的数据丢弃掉;
# no 如果 aof 数据不完整 报错,可以通过 redis-check-aof 来修复 aof 文件;
aof-load-truncated yes
# 开启混合持久化
aof-use-rdb-preamble yes
###### rdb ######
# redis.conf默认是开启rdb持久化的
# save ""
save 3600 1
save 300 100
save 60 10000
aof(append only file)。aof 日志存储的是 redis 服务器的顺序指令协议,只记录对内存修改的指令记录,通过重放(replay)aof 日志中指令序列来恢复 Redis 当前实例的内存数据结构的状态。
这里只给出需要修改的部分
set key val
# 开启 aof
appendonly yes
# 关闭 aof复写
auto-aof-rewrite-percentage 0
# 关闭 混合持久化
aof-use-rdb-preamble no
# 关闭 rdb
save ""
aof的策略
# 1. 每条命令刷盘 redis 事务才具备持久性,会影响性能
# appendfsync always
# 2. 每秒刷盘,一般采用此策略
appendfsync everysec
# 3. 交由系统刷盘
# appendfsync no
随着时间增长,aof 日志会越来越长,如果 redis 重启,重放整个 aof 日志会非常耗时,导致redis 长时间无法对外提供服务。此外,aof是在主线程中操作的,包括刷盘操作,如果命令较多,会影响redis性能。
aof持久化策略会持久化所有修改命令,里面的很多命令其实可以合并或者删除。
aof rewrite在aof的基础上,满足一定策略则fork进程,根据当前内存状态,转换成一系列的 redis 命令,序列化成一个新的 aof 日志文件中,序列化完毕后再将操作期间发生的增量 aof 日志追加到新的 aof 日志文件中,追加完毕后替换旧的 aof 日志文件,以此达到对 aof 日志瘦身的目的。
lpush list mark
lpush list king
lpush list darren
bgrewriteaof
# 此时会将上面三个命令进行合并成为一个命令
# 合并策略:会先检测键所包含的元素数量,如果超过 64 个会使用多个命令来记录键的值;
hset hash mark 10001
hset hash darren 10002
hset hash king 10003
hdel hash mark
bgrewriteaof
# 此时aof中不会出现mark,设置mark跟删除mark变得像从来没操作过
# 开启 aof
appendonly yes
# 开启 aof复写
auto-aof-rewrite-percentage 100
auto-aof-rewrite-min-size 64mb
# 关闭 混合持久化
aof-use-rdb-preamble no
# 关闭 rdb
save ""
注意:aof rewrite 开启的前提是开启 aof。
aof rewrite的策略
# 1. redis 会记录上次aof复写时的size,如果之后累计超过了原来的size,则会发生aof复写
auto-aof-rewrite-percentage 100
# 2. 为了避免策略1中,小数据量时产生多次发生aof复写,策略2在满足策略1的前提下需要超过 64mb才会发生aof复写;
auto-aof-rewrite-min-size 64mb
aof复写在 aof 基础上实现了瘦身,但是 aof 复写的数据量仍然很大,加载仍然会非常慢。
rdb 是一种快照持久化方式,通过 fork 主进程,在子进程中将内存当中的数据键值对按照存储方式持久化到 rdb 文件中,rdb 存储的是经过压缩的二进制数据。
# 关闭 aof 同时也关闭了 aof复写,也就是说,下面两行无论设置什么都失效了
appendonly no
# 关闭 aof复写
auto-aof-rewrite-percentage 0
# 关闭 混合持久化
aof-use-rdb-preamble no
# 开启 rdb 也就是注释 save ""
# save ""
save 3600 1
save 300 100
save 60 10000
策略
# redis 默认策略如下:
# 注意:写了多个 save 策略,只需要满足一个则开启rdb持久化
# 3600 秒内有1次修改
save 3600 1
# 300 秒内有100次修改
save 300 100
# 60 秒内有10000次修改
save 60 10000
若采用 rdb 持久化,一旦 redis 宕机,redis将丢失一段时间的数据。
rdb 需要经常 fork 子进程来保存数据集到硬盘上,当数据集比较大的时候,fork 的过程是非常耗时的,可能会导致 redis 在一些毫秒级内不能响应客户端的请求。如果数据集巨大并且 CPU 性能不是很好的情况下,这种情况会持续1秒,aof也需要 fork,但是可以调节重写日志文件的频率来提高数据集的耐久度。
从上面知道,rdb 文件小且加载快但丢失多,aof 文件大且加载慢但丢失少。混合持久化是吸取rdb 和 aof 两者优点的一种持久化方案,aof 复写的时候实际持久化的内容是 rdb,持久化期间修改的数据以 aof 的形式附加到文件的尾部。
混合持久化实际上是在 aof rewrite 基础上进行优化,所以需要先开启 aof rewrite。
# 开启 aof
appendonly yes
# 开启 aof复写
auto-aof-rewrite-percentage 100
auto-aof-rewrite-min-size 64mb
# 开启 混合持久化
aof-use-rdb-preamble yes
# 关闭 rdb
save ""
# save 3600 1
# save 300 100
# save 60 10000
举一些例子,redis的持久化在以下场景中可以用到:
1、MySQL 缓存方案中,redis 不需要开启持久化,因为redis 只存储热点数据,数据的依据来源于MySQL。但是,若某些数据经常访问需要开启持久化,此时可以选择 rdb 持久化方案,也就是允许丢失一段时间数据。
2、对数据可靠性要求高,在机器性能,内存也安全 (考虑fork 写时复制最差的情况)的情况下,可以让 redis 同时开启 aof 和 rdb,注意此时不是混合持久化。redis 重启优先从 aof 加载数据,因为理论上 aof 包含更多最新数据。如果只能开启一种持久化方式,那么使用混合持久化。
这里提一句,aof文件和rdb文件都有各自对应的头,所以不用担心rdb+aof恢复的时候读取文件会出错。
3、在允许数据可以有一部分丢失的情况下,亦可采用主redis不持久化(不持久化就不会fork,可以充分利用内存,例如96G的内存可以配置到 90G),从redis进行持久化的方法。这种情况就是发送几条命令的事情,而且主从复制是异步的,所以不会占用redis主线程。
4、伪装从数据。rocksdb静态库,加载到一个进程中,伪装成从数据库,没有fork操作,不需要双倍内存。
拷贝持久化文件是安全的,因为持久化文件一旦被创建,就不会进行任何修改。当服务器要创建一个新的持久化文件时, 它先将文件的内容保存在一个临时文件里面, 当临时文件写入完毕时, 程序才使用rename原子地用临时文件替换原来的持久化文件。
数据安全主要考虑两个问题:一是节点宕机(redis 是内存数据库,宕机数据会丢失),二是磁盘故障。虽然以上两个问题出现的概率相对较低,但是还是需要制定一个数据安全的策略:
创建一个定期任务(cron job), 每小时将一个 rdb 文件备份到一个文件夹, 并且每天将一个rdb 文件备份到另一个文件夹。这样可以预防节点宕机。注意确保快照的备份都要带有相应的日期和时间信息, 每次执行定期任务脚本时, 可以使用 find 命令来删除过期的快照。比如说,保留最近 48 小时内的每小时快照,此外还可以保留最近一两个月的每日快照。另外,至少每天一次, 将 rdb备份到数据中心之外, 或者至少是备份到运行redis服务器的物理机器之外,以避免磁盘故障。
另外,在关闭redis时,使用SHUTDOWN SAVE
命令安全关闭redis。在关闭redis之前,把内存中的数据重新生成一次rdb文件。重启后,用这个rdb文件恢复数据,保证redis重启的安全。如果同时开启了aof和rdb,因为出现问题而导致没有安全关闭,redis重启后会根据aof来恢复数据。
主从复制主要用来实现 redis数据的可靠性,防止主 redis 所在磁盘损坏,造成数据永久丢失,主从之间采用异步复制的方式。注意区分一下,主从复制与数据持久化没有关系,只是将rdb文件复制过去。
当然,不只是redis,所有数据库都要知晓主从复制的原理。
redis-server --replicaof 127.0.0.1 7001
在 redis 5.0 以前使用 slaveof,redis 5.0 之后使用 replicaof 。
也可以在redis.conf中配置。
# redis.conf
replicaof 127.0.0.1 7002
info replication
无论主库还是从库都有自己的 RUN ID。RUN ID 启动时自动产生,由40个随机的十六进制字符组成,当从库对主库初次复制时,主库将自身的 RUN ID 传送给从库,从库会将 RUN ID 保存。当从库断线重连主库时,从库将向主库发送之前保存的 RUN ID,如果从库发送的 RUN ID 和主库 RUN ID 一致,说明从库断线前复制的就是当前的主库,主库尝试执行增量同步操作;若不一致,说明从库断线前复制的主库并不时当前的主库,则主库将对从库执行全量同步操作。
这里注意一种情况,就算更换了机器,把rdb文件拷贝到这台机器上,再连主数据库。因为rdb文件中存储了RUN ID和增量数据同步的offset,所以这台机器也可以执行增量同步。
增量数据同步要知晓的两个问题:为什么要设置环形缓冲区?如何计算数据是否在环?
在增量同步中,主从都会维护一个复制偏移量。主库向从库发送N个字节的数据时,将自己的复制偏移量上加N;从库接收到主库发送的N个字节数据时,将自己的复制偏移量加上N。通过比较主从偏移量得知主从之间数据是否一致,偏移量相同则数据一致,偏移量不同则数据不一致。
增量数据同步使用,本质是一个固定长度的先进先出队列。使用环形缓冲区的目的,是为了可以减少分配和释放内存带来的消耗。
当因某些原因(网络抖动或从库宕机)从库与主库断开连接,避免重新连接后开始全量同步,在主库设置了一个环形缓冲区,该缓冲区会在从库失联期间记录主库的写操作,当从库重连,会发送自身的复制偏移量到主库,主库会比较主从的复制偏移量:若从库offset还在复制积压缓冲区中,则进行增量同步;否则主库将对从库执行全量同步。
相应设置
# redis.conf中
# 环形缓冲区大小
repl-backlog-size 1mb
# 如果所有从库断开连接 3600 秒后没有从库连接,则释放环形缓冲区
repl-backlog-ttl 3600
环形缓冲区的大小确定:disconnect_time * write_size_per_second
disconnect_time
:从库断线后重连主库所需的平均时间(以秒为单位)。
write_size_per_second
:主库平均每秒产生的写命令数据量。
哨兵模式是redis可用性的解决方案,它由一个或多个 sentinel 实例(一般奇数个)构成 sentinel 系统。该系统可以监视任意多个主库以及这些主库所属的从库,当主库处于下线状态,自动将该主库所属的某个从库升级为新的主库。客户端来连接集群时,会首先连接 sentinel,通过 sentinel 来查询主节点的地址,然后再连接主节点进行数据交互。当主节点发生故障时,客户端会重新向 sentinel 索要主库地址,sentinel 会将最新的主库地址告诉客户端。通过这样客户端无须重启即可自动完成节点切换。客户端要知道所有哨兵节点的ip和port,每次随机连接其中一个,如果这个宕机了,再换另一个。
工作中,不要使用哨兵模式,因为主数据库宕机后,30秒才能完成切换,时间太长,导致可用性不高。
如图,每个哨兵节点都会与任意主从节点建立两条连接:cmd命令连接;pub/sub发布订阅,主要用于广播。
哨兵模式当中涉及多个选举流程采用的时 Raft 算法的领头选举方法的实现。
# 注意是在sentinel.cnf文件中
# sentinel 只需指定检测主节点就行了,通过主节点自动发现从节点,最后一个参数表示几个哨兵节点认为主观下线就判断为客观下线
sentinel monitor mymaster 127.0.0.1 6379 2
# 判断主观下线时长
sentinel down-after-milliseconds mymaster 30000
# 指定可以有多少个Redis服务同步新的主机,一般而言,这个数字越小同步时间越长,而越大,则对网络资源要求越高
sentinel parallel-syncs mymaster 1
# 指定故障切换允许的毫秒数,超过这个时间,就认为故障切换失败,默认为3分钟
sentinel failover-timeout mymaster 180000
要确保配置文件可写,因为如果主库节点改变,哨兵节点会修改配置。
sentinel要判断节点是否在线,有相应的判断机制。
sentinel 会以每秒一次的频率向所有节点(其他sentinel、主节点、以及从节点)发送 ping 消息,然后通过接收返回判断该节点是否下线,如果在配置指定 down-after-milliseconds 时间内则被判断为主观下线。
当一个 sentinel 节点将一个主节点判断为主观下线之后,为了确认这个主节点是否真的下线,它会向其他sentinel 节点进行询问,如果收到一定数量的已下线回复,sentinel 会将主节点判定为客观下线,并通过选举出来的领头 sentinel 节点对主节点执行故障转移。
故障转移只能由一个哨兵节点进行,否则会有命令冲突。这是分布式系统经常出现的场景,即由谁来执行的问题,所以需要进行选举。
主节点被判定为客观下线后,开始领头 sentinel 选举,选举需要一半以上的 sentinel 支持。选举领头sentinel后,该sentinel开始执行对主节点故障转移:从从节点中选举一个从节点作为新的主节点,通知其他从节点复制连接新的主节点,若故障主节点重新连接,将作为新的主节点的从节点。
redis 采用异步复制的方式,意味着当主节点挂掉时,从节点可能没有收到全部的同步消息,这部分未同步的消息将丢失。如果主从延迟特别大,那么丢失可能会特别多。sentinel 无法保证消息完全不丢失,但是可以通过配置来尽量保证少丢失。
# 主库必须有一个从节点在进行正常复制,否则主库就停止对外写服务,此时丧失了可用性
min-slaves-to-write 1
# 这个参数用来定义什么是正常复制,该参数表示如果在10s内没有收到从库反馈,就意味着从库同步不正常;
min-slaves-max-lag 10
另外,它的致命缺点是不能进行横向扩展。
codis 由国内团队开发,图中可开启多个 codis 实例,client 与 codis 之间通过 redis 协议进行交互,用户可不感知 codis的存在。
由于有诸多限制(事务不支持,更新延迟),现在几乎很少有团队使用。
redis cluster将所有数据划分为 16384( 2 14 2^{14} 214)个槽位,每个 redis 节点负责其中一部分槽位。
提一句,cluster 集群是一种去中心化的集群方式,是一种高可用方案。
如图,该集群由三个 redis 节点组成,每个节点负责整个集群的一部分数据,每个节点负责的数据多少可能不一样。这三个节点相互连接组成一个对等的集群,它们之间通过一种特殊的二进制协议交互集群信息。
当 redis cluster 的客户端来连接集群时,会得到一份集群的槽位配置信息。这样当客户端要查找某个 key时,可以直接定位到目标节点。客户端为了可以直接定位(对 key 通过 crc16 进行 hash, 再对16384取余)某个具体的 key 所在节点,需要缓存槽位相关信息,这样才可以准确快速地定位到相应的节点。同时因为可能会存在客户端与服务器存储槽位的信息不一致的情况,还需要纠正机制(通过返回 -MOVED 3999 127.0.0.1:6479
,客户端收到后需要立即纠正本地的槽位映射表)来实现槽位信息的校验调整。
另外,redis cluster 的每个节点会将集群的配置信息持久化到配置文件中,这就要求确保配置文件是可写的,而且尽量不要依靠人工修改配置文件。
redis cluster 提供了工具 redis-trib ,可以让运维人员手动调整槽位的分配情况。它采用 ruby 语言开发,通过组合原生的 redis cluster 指令来实现。
图
图中A 为待迁移的源节点,B 为待迁移的目标节点。下面介绍一下数据迁移过程。
redis 迁移的单位是槽,redis 是一个槽一个槽地进行迁移,而且这个迁移过程是阻塞的。当一个槽位正在迁移时,这个槽就处于中间过渡状态。这个槽在源节点的状态为 migrating ,在目标节点的状态为importing ,表示此时数据正在从源节点流向目标节点。迁移过程中,不同槽位对应不同节点的配置信息文件还未更新,要等到所有数据迁移完成后才更新。
迁移工具 redis-trib 首先在源节点和目标节点设置好中间过渡状态,然后一次性获取源节点槽位的所有或者部分的 key 列表,再依次将 key 进行迁移。源节点对当前的 key 执行 dump 指令得到序列化内容,然后向目标节点发送 restore 指令,目标节点将源节点的序列化内容进行反序列化并将内容应用到目标节点的内容中,然后返回 +ok
给源节点,源节点收到后删除该 key。按照这些步骤将所有待迁移的 key 进行迁移。
注意:迁移过程是同步的,迁移过程中源节点的主线程处于阻塞状态,直到key被删除。如果迁移过程中源节点出现网络故障,这两个节点依然处于中间状态,重启后,redis-trib可继续迁移。所以,redis-trib 迁移的过程是一个一个 key 来进行,如果这个 key 对应 val 内容很大,将会影响到客户端的正常访问。
先介绍一下主从节点。cluster 集群中节点分为主节点和从节点,其中主节点用于处理槽,而从节点则用于复制该主节点,并在主节点下线时,代替主节点继续处理命令请求。
集群中每个节点都会定期地向集群中的其他节点发送 ping 消息,如果接收 ping 消息的节点没有在规定时间内回复 pong 消息,那么这个没有回复 pong 消息的节点会被标记为 PFAIL(probable fail,疑似下线)。集群中各个节点会通过互相发送消息的方式来交换集群中各个节点的状态信息。如果在一个集群中,半数以上负责处理槽的主节点都将某个主节点 A 报告为疑似下线,那么这个主节点A将被标记为下线(FAIL)。标记主节点 A 为下线状态的主节点会广播这条消息,其他节点(包括A节点的从节点)也会将A节点标识为 FAIL。
当从节点发现自己的主节点进入FAIL状态,从节点将开始对下线主节点进行故障转移。从数据最新的从节点中选举为主节点:
该从节点会执行 replica no one
命令,成为新的主节点。
新的主节点会撤销所有对该已下线主节点的槽指派,并将这些槽全部指派给自己。
新的主节点向集群广播一条 pong 消息,这条 pong 消息可以让集群中的其他节点立即知道这个节点已经由从节点变成主节点,并且这个主节点已经接管了之前下线的主节点。
新的主节点开始接收和自己负责处理的槽有关的命令请求,故障转移结束。
对于客户端而言,如果redis节点发生了变化(新增或宕机),原有的配置信息失效,会断开与所有redis节点的连接,再重新获取配置信息,重新连接。
这就是cluster集群可用性高的一个原因,故障检测和转移的时间较短,因为不需要选举。
持久化操作和集群的做法对于开发人员来讲,具体的细节其实不需要掌握,因为由运维人员负责,但是,主要的原理和流程需要掌握。