Redis持久化及集群架构

知识点

持久化

RDB快照

在默认情况下,Redis将内存数据库快照保存在名字为dump.rdb的二进制文件中。

条件策略

在redis.conf文件中,可以对Redis快照条件策略进行设置,让它在"N秒内数据集至少有M个改动"这一条件被满足时, 自动保存一次数据集。

redis默认开启RDB快照,并且条件策略如下:

bash 复制代码
# 以下三个条件有一个满足时就会自动保存一次数据集
save 900 1
save 300 10
save 60 10000

save 60 10000:满足"60秒内有至少有1000个键被改动"这一条件时,自动保存一次数据集。

关闭RDB只需要将所有的save保存策略注释掉即可。

手动命令

可以手动执行命令生成RDB快照。

进入redis客户端执行命令savebgsave可以生成dump.rdb文件,每次命令执行都会将所有redis内存快照到一个新的rdb文件里,并覆盖原有rdb快照文件。

Redis的默认配置中,dump.rdb文件存放在Redis服务器进程的当前工作目录下:

bash 复制代码
# 获取redis的主目录
[root@hecs-403280 ~]# redis-cli config get dir
1) "dir"
2) "/"

# 查看redis的dump.rdb文件
[root@hecs-403280 ~]# ll /dump.rdb 
-rw-r--r-- 1 root root 92 Aug  9 14:23 /dump.rdb

# 手动生成RDB快照
# 此时可以看到对应的dump.rdb修改时间为当前时间
[root@hecs-403280 ~]# redis-cli
127.0.0.1:6379> save
OK
127.0.0.1:6379> 

通过find命令发现dump.rdb文件存在于多个地方,通过save命令确认/dump.rdb才是当前redis存放的RDB文件,只有这文件修改时间变成了执行save命令时刻的时间,也验证了上面所说的RDB文件默认存放在redis主目录下。

查找所有dump.rdb文件:

bash 复制代码
[root@hecs-403280 /]# find / -name dump.rdb
/root/dump.rdb
/dump.rdb
/usr/local/redis-5.0.3/src/dump.rdb
/usr/local/redis-5.0.3/utils/dump.rdb

bgsave写时复制

Redis 借助操作系统提供的写时复制技术(Copy-On-Write, COW),在生成快照的同时,依然可以正常处理写命令。简单来说,bgsave 子进程是由主线程 fork 生成的,可以共享主线程的所有内存数据。bgsave 子进程运行后,开始读取主线程的内存数据,并把它们写入 RDB 文件。此时,如果主线程对这些数据也都是读操作,那么,主线程和 bgsave 子进程相互不影响。但是,如果主线程要修改一块数据,那么,这块数据就会被复制一份,生成该数据的副本。然后,bgsave 子进程会把这个副本数据写入 RDB 文件,而在这个过程中,主线程仍然可以直接修改原来的数据。

save与bgsave对比

命令 save bgsave
IO类型 同步 异步
是否阻塞redis其它命令 否(在生成子进程执行调用fork函数时会有短暂阻塞)
复杂度 O(n) O(n)
优点 不会消耗额外内存 不阻塞客户端命令
缺点 阻塞客户端命令 需要fork子进程,消耗内存

配置自动生成rdb文件后台使用的是bgsave方式。

AOF(AppendOnly File)

快照功能并不是非常耐久(durable):如果Redis因为某些原因而造成故障停机,那么服务器将丢失最近写入、且仍未保存到快照中的那些数据(RDB策略是到达一定条件才生成快照)。从1.1版本开始,Redis增加了一种完全耐久的持久化方式:AOF持久化,将修改的每一条指令记录进文件appendonly.aof中(先写入os cache,每隔一段时间fsync到磁盘)。

开启AOF

AOF默认关闭是关闭的,修改为开启后,需要重启redis服务。

redis.conf文件:

bash 复制代码
# 将no改成yes开启AOF
appendonly no
# 文件名
appendfilename "appendonly.aof"

测试aof功能:

bash 复制代码
# 重启redis,systemctl命令表示redis服务已被我做成了linux服务
[root@hecs-403280 /]# systemctl restart redis

# 查找aof文件位置,同dump.rdb默认还是在redis工作目录下
[root@hecs-403280 /]# find / -name appendonly.aof
/appendonly.aof

# 加入数据
[root@hecs-403280 /]# redis-cli
127.0.0.1:6379> keys *
(empty list or set)
127.0.0.1:6379> set name jay
OK
127.0.0.1:6379> quit

# 查看aof内容
[root@hecs-403280 /]# cat /appendonly.aof 
*2
$6
SELECT
$1
0
*3
$3
set
$4
name
$3
jay

AOF功能开启后,每当 Redis 执行一个改变数据集的命令时(比如SET命令),这个命令就会被追加到AOF文件的末尾。

这样的话,当 Redis 重新启动时,程序就可以通过重新执行 AOF 文件中的命令来达到重建数据集的目的。

你可以配置 Redis 多久才将数据 fsync 到磁盘一次。

redis.conf文件:

bash 复制代码
# 每次有新命令追加到AOF文件时就执行一次fsync,非常慢,也非常安全。
# appendfsync always
# 默认设置,每秒fsync一次,足够快,并且在故障时只会丢失1秒钟的数据。
appendfsync everysec
# 从不fsync,将数据交给操作系统来处理。更快,也更不安全的选择。
# appendfsync no

推荐(并且也是默认)的措施为每秒fsync一次, 这种fsync策略可以兼顾速度和安全性。

AOF文件内容

这是一种resp协议格式数据,星号后面的数字代表命令有多少个参数,$号后面的数字代表这个参数有几个字符。

注意,如果执行带过期时间的set命令,aof文件里记录的并不是执行的原始命令,而是记录key过期的时间戳。

比如执行set name jolin ex 1000,对应aof文件里记录如下:

bash 复制代码
*3
$3
set
$4
name
$5
jolin
*3
$9
PEXPIREAT
$4
name
$13
1691565415463

AOF重写

AOF文件里可能有太多没用指令,所以AOF会定期根据内存的最新数据 生成aof文件。

例如,执行了如下几条命令:

bash 复制代码
127.0.0.1:6379> incr readcount
(integer) 1
127.0.0.1:6379> incr readcount
(integer) 2
127.0.0.1:6379> incr readcount
(integer) 3
127.0.0.1:6379> incr readcount
(integer) 4
127.0.0.1:6379> incr readcount
(integer) 5

重写后AOF文件里变成:

bash 复制代码
*3
$3
SET
$2
readcount
$1
5

如下两个配置可以控制AOF自动重写频率:

bash 复制代码
# aof文件至少要达到64M才会自动重写,文件太小恢复速度本来就很快,重写的意义不大
# auto-aof-rewrite-min-size 64mb

# aof文件自上一次重写后文件大小增长了100%则再次触发重写
# auto-aof-rewrite-percentage 100

AOF还可以手动重写,进入redis客户端执行命令bgrewriteaof重写AOF。

注意,AOF重写redis会fork出一个子进程去做(与bgsave命令类似),不会对redis正常命令处理有太多影响。

RDB与AOF比较

命令 RDB AOF
启动优先级
体积
恢复速度
数据安全性 容易丢数据 根据策略决定

生产环境可以都启用,redis启动时如果既有rdb文件又有aof文件则优先选择aof文件恢复数据,因为aof一般来说数据更全一点。

Redis 4.0 混合持久化

重启Redis时,我们很少使用RDB来恢复内存状态,因为会丢失大量数据。我们通常使用AOF日志重放,但是重放AOF日志性能相对RDB来说要慢很多,这样在Redis实例很大的情况下,启动需要花费很长的时间。 Redis 4.0为了解决这个问题,带来了一个新的持久化选项------混合持久化。

开启混合持久化,必须先开启aof。

redis.conf文件:

bash 复制代码
# 默认没有被注释,但是aof默认没开启,所以混合持久化功能默认也是不开启的,开启aof默认开启混合持久化
aof-use-rdb-preamble yes 

如果开启了混合持久化,AOF在重写时 ,不再是单纯将内存数据转换为RESP命令写入AOF文件,而是将重写这一刻之前 的内存做RDB快照处理,并且将RDB快照内容和增量的 AOF修改内存数据的命令存在一起,都写入新的AOF文件,新的文件一开始不叫appendonly.aof,等到重写完新的AOF文件才会进行改名,覆盖原有的AOF文件,完成新旧两个AOF文件的替换。

于是在Redis重启的时候,可以先加载RDB的内容,然后再重放增量AOF日志就可以完全替代之前的AOF全量文件重放,因此重启效率大幅得到提升。

混合持久化AOF文件结构:

实操:

先执行bgrewriteaof命令触发AOF重写,aof文件的内容为RDB格式的二进制文件(第一个红框),再在redis客户端执行set命令插入数据,此时aof的内容包括RDB格式内容和AOF格式内容(第二个红框),图示如下:

Redis数据备份方案

  1. 写crontab定时调度脚本,每小时都copy一份rdb或aof的备份到一个目录中去,仅仅保留最近48小时的备份
  2. 每天都保留一份当日的数据备份到一个目录中去,可以保留最近1个月的备份
  3. 每次copy备份的时候,都把太旧的备份给删了
  4. 每天晚上将当前机器上的备份复制一份到其他机器上,以防机器损坏

集群

主从架构

主从架构搭建

复制一份redis.conf文件:

bash 复制代码
[root@hecs-403280 redis]# pwd
/etc/redis
[root@hecs-403280 redis]# cp 6379.conf 6380.conf

修改配置如下:

bash 复制代码
port 6380
# 把pid进程号写入pidfile配置的文件
pidfile /var/run/redis_6380.pid
logfile "6380.log"
# 指定数据存放目录,该目录需要手动新建
dir /usr/local/redis-5.0.3/data/6380
# 需要注释掉bind
# bind绑定的是自己机器网卡的ip,如果有多块网卡可以配多个ip,代表允许客户端通过机器的哪些网卡ip去访问,内网一般可以不配置bind,注释掉即可
# master节点的redis配置文件中此处也要注释掉,否则会报错,报错日志在6380.log中
# bind 127.0.0.1

# 配置主从复制
# 从本机6379的redis实例复制数据,Redis 5.0之前使用slaveof
replicaof 192.168.0.83 6379
# 配置从节点只读
replica-read-only yes

启动从节点:

bash 复制代码
# redis.conf文件务必用你复制并修改了之后的redis.conf文件
[root@hecs-403280 redis]# redis-server 6380.conf

连接从节点:

bash 复制代码
[root@hecs-403280 redis]# redis-cli -p 6380

测试:

测试在6379实例上写数据,6380实例是否能及时同步新修改数据。

bash 复制代码
# ps查看服务是否启动
[root@hecs-403280 ~]# ps -ef|grep redis
root      4095     1  0 14:48 ?        00:00:06 /usr/local/redis-5.0.3/src/redis-server 127.0.0.1:6379
root      8714     1  0 16:19 ?        00:00:00 redis-server *:6380
root      9154  8438  0 16:27 pts/2    00:00:00 grep --color=auto redis

# 新加的position在6380上已经有了
[root@hecs-403280 ~]# redis-cli -p 6380
127.0.0.1:6380> keys *
1) "name"
2) "age"
3) "position"

按照上面方法,再配置一台从节点,端口为6381。

bash 复制代码
[root@hecs-403280 redis]# ps -ef|grep redis
root      8714     1  0 16:19 ?        00:00:01 redis-server *:6380
root      9214  2745  0 16:29 pts/1    00:00:00 redis-cli
root      9653     1  0 16:37 ?        00:00:00 /usr/local/redis-5.0.3/src/redis-server *:6379
root      9673  8438  0 16:37 pts/2    00:00:00 redis-cli -p 6380
root      9996     1  0 16:43 ?        00:00:00 redis-server *:6381
root     10009  1821  0 16:44 pts/0    00:00:00 grep --color=auto redis

主从工作原理

如果你为master配置了一个slave,不管这个slave是否是第一次连接上Master,它都会发送一个PSYNC命令给master请求复制数据。

master收到PSYNC命令后,会在后台进行数据持久化通过bgsave生成最新的rdb快照文件,持久化期间,master会继续接收客户端的请求,它会把这些可能修改数据集的请求缓存在内存中。当持久化进行完毕以后,master会把这份rdb文件数据集发送给slave,slave会把接收到的数据进行持久化生成rdb,然后再加载到内存中。然后,master再将之前缓存在内存中的命令发送给slave。

当master与slave之间的连接由于某些原因而断开时,slave能够自动重连Master,如果master收到了多个slave并发连接请求,它只会进行一次持久化,而不是一个连接一次,然后再把这一份持久化的数据发送给多个并发连接的slave。

图示-全量复制:

数据部分复制:

当master和slave断开重连后,一般都会对整份数据进行复制。但从redis2.8版本开始,redis改用可以支持部分数据复制的命令PSYNC去master同步数据,slave与master能够在网络连接断开重连后只进行部分数据复制(断点续传)。

master会在其内存中创建一个复制数据用的缓存队列,缓存最近一段时间的数据,master和它所有的slave都维护了复制的数据下标offset和master的进程id,因此,当网络连接断开后,slave会请求master继续进行未完成的复制,从所记录的数据下标开始。如果master进程id变化了,或者从节点数据下标offset太旧,已经不在master的缓存队列里了,那么将会进行一次全量数据的复制。

图示-部分复制:

避免主从复制风暴:

如果有很多从节点,为了缓解主从复制风暴 (多个从节点同时复制主节点导致主节点压力过大),可以做如下架构,让部分从节点与从节点(与主节点同步)同步数据。

哨兵模式

哨兵原理

sentinel哨兵是特殊的redis服务(redis实例),不提供读写服务,主要用来监控redis实例节点。

哨兵架构下client端第一次从哨兵找出redis的主节点,后续就直接访问redis的主节点,不会每次都通过sentinel代理访问redis的主节点,当redis的主节点发生变化,哨兵会第一时间感知到,并且将新的redis主节点通知给client端(这里面redis的client端一般都实现了订阅功能,订阅sentinel发布的节点变动消息)。

Redis哨兵集群搭建

复制一份sentinel.conf文件:

bash 复制代码
[root@hecs-403280 redis-5.0.3]# cp sentinel.conf sentinel-26379.conf

修改配置:

sentinel-26379.conf文件文件:

bash 复制代码
port 26379
daemonize yes
pidfile "/var/run/redis-sentinel-26379.pid"
logfile "26379.log"
dir "/usr/local/redis-5.0.3/data"
# sentinel monitor <master-redis-name> <master-redis-ip> <master-redis-port> <quorum>
# quorum是一个数字,指明当有多少个sentinel认为一个master失效时(值一般为:sentinel总数/2 + 1),master才算真正失效
sentinel monitor mymaster 192.168.0.83 6379 2   # mymaster这个名字随便取,客户端访问时会用到

启动哨兵实例:

bash 复制代码
[root@hecs-403280 redis-5.0.3]# redis-sentinel sentinel-26379.conf

查看sentinel的info信息:

bash 复制代码
[root@hecs-403280 redis-5.0.3]# redis-cli -p 26379
127.0.0.1:26379>info

结果:

可以看到Sentinel的info里已经识别出了redis的主从。

再配置两个sentinel实例,端口号分别是26380和26381。

sentinel集群都启动完毕后,会将哨兵集群的元数据信息写入所有sentinel的配置文件里去(追加在文件的最下面),我们查看下如下配置文件sentinel-26379.conf,如下所示:

bash 复制代码
sentinel known-replica mymaster 192.168.0.60 6380 #代表redis主节点的从节点信息
sentinel known-replica mymaster 192.168.0.60 6381 #代表redis主节点的从节点信息
sentinel known-sentinel mymaster 192.168.0.60 26380 52d0a5d70c1f90475b4fc03b6ce7c3c56935760f  #代表感知到的其它哨兵节点
sentinel known-sentinel mymaster 192.168.0.60 26381 e9f530d3882f8043f76ebb8e1686438ba8bd5ca6  #代表感知到的其它哨兵节点

当redis主节点如果挂了,哨兵集群会重新选举出新的redis主节点,同时会修改所有sentinel节点配置文件的集群元数据信息,比如6379的redis如果挂了,假设选举出的新主节点是6380,则sentinel文件里的集群元数据信息会变成如下所示:

bash 复制代码
sentinel known-replica mymaster 192.168.0.60 6379 #代表主节点的从节点信息
sentinel known-replica mymaster 192.168.0.60 6381 #代表主节点的从节点信息
sentinel known-sentinel mymaster 192.168.0.60 26380 52d0a5d70c1f90475b4fc03b6ce7c3c56935760f  #代表感知到的其它哨兵节点
sentinel known-sentinel mymaster 192.168.0.60 26381 e9f530d3882f8043f76ebb8e1686438ba8bd5ca6  #代表感知到的其它哨兵节点

同时还会修改sentinel文件里之前配置的mymaster对应的6379端口,改为6380

bash 复制代码
sentinel monitor mymaster 192.168.0.60 6380 2 

当6379的redis实例再次启动时,哨兵集群根据集群元数据信息就可以将6379端口的redis节点作为从节点加入集群。

简单代码示例

bash 复制代码
public class JedisSentinelTest {
    public static void main(String[] args) throws IOException {

        JedisPoolConfig config = new JedisPoolConfig();
        config.setMaxTotal(20);
        config.setMaxIdle(10);
        config.setMinIdle(5);

        String masterName = "mymaster";
        Set<String> sentinels = new HashSet<String>();
        sentinels.add(new HostAndPort("192.168.0.60",26379).toString());
        sentinels.add(new HostAndPort("192.168.0.60",26380).toString());
        sentinels.add(new HostAndPort("192.168.0.60",26381).toString());
        //JedisSentinelPool其实本质跟JedisPool类似,都是与redis主节点建立的连接池
        //JedisSentinelPool并不是说与sentinel建立的连接池,而是通过sentinel发现redis主节点并与其建立连接
        JedisSentinelPool jedisSentinelPool = new JedisSentinelPool(masterName, sentinels, config, 3000, null);
        Jedis jedis = null;
        try {
            jedis = jedisSentinelPool.getResource();
            System.out.println(jedis.set("sentinel", "jay"));
            System.out.println(jedis.get("sentinel"));
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            //注意这里不是关闭连接,在JedisPool模式下,Jedis会被归还给资源池。
            if (jedis != null)
                jedis.close();
        }
    }
}

哨兵SpringBoot整合

引入依赖:

bash 复制代码
<dependency>
   <groupId>org.springframework.boot</groupId>
   <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

<dependency>
   <groupId>org.apache.commons</groupId>
   <artifactId>commons-pool2</artifactId>
</dependency>

springboot核心配置:

bash 复制代码
server:
  port: 8080

spring:
  redis:
    database: 0
    timeout: 3000
    sentinel:    #哨兵模式
      master: mymaster #主服务器所在集群名称
     nodes: 192.168.0.60:26379,192.168.0.60:26380,192.168.0.60:26381
   lettuce:
      pool:
        max-idle: 50
        min-idle: 10
        max-active: 100
        max-wait: 1000

访问代码:

bash 复制代码
@RestController
public class IndexController {

    private static final Logger logger = LoggerFactory.getLogger(IndexController.class);

    @Autowired
    private StringRedisTemplate stringRedisTemplate;

    /**
     * 测试节点挂了哨兵重新选举新的master节点,客户端是否能动态感知到
     * 新的master选举出来后,哨兵会把消息发布出去,客户端实际上是实现了一个消息监听机制,
     * 当哨兵把新master的消息发布出去,客户端会立马感知到新master的信息,从而动态切换访问的masterip
     *
     * @throws InterruptedException
     */
    @RequestMapping("/test_sentinel")
    public void testSentinel() throws InterruptedException {
        int i = 1;
        while (true){
            try {
                stringRedisTemplate.opsForValue().set("jay"+i, i+"");
                System.out.println("设置key:"+ "jay" + i);
                i++;
                Thread.sleep(1000);
            }catch (Exception e){
                logger.error("错误:", e);
            }
        }
    }
}
相关推荐
_.Switch1 小时前
Python Web 应用中的 API 网关集成与优化
开发语言·前端·后端·python·架构·log4j
韩楚风3 小时前
【linux 多进程并发】linux进程状态与生命周期各阶段转换,进程状态查看分析,助力高性能优化
linux·服务器·性能优化·架构·gnu
BergerLee5 小时前
对不经常变动的数据集合添加Redis缓存
数据库·redis·缓存
huapiaoy6 小时前
Redis中数据类型的使用(hash和list)
redis·算法·哈希算法
【D'accumulation】6 小时前
令牌主动失效机制范例(利用redis)注释分析
java·spring boot·redis·后端
Cikiss7 小时前
微服务实战——SpringCache 整合 Redis
java·redis·后端·微服务
_.Switch8 小时前
Python机器学习:自然语言处理、计算机视觉与强化学习
python·机器学习·计算机视觉·自然语言处理·架构·tensorflow·scikit-learn
一休哥助手8 小时前
Redis 五种数据类型及底层数据结构详解
数据结构·数据库·redis
盒马盒马9 小时前
Redis:zset类型
数据库·redis
Jay_fearless10 小时前
Redis SpringBoot项目学习
spring boot·redis