WEB后端_Day11(持久化、redis容灾处理-redis主从复制、Jedis、Jedis常用操作(单机版)、高可用访问)
redis除了支持多种多样的存储类型,还有一点也非常重要,那就是尽管它是基于内存的存储系统,但它也能进行数据的持久化操作。这一点,对于缓存不幸宕机想恢复缓存数据时相当有效。同样,我们实际使用redis时,为了更高的性能和更高的可用性会将redis配置为集群主从模式。本章节重点介绍持久化和主从复制的相关配置。
1.持久化
redis的持久化有两种方式:快照持久化(RDB)和AOF持久化。
1.1.快照持久化(RDB)
快照持久化,是在某一时刻的所有数据写入到硬盘中持久化。显然,这存在一个“何时”写入硬盘的问题。如果相隔时间过长,那么恰好在没有持久化前宕机,这部分数据就会丢失。也就是说,无论如何配置持久化的时机,都有可能存在丢失数据的风险。所以,快照持久化适用于即使丢失一部分数据也不会造成问题的场景。
配置快照持久化,既可以直接通过命令,也可以通过配置文件的方式。
1.1.1.自动生成RDB(配置文件)
stop-writes-on-bgsave-error yes #当持久化出现错误时,是否停止数据写入,默认停止数据写入。可配置为no,当持久化出现错误时,仍然能继续写入缓存数据。
rdbcompression yes #是否压缩数据,默认压缩。可配置为no,不压缩。
rdbchecksum yes #对持久化rdb文件是否进行校验,默认校验。可配置为no,不校验。
在redis中,持久化默认使用快照持久化方式,如果想要开启AOF持久化appendonly yes,下文会讲AOF持久化的配置。
注意,尽管这是redis安装目录下默认的配置文件,但我们在启动时需要制定配置文件的路径。如果在启动时使用redis-server则会有以下提示:
为了验证rdb的保存条件 增加一条保存条件的配置
同时为了观察效果 需要配置redis的日志 通过日志来查看redis的运行情况
通过日志观察 可以看到当在10秒之内发生一次的改变时 就会进行一次的持久化保存
直接修改配置文件的方式需要重启redis服务,我们可以直接通过命令动态修改配置 这种配置只针对本次服务有效 重启服务之后 该配置就失效了
CONFIG SET save "5 1"
**1.1.2.手动同步RDB(**命令)
除了通过配置文件的方式快照持久化,我们还可以通过命令的方式“随时”地进行快照持久化,有两个命令可供使用:bgsave和save。bgsave,redis会创建一个子进程,通过子进程将快照写入硬盘,父进程则继续处理命令请求。save则是redis在快照创建完成前不会响应其他命令,也就是阻塞式的,并不常用。
save 与 bgsave 对比
命令 | save | bgsave |
---|---|---|
IO类型 | 同步 | 异步 |
阻塞? | 是 | 是(阻塞发生在fock(),通常非常快) |
复杂度 | O(n) | O(n) |
优点 | 不会消耗额外的内存 | 需要fock子进程,消耗内存 |
缺点 | 阻塞客户端命令 | 不阻塞客户端命令 |
通过redis服务端的日志我们就能发现,配置文件中的save配置底层调用的是bgsave命令。那么什么时候会用到save命令呢?
那就是在调用shutdown命令时,将会调用save命令阻塞其他命令,当执行完成后关闭服务器。
1.1.3. RDB的优点
- RDB是一个非常紧凑的文件,它保存了某个时间点得数据集,非常适用于数据集的备份,比如你可以在每个小时报保存一下过去24小时内的数据,同时每天保存过去30天的数据,这样即使出了问题你也可以根据需求恢复到不同版本的数据集。
- RDB是一个紧凑的单一文件,很方便传送到另一个远端数据中心或者亚马逊的S3(可能加密),非常适用于灾难恢复。
- RDB在保存RDB文件时父进程唯一需要做的就是fork出一个子进程,接下来的工作全部由子进程来做,父进程不需要再做其他IO操作,所以RDB持久化方式可以最大化redis的性能。
- 与AOF相比,在恢复大的数据集的时候,RDB方式会更快一些。
1.1.4. RDB的缺点
- 耗时、耗性能。RDB 需要经常fork子进程来保存数据集到硬盘上,当数据集比较大的时候,fork的过程是非常耗时的,可能会导致Redis在一些毫秒级内不能响应客户端的请求。如果数据集巨大并且CPU性能不是很好的情况下,这种情况会持续1秒,AOF也需要fork,但是你可以调节重写日志文件的频率来提高数据集的耐久度。
- 不可控、丢失数据。如果你希望在redis意外停止工作(例如电源中断)的情况下丢失的数据最少的话,那么RDB不适合你。虽然你可以配置不同的save时间点(例如每隔5分钟并且对数据集有100个写的操作),是Redis要完整的保存整个数据集是一个比较繁重的工作,你通常会每隔5分钟或者更久做一次完整的保存,万一在Redis意外宕机,你可能会丢失几分钟的数据。
1.2.AOF持久化
快照功能(RDB)并不是非常耐久(durable): 如果 Redis 因为某些原因而造成故障停机, 那么服务器将丢失最近写入、且仍未保存到快照中的那些数据。 从 1.1 版本开始, Redis 增加了一种完全耐久的持久化方式: AOF 持久化。
你可以在配置文件中打开AOF方式:
写入的时机的配置
打开AOF后, 每当 Redis 执行一个改变数据集的命令时(比如 SET), 这个命令就会被追加到 AOF 文件的末尾。这样的话, 当 Redis 重新启时, 程序就可以通过重新执行 AOF 文件中的命令来达到重建数据集的目的。
1.2.1.AOF运行原理 - 创建
1.2.2.AOF运行原理 - 恢复
1.2.4.AOF持久化的三种策略
可以通过配置文件配置 Redis 多久才将数据 fsync 到磁盘一次。
always
每次有新命令追加到 AOF 文件时就执行一次 fsync :非常慢,也非常安全。
每秒 fsync 一次:足够快(和使用 RDB 持久化差不多),并且在故障时只会丢失 1 秒钟的数据。
推荐(并且也是默认)的措施为每秒 fsync 一次, 这种 fsync 策略可以兼顾速度和安全性。
no
从不 fsync :将数据交给操作系统来处理,由操作系统来决定什么时候同步数据。更快,也更不安全的选择。
always、everysec、no对比
命令 | 优点 | 缺点 |
---|---|---|
always | 不丢失数据 | IO开销大,一般SATA磁盘只有几百TPS |
everysec | 每秒进行与fsync,最多丢失1秒数据 | 可能丢失1秒数据 |
no | 不用管 | 不可控 |
推荐(并且也是默认)的措施为每秒 fsync 一次, 这种 fsync 策略可以兼顾速度和安全性。
1.2.5. AOF重写
因为 AOF 的运作方式是不断地将命令追加到文件的末尾, 所以随着写入命令的不断增加, AOF 文件的体积也会变得越来越大。举个例子, 如果你对一个计数器调用了 100 次 INCR , 那么仅仅是为了保存这个计数器的当前值, AOF 文件就需要使用 100 条记录(entry)。然而在实际上, 只使用一条 SET 命令已经足以保存计数器的当前值了, 其余 99 条记录实际上都是多余的。
为了处理这种情况, Redis 支持一种有趣的特性: 可以在不打断服务客户端的情况下, 对 AOF 文件进行重建(rebuild)。执行 bgrewriteaof 命令, Redis 将生成一个新的 AOF 文件, 这个文件包含重建当前数据集所需的最少命令。
Redis 2.2 需要自己手动执行 bgrewriteaof 命令; Redis 2.4 则可以通过配置自动触发 AOF 重写。
AOF重写的作用
- 减少磁盘占用量
- 加速数据恢复
AOF重写的实现方式
- bgrewriteaof 命令
使用:修改了配置文件之后 重启redis服务 注意一定要指定使用我们修改之后的配置文件
重启之后
- AOF重写配置
配置名 | 含义 |
---|---|
auto-aof-rewrite-min-size | 触发AOF文件执行重写的最小尺寸 |
auto-aof-rewrite-percentage | 触发AOF文件执行重写的增长率 |
aof_current_size | AOF文件当前尺寸(字节) |
---|---|
aof_base_size | AOF文件上次启动和重写时的尺寸(字节) |
AOF重写自动触发机制,需要同时满足下面两个条件:
-
aof_current_size > auto-aof-rewrite-min-size
-
(aof_current_size - aof_base_size) * 100 / aof_base_size > auto-aof-rewrite-percentage
-
auto-aof-rewrite-min-size 64mb auto-aof-rewrite-percentage 100
当AOF文件的体积大于64Mb,并且AOF文件的体积比上一次重写之久的体积大了至少一倍(100%)时,Redis将执行 bgrewriteaof 命令进行重写
1.2.6配置文件
############################## APPEND ONLY MODE ###############################
appendonly no #是否开起AOF持久化,默认关闭,yes打开。在redis4.0以前不允许混合使用RDB和AOF,但此后允许使用混合模式,通过最后两个参数配置。
appendfilename "appendonly.aof" #AOF持久化数据的文件名。
appendfsync everysec #执行AOF持久化的频率,默认“每秒执行一次同步”。还有“always”选项,表示每个redis命令都要同步写入硬盘,但是这样会严重减低redis的速度。还有“no”选项,此时持久化的时机将有操作系统决定。redis建议如果你不知道怎么做,就使用“everysec”配置。
no-appendfsync-on-rewrite no #默认不重写AOF文件。
#下面这两个配置是用于AOF“重写”触发条件。当AOF文件的体积大于64m,且AOF文件的体积比上一次重写之后的体积至少打了一倍(100%)则执行重新命令。
auto-aof-rewrite-percentage 100
auto-aof-rewrite-min-size 64mb
aof-load-truncated yes #指redis在恢复时,会忽略最后一条可能存在问题的指令,默认值yes。
aof-use-rdb-preamble yes #是否打开RDB和AOF混合模式,默认yes打开。
1.2.7.AOF的优点
- 使用AOF 会让你的Redis更加耐久: 你可以使用不同的fsync策略:无fsync,每秒fsync,每次写的时候fsync。使用默认的每秒fsync策略,Redis的性能依然很好(fsync是由后台线程进行处理的,主线程会尽力处理客户端请求),一旦出现故障,你最多丢失1秒的数据。
- AOF文件是一个只进行追加的日志文件,所以不需要写入seek,即使由于某些原因(磁盘空间已满,写的过程中宕机等等)未执行完整的写入命令,你也也可使用redis-check-aof工具修复这些问题。
- Redis 可以在 AOF 文件体积变得过大时,自动地在后台对 AOF 进行重写: 重写后的新 AOF 文件包含了恢复当前数据集所需的最小命令集合。 整个重写操作是绝对安全的,因为 Redis 在创建新 AOF 文件的过程中,会继续将命令追加到现有的 AOF 文件里面,即使重写过程中发生停机,现有的 AOF 文件也不会丢失。 而一旦新 AOF 文件创建完毕,Redis 就会从旧 AOF 文件切换到新 AOF 文件,并开始对新 AOF 文件进行追加操作。
- AOF 文件有序地保存了对数据库执行的所有写入操作, 这些写入操作以 Redis 协议的格式保存, 因此 AOF 文件的内容非常容易被人读懂, 对文件进行分析(parse)也很轻松。 导出(export) AOF 文件也非常简单: 举个例子, 如果你不小心执行了 FLUSHALL 命令, 但只要 AOF 文件未被重写, 那么只要停止服务器, 移除 AOF 文件末尾的 FLUSHALL 命令, 并重启 Redis , 就可以将数据集恢复到 FLUSHALL 执行之前的状态。
1.2.8. AOF的缺点
- 对于相同的数据集来说,AOF 文件的体积通常要大于 RDB 文件的体积。
- 根据所使用的 fsync 策略,AOF 的速度可能会慢于 RDB 。 在一般情况下, 每秒 fsync 的性能依然非常高, 而关闭 fsync 可以让 AOF 的速度和 RDB 一样快, 即使在高负荷之下也是如此。 不过在处理巨大的写入载入时,RDB 可以提供更有保证的最大延迟时间(latency)
**1.3.**RDB和AOF的抉择
1.3.1. RDB 和 AOF 对比
- | RDB | AOF |
---|---|---|
启动优先级 | 低 | 高 |
体积 | 小 | 大 |
恢复速度 | 快 | 慢 |
数据安全性 | 丢数据 | 根据策略决定 |
1.3.2. 如何选择使用哪种持久化方式?
一般来说, 如果想达到足以媲美 PostgreSQL 的数据安全性, 你应该同时使用两种持久化功能。
只做缓存:如果你只希望你的数据在服务器运行的时候存在,你也可以不使用任何持久化方式.
如果你非常关心你的数据, 但仍然可以承受数分钟以内的数据丢失, 那么你可以只使用 RDB 持久化。
有很多用户都只使用 AOF 持久化, 但并不推荐这种方式: 因为定时生成 RDB 快照(snapshot)非常便于进行数据库备份, 并且 RDB 恢复数据集的速度也要比 AOF 恢复的速度要快。
面试:Redis的持久
2.redis容灾处理-redis主从复制
行话:也就是我们所说的主从复制,主机数据更新后根据配置和策略,自动同步到备机的master/slaver机制,Master以写为主,Slave以读为主
2.1.作用
- 读写分离
- 容灾恢复
2.2.配置
2.2.1.配置原则
配从(库)不配主(库)
2.2.2.配置方式:
从库配置:slaveof 主库IP 主库端口
Redis中,用户可以通过执行SLAVEOF命令或者设置slaveof选项,让一个服务器去复制(replicate)另一个服务器,我们称被复制的服务器为主服务器(master),而对主服务器进行复制的服务器则被称为从服务器(slave),如图所示。
假设现在有两个Redis服务器,地址分别为127.0.0.1:6379和127.0.0.1:12345,如果我们向服务器127.0.0.1:12345发送以下命令:
127.0.0.1:12345> SLAVEOF 127.0.0.1 6379
OK
那么服务器127.0.0.1:12345将成为127.0.0.1:6379的从服务器,而服务器127.0.0.1:6379则会成为127.0.0.1:12345的主服务器。
2.2.3.修改配置文件细节操作
1.拷贝多个redis.conf文件
分别修改80 和81 的配置文件
修改启动端口
修改pid文件的名称
日志文件的名称
dump文件的名称
在从机中配置主机的ip和端口
如果主机设置了密码 从机需要配置主机的密码
aof文件的名称
保存退出即可
2.启动三台服务器
查看进程
2.2.4.配置策略
分别在三台服务器上 登陆客户端
为三台服务器分别授权
auth lanqiao
2.2.4.1.一主二仆
查看三台服务器的主从信息
主从问题演示
- 切入点问题?slave1、slave2是从头开始复制还是从切入点开始复制?比如从k4进来,那之前的123是否也可以复制 从头开始复制 在第一次加入的时候 实现的全量复制
- 从机是否可以写?set可否? 不可以
- 主机shutdown后情况如何?从机是上位还是原地待命 原地待命
- 主机又回来了后,主机新增记录,从机还能否顺利复制? 可以
- 其中一台从机down后情况如何?依照原有它能跟上大部队吗? 完全可以跟得上
2.2.4.2.薪火相传
上一个Slave可以是下一个slave的Master,Slave同样可以接收其他slaves的连接和同步请求,那么该slave作为了链条中下一个的master,可以有效减轻master的写压力
中途变更转向:会清除之前的数据,重新建立拷贝最新的
修改80的master为81
2.2.4.3.反客为主(容灾处理)
当 Master 服务出现故障,需手动将 slave 中的一个提升为 master, 剩下的 slave 挂至新的master 上(冷处理:机器挂掉了,再处理)
SLAVEOF no one
A、 将 Master:6379 停止(模拟挂掉)
B、 选择一个(80) Slave 升到 Master,其它的 Slave 挂到新提升的 Master
当79 重新启动之后 此时他已经脱离了原来的主从关系 虽然他的角色还是master
2.3.复制原理
2.3.1.全量同步
Redis全量复制一般发生在Slave初始化阶段,这时Slave需要将Master上的所有数据都复制一份。具体步骤如下:
- 从服务器连接主服务器,发送SYNC命令;
- 主服务器接收到SYNC命名后,开始执行BGSAVE命令生成RDB文件并使用缓冲区记录此后执行的所有写命令;
- 主服务器BGSAVE执行完后,向所有从服务器发送快照文件,并在发送期间继续记录被执行的写命令;
- 从服务器收到快照文件后丢弃所有旧数据,载入收到的快照;
- 主服务器快照发送完毕后开始向从服务器发送缓冲区中的写命令;
- 从服务器完成对快照的载入,开始接收命令请求,并执行来自主服务器缓冲区的写命令;
完成上面几个步骤后就完成了从服务器数据初始化的所有操作,从服务器此时可以接收来自用户的读请求。
2.3.2.增量同步
Redis增量复制是指Slave初始化后开始正常工作时主服务器发生的写操作同步到从服务器的过程。
增量复制的过程主要是主服务器每执行一个写命令就会向从服务器发送相同的写命令,从服务器接收并执行收到的写命令
但是只要是重新连接master,一次完全同步(全量复制)将被自动执行
2.4.哨兵模式(sentinel)(容灾处理)
反客为主的自动版,能够后台监控主机是否故障,如果故障了根据投票数自动将从库转换为主库
2.4.1.使用步骤
2.4.1.1.调整结构,6379带着80、81
2.4.1.2.配置哨兵 sentinel.conf
在/usr/local目录下 拷贝redis解压目录下的sentinel.conf文件,名字绝不能错
2.4.1.3配置项:
修改启动模式
修改pid文件名称
哨兵监视
配置授权
sentinel auth-pass mymaster lanqiao
2.4.1.4.配置文件说明:
-
port :当前Sentinel服务运行的端口
-
dir : Sentinel服务运行时使用的临时文件夹
-
sentinel monitor master001 192.168.110.101 6379 2:Sentinel去监视一个名为master001的主redis实例,这个主实例的IP地址为本机地址192.168.110.101,端口号为6379,而将这个主实例判断为失效至少需要2个 Sentinel进程的同意,只要同意Sentinel的数量不达标,自动failover就不会执行
-
sentinel down-after-milliseconds master001 30000:指定了Sentinel认为Redis实例已经失效所需的毫秒数。当实例超过该时间没有返回PING,或者直接返回错误,那么Sentinel将这个实例标记为主观下线。只有一个 Sentinel进程将实例标记为主观下线并不一定会引起实例的自动故障迁移:只有在足够数量的Sentinel都将一个实例标记为主观下线之后,实例才会被标记为客观下线,这时自动故障迁移才会执行
-
sentinel parallel-syncs master001 1:指定了在执行故障转移时,最多可以有多少个从Redis实例在同步新的主实例,在从Redis实例较多的情况下这个数字越小,同步的时间越长,完成故障转移所需的时间就越长
-
sentinel failover-timeout master001 180000:如果在该时间(ms)内未能完成failover操作,则认为该failover失败
模拟master 宕机
经过一段时间 哨兵投票之后 选出 新的master
当原来的主机回来之后 此时的角色就变为了slave
3. Jedis
使用java来操作redis 数据库 类似jdbc
1 创建一个maven工程
创建结束之后 需要补全项目的目录结构
2 引入依赖
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
<version>3.3.0</version>
</dependency>
<dependency>
<groupId>commons-io</groupId>
<artifactId>commons-io</artifactId>
<version>2.7</version>
</dependency>
4.Jedis常用操作(单机版)
4.1.测试连通性
public static void main( String[] args )
{
//创建jedis对象 ip 端口
Jedis jedis = new Jedis("192.168.112.3",6379);
jedis.auth("lanqiao");//设置授权密码
String pong = jedis.ping();//测试是否联通
System.out.println(pong);
}
4.2.存取数据的测试
在redis中有什么命令 在jedis中就有响应的方法
public static void main( String[] args )
{
//创建jedis对象 ip 端口 此时的链接对象是master 写操作只能在master上进行
Jedis jedis = new Jedis("192.168.112.3",6380);
jedis.auth("lanqiao");//设置授权密码
String pong = jedis.ping();//测试是否联通
System.out.println(pong);
//存取字符串
jedis.set("hello","world");
String value = jedis.get("hello");
System.out.println(value);
// 存取集合list
jedis.lpush("list","aaa","bbb","ccc","ddd");
List<String> list = jedis.lrange("list",0 , -1);
for(String str : list){
System.out.println(str);
}
}
5.高可用访问
Jedis 使用 commons-pool 完成池化实现。
public class MSTest {
private static JedisSentinelPool pool;
static {
JedisPoolConfig config = new JedisPoolConfig();
config.setMaxTotal(50);
config.setMaxIdle(10);
config.setMinIdle(5);
Set<String> sentinel = new HashSet<>();
sentinel.add("192.168.112.3:26379");
sentinel.add("192.168.112.3:26380");
sentinel.add("192.168.112.3:26381");
pool = new JedisSentinelPool("mymaster",sentinel,"lanqiao");
}
public static void main(String[] args) {
Jedis jedis = pool.getResource();
System.out.println(jedis.ping());
jedis.lpush("pool","AAA","BBB","CCC","DDDD");
List<String> list = jedis.lrange("pool",0,-1);
for(String str : list){
System.out.println(str);
}
HostAndPort ha = pool.getCurrentHostMaster();
System.out.println(ha.getHost() +"--"+ha.getPort());
jedis.close();
}
}
public class JedisPoolUtils {
private static JedisSentinelPool pool;
static {
JedisPoolConfig config = new JedisPoolConfig();
config.setMaxTotal(50);
config.setMaxIdle(10);
config.setMinIdle(5);
Set<String> sentinel = new HashSet<>();
sentinel.add("192.168.112.3:26379");
sentinel.add("192.168.112.3:26380");
sentinel.add("192.168.112.3:26381");
pool = new JedisSentinelPool("mymaster",sentinel,"lanqiao");
}
public static Jedis getJedis(){
return pool.getResource();
}
public static void releases(Jedis jedis){
if(jedis != null){
jedis.close();
}
}
}