Redis - 02持久化和主从哨兵架构

130 阅读15分钟

本文已参与「新人创作礼」活动,一起开启掘金创作之路。

Redis - 持久化和主从哨兵架构

Redis持久化机制

RDB快照

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

生成RDB快照的配置

  1. 自动生成

    • 你可以对 Redis 进行设置, 让它在“ N 秒内数据集至少有 M 个改动”这一条件被满足时, 自动保存一次数据集。

      • 比如说, 以下设置会让 Redis 在满足“ 60 秒内有至少有 1000 个键被改动”这一条件时, 自动保存一次数据集:

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

  2. 手动生成

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

RDB快照的特点

  1. 体积小,恢复速度快
  2. 容易丢失数据
    • 缺点:快照功能并不是非常耐久(durable),如果Redis因为某些原因而造成故障停机,那么服务器将丢失最近写入、且仍未保存到快照中的那些数据。

bgsave 的写时复制(COW)机制

  • Redis 借助操作系统提供的写时复制技术(Copy-On-Write, COW),在生成快照的同时,依然可以正常处理写命令。

简单来说,bgsave 子进程是由主线程 fork 生成的,可以共享主线程的所有内存数据。bgsave 子进程运行后,开始读取主线程的内存数据,并把它们写入 RDB 文件。此时,如果主线程对这些数据也都是读操作,那么,主线程和 bgsave 子进程相互不影响。

  • 但是,如果主线程要修改一块数据,那么,这块数据就会被复制一份,生成该数据的副本。然后,bgsave 子进程会把这个副本数据写入 RDB 文件,而在这个过程中,主线程仍然可以直接修改原来的数据。

save与bgsave对比:

命令savebgsave
IO类型同步异步
是否阻塞redis其它命令否(在生成子进程执行调用fork函数时会有短暂阻塞)
复杂度O(n)O(n)
优点不会消耗额外内存不阻塞客户端命令
缺点阻塞客户端命令需要fork子进程,消耗内存
  • 配置自动生成rdb文件后台使用的是bgsave方式。

AOF持久化

  • 完全耐久的持久化方式: AOF 持久化,将修改的每一条指令记录进文件appendonly.aof中(先写入os cache,每隔一段时间fsync到磁盘)

AOF的用法:

  • 你可以通过修改配置文件来打开 AOF 功能:

    # appendonly yes

    • 从现在开始, 每当 Redis 执行一个改变数据集的命令时(比如 SET), 这个命令就会被追加到 AOF 文件的末尾。
    • 当 Redis 重新启动时, 程序就可以通过重新执行 AOF 文件中的命令来达到重建数据集的目的。
  • 你可以配置 Redis 多久才将数据 fsync 到磁盘一次。

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

AOF的特点

  • 完全耐久的持久化方式:AOF持久化
  • 缺点:体积大,恢复速度慢

AOF重写

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

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

    # auto-aof-rewrite-min-size 64mb   //aof文件至少要达到64M才会自动重写,文件太小恢复速度本来就很快,重写的意义不大
    # auto-aof-rewrite-percentage 100  //aof文件自上一次重写后文件大小增长了100%则再次触发重写
    
    • 当然AOF还可以手动重写,进入redis客户端执行命令 bgrewriteaof重写AOF

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

RDB 和 AOF ,我应该用哪一个?

命令RDBAOF
启动优先级
体积
恢复速度
数据安全性容易丢数据根据策略决定
  • 生产环境可以都启用,redis启动时如果既有rdb文件又有aof文件则优先选择aof文件恢复数据,因为aof一般来说数据更全一点。

Redis4.0 混合持久化

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

  • 混合持久化原理:

    • 混合持久化改进的是 AOF重写流程
    • AOF在重写时,不再是单纯将内存数据转换为RESP命令写入AOF文件,而是将重写这一刻之前的内存做RDB快照处理,并且将RDB快照内容和增量的AOF修改内存数据的命令存在一起,都写入新的AOF文件,新的文件一开始不叫appendonly.aof,等到重写完新的AOF文件才会进行改名,覆盖原有的AOF文件,完成新旧两个AOF文件的替换。
    • 于是在 Redis 重启的时候,可以先加载 RDB 的内容,然后再重放增量 AOF 日志就可以完全替代之前的 AOF 全量文件重放,因此重启效率大幅得到提升。
  • 混合持久化AOF文件结构如下

image.png

Redis数据备份的几种策略

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

Redis主从架构

image.png

主从复制 工作原理

  • 如果你为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。

主从复制(全量复制)流程图:

image.png

数据部分复制:

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

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

  • 主从复制(部分复制,断点续传)流程图:

image.png

主从复制风暴

  • 多个从节点同时复制主节点导致主节点压力过大

如果有很多从节点,为了缓解主从复制风暴(多个从节点同时复制主节点导致主节点压力过大),

可以做如下架构,让部分从节点与从节点(与主节点同步)同步数据

image.png

Redis 管道(Pipeline)

  • 管道的定义:

    客户端可以一次性发送多个请求而不用等待服务器的响应,待所有命令都发送完后再一次性读取服务的响应

    这样可以极大的降低多条命令执行的网络传输开销,管道执行多条命令的网络开销实际上只相当于一次命令执行的网络开销。

  • 需要注意到是用pipeline方式打包命令发送,redis必须在处理完所有命令前先缓存起所有命令的处理结果。打包的命令越多,缓存消耗内存也越多。

    所以并不是打包的命令越多越好。

  • 管道中前面命令失败,后面命令不会有影响,继续执行。 不具备原子性

Redis Lua脚本(原子性)

  • Redis在2.6推出了脚本功能,允许开发者使用Lua语言编写脚本传到Redis中执行。

  • 使用脚本的好处如下:

    1. 减少网络开销:本来5次网络请求的操作,可以用一个请求完成,原先5次请求的逻辑放在redis服务器上完成。使用脚本,减少了网络往返时延。这点跟管道类似
    2. 原子操作:Redis会将整个脚本作为一个整体执行,中间不会被其他命令插入。管道不是原子的,不过redis的批量操作命令(类似mset)是原子的。
    3. 替代redis的事务功能:redis自带的事务功能很鸡肋,而redis的lua脚本几乎实现了常规的事务功能,官方推荐如果要使用redis的事务功能可以用redis lua替代。
  • 在 Lua 脚本中,可以使用redis.call()函数来执行Redis命令

    
    jedis.set("product_stock_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_stock_10016"), Arrays.asList("10"));
    System.out.println(obj);
    
    
  • 注意,不要在Lua脚本中出现死循环和耗时的运算,否则redis会阻塞,将不接受其他的命令, 所以使用时要注意不能出现死循环、耗时的运算。 redis是单进程、单线程执行脚本。管道不会阻塞redis。

Redis哨兵高可用架构

image.png

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

  • 哨兵架构下:

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

当redis主节点如果挂了,哨兵集群会重新选举出新的redis主节点,同时会修改所有sentinel节点配置文件的集群元数据信息,

比如6379的redis如果挂了,假设选举出的新主节点是6380,则sentinel文件里的集群元数据信息会变成如下所示:

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

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

StringRedisTemplate与RedisTemplate详解

spring 封装了 RedisTemplate 对象来进行对redis的各种操作,它支持所有的 redis 原生的 api。

RedisTemplate

在RedisTemplate中提供了几个常用的接口方法的使用,分别是:

private ValueOperations<K, V> valueOps;
private HashOperations<K, V> hashOps;
private ListOperations<K, V> listOps;
private SetOperations<K, V> setOps;
private ZSetOperations<K, V> 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都是采用此策略序列化保存的。

工作中用的是RedisTemplate;使用如下代码配置序列化方式:

 /**
     * 设置RedisTemplate的序列化方式
     *
     * @param redisTemplate
     */
    public void setSerializer(RedisTemplate redisTemplate) {
        StringRedisSerializer stringRedisSerializer = new StringRedisSerializer();
        //设置键(key)的序列化方式
        redisTemplate.setKeySerializer(stringRedisSerializer);
        //设置值(value)的序列化方式
        redisTemplate.setValueSerializer(stringRedisSerializer);
        redisTemplate.setHashKeySerializer(stringRedisSerializer);
        redisTemplate.setHashValueSerializer(stringRedisSerializer);
        redisTemplate.afterPropertiesSet();
    }

RedisTemplate使用的区别

RedisTemplate使用的序列类在在操作数据的时候,比如说存入数据会将数据先序列化成字节数组然后在存入Redis数据库,这个时候打开Redis查看的时候,你会看到你的数据不是以可读的形式展现的,而是以字节数组显示,类似下面(RedisTemplate)

image.png

当然从Redis获取数据的时候也会默认将数据当做字节数组转化,这样就会导致一个问题,当需要获取的数据不是以字节数组存在redis当中而是正常的可读的字符串的时候,比如说下面这种形式的数据(StringRedisTemplate)

image.png

  • 当Redis当中的数据值是以数组形式显示出来的时候,只能使用RedisTemplate才能获取到里面的数据。

  • 当Redis当中的数据值是以可读的形式显示出来的时候,只能使用StringRedisTemplate才能获取到里面的数据。

  • 所以当你使用RedisTemplate获取不到数据为NULL时,一般是获取的方式错误。检查一下数据是否可读即可。

  • 使用总结

    • StringRedisTemplate:当你的redis数据库里面本来存的是字符串数据或者你要存取的数据就是字符串类型数据的时候。

    • RedisTemplate:但是如果你的数据是复杂的对象类型,而取出的时候又不想做任何的数据转换,直接从Redis里面取出一个对象。(使用RedisTemplate时,可以使用上面设置RedisTemplate的序列化方式,转为String序列化 )

Redis客户端命令对应的RedisTemplate中的方法列表:

String类型结构
RedisRedisTemplate rt
set key valuert.opsForValue().set("key","value")
get keyrt.opsForValue().get("key")
del keyrt.delete("key")
strlen keyrt.opsForValue().size("key")
getset key valuert.opsForValue().getAndSet("key","value") // 获取key对应的原来值,并重新赋值
getrange key start endrt.opsForValue().get("key",start,end)
append key valuert.opsForValue().append("key","value")
SETNX //原子性rt.opsForVaue().setIfAbsent (“key”,"value") //如果为空就set值,并返回1;如果存在(不为空)就不操作,返回0
Hash结构
hmset key field1 value1 field2 value2...rt.opsForHash().putAll("key",map) //map是一个集合对象
hset key field valuert.opsForHash().put("key","field","value")
hexists key fieldrt.opsForHash().hasKey("key","field")
hgetall keyrt.opsForHash().entries("key") //返回Map对象
hvals keyrt.opsForHash().values("key") //返回List对象
hkeys keyrt.opsForHash().keys("key") //返回List对象
hmget key field1 field2...rt.opsForHash().multiGet("key",keyList)
hsetnx key field valuert.opsForHash().putIfAbsent("key","field","value"
hdel key field1 field2rt.opsForHash().delete("key","field1","field2")
hget key fieldrt.opsForHash().get("key","field")
List结构
lpush list node1 node2 node3...rt.opsForList().leftPush("list","node")
rt.opsForList().leftPushAll("list",list) //list是集合对象
rpush list node1 node2 node3...rt.opsForList().rightPush("list","node")
rt.opsForList().rightPushAll("list",list) //list是集合对象
lindex key indexrt.opsForList().index("list", index)
llen keyrt.opsForList().size("key")
lpop keyrt.opsForList().leftPop("key")
rpop keyrt.opsForList().rightPop("key")
lpushx list nodert.opsForList().leftPushIfPresent("list","node")
rpushx list nodert.opsForList().rightPushIfPresent("list","node")
lrange list start endrt.opsForList().range("list",start,end)
lrem list count valuert.opsForList().remove("list",count,"value")
lset key index valuert.opsForList().set("list",index,"value")
Set结构
sadd key member1 member2...rt.boundSetOps("key").add("member1","member2",...)
rt.opsForSet().add("key", set) //set是一个集合对象
scard keyrt.opsForSet().size("key")
sidff key1 key2rt.opsForSet().difference("key1","key2") //返回一个集合对象
sinter key1 key2rt.opsForSet().intersect("key1","key2")//同上
sunion key1 key2rt.opsForSet().union("key1","key2")//同上
sdiffstore des key1 key2rt.opsForSet().differenceAndStore("key1","key2","des")
sinter des key1 key2rt.opsForSet().intersectAndStore("key1","key2","des")
sunionstore des key1 key2rt.opsForSet().unionAndStore("key1","key2","des")
sismember key memberrt.opsForSet().isMember("key","member")
smembers keyrt.opsForSet().members("key")
spop keyrt.opsForSet().pop("key")
srandmember key countrt.opsForSet().randomMember("key",count)
srem key member1 member2...rt.opsForSet().remove("key","member1","member2",...)