redis持久化、主从和哨兵模式详解

Redis持久化:

RDB快照(snapshot)

在默认情况下,redis将内存数据快照保存到当前目录的dump.rdb的二进制文件;其默认的保存策略如下图所示
redis持久化、主从和哨兵模式详解_第1张图片
“n秒内有m个变动”满足这一条件,将会保存一次redis进程中的所有数据,比如上图中第一个策略就是900秒内有1次改动就会触发备份数据;可以配置多个策略,满足任意策略就会触发保存机制;(关闭rdb,只需要注释掉这些策略即可)

也可以手动执行生成RDB文件的命令:save和bgsave;在客户端执行这两个命令可以生成dump.rdb文件,每次执行命令都会把redis内存数据快照到新的rdb文件里,并覆盖原有的rdb文件。

bgsave使用的是写时复制(COW)机制实现快照的:
redis利用操作系统提供的写时复制技术(多进程技术copy-on-write)来实现生成快照的同时,不影响正常处理客户端发过来的命令;
当执行bgsave命令时,主进程会调用操作系统的fork函数来生成一个子进程,fork出来的子进程会共享主进程的的内存数据的;fork出的子进程运行后,开始读取主进程内存中的数据,并把这些数据序列化写入RDB文件;
如果这时候对一块数据发生了修改,这块数据会被复制出来一份,生成该数据页的副本,bgsave子进程会把这个副本数据写入RDB文件,而这个过程中主线程仍然可以对这块数据继续修改。

save与bgsave命令给对比:
redis持久化、主从和哨兵模式详解_第2张图片
RDB和AOF默认的持久化文件地址,可以自定义
redis持久化、主从和哨兵模式详解_第3张图片

AOf(append-only file):

从名字可以看出是“文件追加数据”,也就是AOF文件存的是客户端修改数据的一系列命令;
RDB快照功能并不是那么的耐久,如果redis因为某些原因宕机,那么将会丢失最近修改且尚未保存到快照中的那些数据;从redis1.1版本开始,增加完全耐久的持久化方式:AOF持久化方式,将修改数据的每一条指令按顺序都保存到appendonly.aof中(先写入os cache中,再写入磁盘文件中)。

AOF功能的打开需要修改redis.conf配置文件:修改成yes就行了,默认是no;从打开AOF配置后开始把修改数据的命令追加到aof文件;
redis持久化、主从和哨兵模式详解_第4张图片
AOF持久化的策略有三种,默认是everySec;
1.appendfsync always :每次修改数据,都需要将命令追加到AOF文件,效率低,但是非常安全;
2.appendfsync everysec:每秒fsync一次,也是默认的策略,足够快,就算宕机,最多丢失1秒的数据;
3.appendfsync no:自己从不fsync,数据的同步交给操作系统来处理,性能高,但是安全性不能保证;
redis持久化、主从和哨兵模式详解_第5张图片
比如执行命令:set bijian 123

则在aof文件中存储的数据为:
redis持久化、主从和哨兵模式详解_第6张图片
这是一种resp格式数据,*后的数字代表命令的参数个数,$后代表这个参数有几个字符组成;

AOF文件重写:
aof文件里可能有太多无用的命令,比如:
redis持久化、主从和哨兵模式详解_第7张图片
重写后的aof文件直接记录的是set mycount 5的命令,因为这样可以减小aof文件的大小,从而更快的被redis加载,redis在重启时首选加载的数据的文件就是aof,但是加载时间比rdb长,不过较安全;

通过如下配置来控制AOF文件的重写频率:
auto-aof-rewrite-percentage 100 //文件自上一次重写之后,文件大小增长100%,则会再次进行重新;
auto-aof-rewrite-min-size 64mb //文件大小至少达到64M才会发生重写;

可以手动进行重写:
在客户端输入:bgrewriteaof;(也会fork出一个子进程来重写AOF文件,与bgsave命令类似);

AOF与RDB应该选哪一个?
redis持久化、主从和哨兵模式详解_第8张图片
生成环境都可以启动,
Redis启动时如果既有RDB文件又有AOF文件,则优先选择AOF文件来恢复数据,因为aof数据更安全一些。

Redis4.0混合持久化:
重启redis时,我们一般选择AOF来加载重放数据,因为安全性更高,但是相对于RDB来时,重放数据要更慢,启动需要花费更长的时间。Redis4.0为了解决这一问题,带来了新的持久化选项-混合持久化。通过如下配置开启的(必须先开启aof):
aof-use-rdb-preamble yes;
如果开启了混合持久化,则在AOF重写时,不会单纯的将数据按resp命令的方式写入文件,而是将重写这一刻之前的内存数据按照RDB快照的方式进行存储,并且重写这一刻之后的数据还是以RESP命令的方式存储的AOF文件中,所以RDB快照数据和RESP格式的命令一同存在aof文件中;新的文件不叫appendonly.aof,等到重写完成,才会改明成这个文件,覆盖原来的aof文件完成替换;

Redis备份策略:
1、写一个crontab定时任务调度脚本,每小时都copy一份RDB或AOF到备份目录中去,仅仅保留之前48小时之内的备份;
2、每天都备份一份当日的数据到备份目录中去,可以保留最近一个月的备份;
3、每次备份的时候,把很旧的备份进行删除;
4、每天晚上将备份文件复制到其它机器上,以防机器损坏;

Redis主从架构:
redis持久化、主从和哨兵模式详解_第9张图片
Redis主从架构搭建配置:
搭建一个master端口号为6379 slave端口号为6380的伪主从架构
1、复制一份redis.conf文件
2、将相关配置修改为如下值:
port 6380
pidfile /var/run/redis_6380.pid # 把pid进程号写入pidfile配置的文件
logfile “6380.log”
dir /usr/local/redis‐5.0.3/data/6380 # 指定数据存放目录,包括rdb和aof文件
#需要注释掉bind
#bind127.0.0.1(bind绑定的是自己机器网卡的ip,如果有多块网卡可以配多个ip,代表允许客户端通 过机器的哪些网卡ip去访问,内网一般可以不配置bind,注释掉即可)
3、配置主从复制
replicaof 192.168.244.129 6379 # 从本机6379的redis实例复制数据,Redis 5.0之前使用slaveof replica‐read‐only yes # 配置从节点只读
启动从节点
redis‐server redis.conf
连接从节点
redis‐cli ‐p 6380
测试在6379实例上写数据,6380实例是否能及时同步新修改数据
可以自己再配置一个6381的从节点

Redis主从工作原理:
1、当为一个master节点配置了一个slave节点后,不管这个slave节点是否第一次连接到master,slave都会发送一个psync命令给master请求复制数据;
2、master收到psync命令后,会在后台通过bgsave命令生成RDB文件进行数据持久化,持久化期间会继续接受客户端的请求,它把这期间可能对数据修改的命令缓存到内存中
3、当持久化完成之后,master会将这份rdb文件发送给slave,slave会将rdb文件保存到磁盘上并读取该文件将数据加载到内存,(如果是第一次进行数据同步,则会丢弃自己的持久化文件)然后再加载到内存中
4、最后master再将之前缓存到内存中的命令发送给slave;

主从复制(全量复制)流程图:
redis持久化、主从和哨兵模式详解_第10张图片

部分复制:
Redis2.8之前,但master和slave断开连接以后,不支持部分复制,只能全量复制;但从redis2.8版本之后就可以支持部分数据复制的命令psync去master同步数据,断开连接后只进行部分数据复制(断点续传);
Master会在其内存中创建一个复制数据用的队列 ,缓存最近一段时间的数据,master和slave都维护了复制数据的下标offset和master进程id,;
当断开连接后,slave再次连接进行同步请求时,会携带最后同步位置的下标和master的进程id去继续进行未完成的复制,从所请求的下标开始;
如果master的进程id发生了变化,或携带的请求数据起始位置的下标太旧,已经不再master的缓存队列中,那么会进行一次全量复制;
主从复制(部分复制,断点续传)流程图
redis持久化、主从和哨兵模式详解_第11张图片
如果有很多从节点,为了缓解主从复制风暴(多个从节点同时复制主节点,导致主节点压力过大),可以让部分从节点与从节点同步;
redis持久化、主从和哨兵模式详解_第12张图片

Redis管道(Pipeline)和Lua脚本的使用

管道(Pipelice):
介绍:
我们一般使用redis是发送一次命令等待结果,然后再发送下一个命令;但是使用Pipeline可以一次性发送多个命令,并且不用等待服务器的响应;待所有命令都发送完以后,再读取服务器的响应结果,这样可以大大降低多条命令执行的网络开销;
需要注意的是用Pipeline打包方式发送命令,redis在处理完所有命令之前必须先缓存已完成的命令结果,所以打包命令越多,缓存消耗内存也越多,不是打包命令越多越好;
如果其中的一个命令执行失败,不会影响后面的命令执行,会在此后得到服务器得到的相应信息中有这个失败命令返回的相关报错信息;

Lua脚本:

Redis2.6推出了脚本功能,允许开发者编写Lua脚本传到Redis中执行;使用脚本的好处有:
减少网络开销:多个命令可以放在一个Lua脚本中执行,和管道类似;
原子操作:Redis会将整个脚本作为一个整体来执行,中间不会被其他命令插入;管道不是原子的,但是mget、mset等式原子的;
代替redis的事务功能:脚本上的所有操作要么都执行成功要么都执行失败;

从redis2.6.0开始,通过内置的Lua解释器,可以使用EVAL命令的Lua脚本进行执行,格式如下:
EVAL script numkeys key [key …] arg [arg …]

script参数是一段Lua脚本程序,它会被运行在Redis服务器上下文中,这段脚本不必(也不应该)定义为一 个Lua函数。numkeys参数用于指定键名参数的个数。键名参数 key [key …] 从EVAL的第三个参数开始算 起,表示在脚本中所用到的那些Redis键(key),这些键名参数可以在 Lua中通过全局变量KEYS数组,用1维基址的形式访问( KEYS[1] , KEYS[2] ,以此类推)。
在命令的最后,那些不是键名参数的附加参数 arg [arg …] ,可以在Lua中通过全局变量ARGV数组访问,访问的形式和KEYS变量类似( ARGV[1] 、 ARGV[2] ,诸如此类)。例如
在这里插入图片描述
其中 “return {KEYS[1],KEYS[2],ARGV[1],ARGV[2]}” 是被求值的Lua脚本,数字2指定了键名参数的数
量, key1和key2是键名参数,分别使用 KEYS[1] 和 KEYS[2] 访问,而最后的 first 和 dssd则是附加
参数,可以通过 ARGV[1] 和 ARGV[2] 访问它们。

在 Lua 脚本中,可以使用redis.call()函数来执行Redis命令。
注意:不要在Lua脚本中出现死循环和耗时的运算,否则redis将阻塞,不能接受其它命令;redis
是单进程、单线程执行脚本;管道不会阻塞redis;
Jedis连接代码示例:
引入相关依赖

<dependency>
    <groupId>redis.clients</groupId>
    <artifactId>jedis</artifactId>
    <version>2.9.0</version>
</dependency>

代码演示:

public class JedisDemo {

    public static void main(String[] args) throws IOException {

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

        // timeout,这里既是连接超时又是读写超时,从Jedis 2.8开始有区分connectionTimeout和soTimeout的构造函数
        JedisPool jedisPool = new JedisPool(jedisPoolConfig, "192.168.244.129", 6379, 3000, null);

        Jedis jedis = null;
        try {
            //从redis连接池里拿出一个连接执行命令
            jedis = jedisPool.getResource();

            System.out.println(jedis.set("bijian", "6666"));
            System.out.println(jedis.get("bijian"));

            //管道示例
            //管道的命令执行方式:cat redis.txt | redis-cli -h 127.0.0.1 -a password - p 6379 --pipe
            Pipeline pl = jedis.pipelined();
            for (int i = 0; i < 10; i++) {
                pl.incr("pipelineKey");
                pl.set("bijian" + i, "bijian");
            }
            List<Object> results = pl.syncAndReturnAll();
            System.out.println(results);

            //lua脚本模拟一个商品减库存的原子操作
            //lua脚本命令执行方式:redis-cli --eval /tmp/test.lua , 10
            jedis.set("product_count_10016", "15");  //初始化商品10016的库存
            String script = " local count = redis.call('get', KEYS[1]) " +
                            " local a = tonumber(count) " +
                            " local b = tonumber(ARGV[1]) " +
                            " if a >= b then " +
                            "   redis.call('set', KEYS[1], a-b) " +
                            "   return 1 " +
                            " end " +
                            " return 0 ";
            Object obj = jedis.eval(script, Arrays.asList("product_count_10016"), Arrays.asList("10"));
            System.out.println(obj);

        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            //注意这里不是关闭连接,在JedisPool模式下,Jedis会被归还给资源池。
            if (jedis != null)
                jedis.close();
        }
    }
}

Redis哨兵高可用架构

redis持久化、主从和哨兵模式详解_第13张图片

Sentinel哨兵也属于redis实列,是特殊的redis服务,但是不提供任何读取操作数据的服务;主要用来监控redis实例;
哨兵架构下,client第一次通过哨兵来找到redis的主节点,后续就会直接访问redis的主节点,不会每次都通过sentinel代理访问redis主节点;
当redis节点发生变化时,哨兵会第一时间感知到,并将新的redis主节点通知给client端(client端一般都实现了订阅功能,订阅sentinel发布的节点变动信息)

redis哨兵架构搭建步骤:
1、复制一份sentinel.conf文件
cp sentinel.conf sentinel-26380.conf
2、将相关配置修改为如下值:
port 263780
daemonize yes
pidfile “/var/run/redis-sentinel-26380.pid”
logfile “26380.log”
dir “/usr/local/redis-5.0.3/data”
#sentinel monitor
#quorum是一个数字,指明当有多少个sentinel认为一个master失效时(值一般为:sentinel总数/2 + 1),master才算真正失效
sentinel monitor mymaster 192.168.244.129 6379 2
#mymaster这个名字随便取,客户端访问时会用到
3、启动sentinel哨兵实例
src/redis-sentinel sentinel-26379.conf
4、查看sentinel的info信息
src/redis-cli -p 26379 127.0.0.1:26379>info 可以看到Sentinel的info里已经识别出了redis的主从
5、可以自己再配置两个sentinel,端口26380和26381,注意上述配置文件里的对应数字都要修改

sentinel集群都启动完毕后,会将哨兵集群的元数据信息写入所有sentinel的配置文件里去(追加在文件的最下面),我们查看下如下配置文件:
sentinel-26379.conf,如下所示:
sentinel known-replica mymaster 127.0.0.1 6381 #代表redis主节点的从节点信息

sentinel known-replica mymaster 127.0.0.1 6380 #代表redis主节点的从节点信息

sentinel known-sentinel mymaster 192.168.244.129 26379 246389a6398fa819624215beca2bda34717e8612 #代表感知到的其它哨兵节点

sentinel known-sentinel mymaster 192.168.244.129 26381 71e5a5eedfcd4bf7b8e928d8166eaa0f0f99024d #代表感知到的其它哨兵节点

当redis主节点如果挂了,哨兵集群会重新选举出新的redis主节点,同时会修改所有sentinel节点配置文件的集群元数据信息,比如6379的redis如果挂了,假设选举出的新主节点是6381,则sentinel文件里的集群元数据信息会变成如下所示:(因为是伪哨兵架构,所以自己选出的主节点地址会使用127.0.0.1,自测的时候会出现问题)
sentinel known-replica mymaster 192.168.244.129 6379
sentinel known-replica mymaster 127.0.0.1 6380
sentinel known-sentinel mymaster 127.0.0.1 26379 246389a6398fa819624215beca2bda34717e8612
sentinel known-sentinel mymaster 192.168.244.129 26381 71e5a5eedfcd4bf7b8e928d8166eaa0f0f99024d

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

sentinel monitor mymaster 127.0.0.1 6381 2

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

哨兵的Jedis连接代码:

public class RedisSentineDemo {
    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.244.129",26379).toString());
        sentinels.add(new HostAndPort("192.168.244.129",26380).toString());
        sentinels.add(new HostAndPort("192.168.244.129",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("bijian", "6666"));
            System.out.println(jedis.get("bijian"));
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            //注意这里不是关闭连接,在JedisPool模式下,Jedis会被归还给资源池。
            if (jedis != null)
                jedis.close();
        }
    }
}

哨兵的Spring Boot整合Redis连接代码见示例项目:redis-sentinel-cluster

引入相关依赖:

<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项目核心配置:

server:
    port: 8080

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

代码案例:

@RestController
@Slf4j
public class RedisController {
    @Autowired
    private StringRedisTemplate stringRedisTemplate;

    /**
     * 测试节点挂了哨兵重新选举新的master节点,客户端是否能动态感知到
     * 新的master选举出来后,哨兵会把消息发布出去,客户端实际上是实现了一个消息监听机制,
     * 当哨兵把新master的消息发布出去,客户端会立马感知到新master的信息,从而动态切换访问的masterip
     *
     * @throws InterruptedException
     */
    @RequestMapping("/testSentinel")
    public void testSentinel() throws InterruptedException {

        stringRedisTemplate.opsForValue().set("bijianiii", "rrrrr");
        String value = stringRedisTemplate.opsForValue().get("bijianiii");
        System.out.println("获取的value=:" + value);
    }
}

StringRedisTemplate与RedisTemplate详解
spring 封装了 RedisTemplate 对象来进行对redis的各种操作,它支持所有的 redis 原生的 api。在RedisTemplate中提供了几个常用的接口方法的使用,分别是:
private ValueOperations valueOps;
private HashOperations hashOps;
private ListOperations listOps;
private SetOperations setOps;
private ZSetOperations zSetOps;

RedisTemplate中定义了对5种数据结构操作
redisTemplate.opsForValue();//操作字符串 redisTemplate.opsForHash();//操作hash redisTemplate.opsForList();//操作list redisTemplate.opsForSet();//操作set redisTemplate.opsForZSet();//操作有序set

StringRedisTemplate继承自RedisTemplate,也一样拥有上面这些操作。
StringRedisTemplate默认采用的是String的序列化策略,保存的key和value都是采用此策略序列化保存的。
RedisTemplate默认采用的是JDK的序列化策略,保存的key和value都是采用此策略序列化保存的。

你可能感兴趣的:(redis,java)