0%

Redis的持久化以及选择

为什么使用 Redis,主要是它提供了一个高速的缓存,还有通知,及事务的支持,最重要的是,它够高效。因此,在做一些游戏开发的时候就打算用它来做存储,当然,主要还做一个缓存。持久化还是需要考虑到文件或者是其他的关系数据库里面去,方便进行数据的分析和运营。

内存的消耗

Redis 将所有的数据都放在内存,因此,如果数据量非常庞大的话,使用内存起来就是一个不小的开销了。当然,数据量小的话那就没什么说的了。针对这个问题,就有了不同的处理方式:

  • 基于 ssd 硬盘的 kv 数据库,如 ssdb,将数据放在硬盘中而不是内存中,当然有部分会进行缓存,这个利用了 Google 的 LevelDb 库。
  • 冷热数据分离。对于不活跃的数据,那么就把它进行持久化(到数据库,或者是文件)。活跃与否,可以给数据设置一个有效期。而这,就又涉及到了一个如何进行持久化的问题。

持久化

Redis 本身,在几种持久化模式:

  • RDB,Redis Database:在这种模式下,Redis 会以特定的周期对我们内存中的数据集做快照。
  • AOF,Append Only File:AOF 会将服务器收到的每个写操作写日志,在服务器重启的时候进行重放来重建原始的数据集。命令记录的格式和 Redis 协议一致。当日志变得巨大的时候,Redis 可以在后台重写。
  • RDB+AOF,将两者结合起来。但要注意的是,在重启的时候,Redis 会优先读取 RDF 文件,因为它提供了更多的完整性。

我们需要命令, RDF 和 AOF 间的权衡。

RDB VS AOF

RDB 优点

  • 它是对我们 Redis 数据库在某个时间点代紧凑的单个文件表示。用来备份是完美的。例如,我们可能会在过去的 24 小时内每小时归档一次 RDF 文件,30 天内每天备份一次。这就让我们可以很容易的恢复不同版本的数据。
  • 单一文件方便进行灾备恢复。
  • RDB 看起来会提高 Redis 的性能,为了进行 RDF 持久化,Redis 只需要 fork 一个子进程,剩下的工作就交给子进程了。那么 Redis 实例也就不会进行磁盘的 I/O。
  • 相对 AOF 而言,RDB 在面对大数据集时启动更快。
  • 在复制中,RDB 支持在重启和故障转移后的部分重新同步。

RDB 的劣势

  • 在我们需要减少数据丢失时,RDB 就不太好了(比如说断电)。我们可以配置不同的保存点,在这个点就会产生一个 RDB(比如是在最少 5 分钟和 100 个写操作到数据后进行,当然,我们可以有多个保存点)。然而,那我们就会每 5 分钟就产生一个 RDB 快照,Redis 要是崩了,那就会丢失这几分钟的数据。
  • RDB 通常需要 fork() 子进程来进持久化到磁盘的工作。数据集大的情况下, fork 是比较消耗时间的;当数据非常大而 CPU 性能不太好时 Redis 可能会停止工作几毫秒或者是 1 秒。AOF 也需要 fork,但我们可以配置我们重写日志的频率而不需要在持续时间上进行让步。

AOF 优点

  • AOF 会更耐久一些:我们可以有不同的 fsync 策略:
    • 不进行 fsync
    • 每秒进行 fsync
    • 每个查询进行 fsync
      默认情况下是每秒进行 fsync,性能也不错(fsync 会通过后台线程完成,在没有进行中的 fsync 时,主线程会全力进行写出操作)我们只会丢失一秒的数据。
  • AOF 是一个文件尾写的操作,没有移位,在掉电的时候也不会有错误的问题。即使日志会有一个半写完的命令(磁盘满了或其他原因),redis-check-aof 工具会把它进行修复。
  • 当 AOF 变得太大的时候,Redis 可以自动的在后台重写。重写是完全安全的因为 Redis 依然是在老的文件后面写,一个完整的新文件会以建立当前数据集所需要的最少操作来产生,一旦第二个文件就绪,Redis 才进行切换和写入到新文件。
  • AOF 日志包含所有操作的日志,非常易于理解和解析。若是我们不小心的使用了 flushall 命令来刷新所有东西,而还没有重新日志,那噩梦可以直接停止服务器,删除最新的命令,然后重启 Redis。

AOF 劣势

  • 文件会比 RDB 大。
  • 依据 fsync 策略,可能会比 RDP 慢。通常,每秒进行 fsync 性能是非常高的,而如果禁止了 fsync,那么就和 RDB 一样高效(即使负载很高)。当然,RDB 在有很大的写负载的时候也能提供最大延迟的保证。
  • 过去,我们遇到一些命令的不常见的 BUG 导致 AOF 不能重新正确的构造数据集。

问题在哪里?

那么,到底问题在哪里? Redis 是一个内存库,它的所有数据一旦面临硬件问题,那么可能你就无法恢复。所以,持久化是一个无法绕开的问题;同时,数据量大的时候,内存也是一笔非常大的开支。所以解决问题的思路是:

  1. Redis 只保存活跃的数据(以及一些时间相关的,不方便存在关系数据库内的),减少内存的消耗。
  2. Redis 中的数据,只作为缓存存在,关闭 AOF, RDP 设置适当的值。这样做是为了避免 Redis 出现缓存雪崩的情况。
  3. 开一个代理进程来负责持久化和 Redis 间的判断。

代理模式

@startuml A
!include C4_Dynamic.puml
skinparam linetype ortho
skinparam ranksep 100
skinparam nodesep 150
AddRelTag("read",$textColor="blue",$lineColor="blue",$techn="read")
AddRelTag("write",$textColor="red",$lineColor="red",$techn="write")
Container(redis,"Redis")
Container(mysql,"MySQL")
Container(skynet,"Skynet")
Container(proxy,"Proxy")

Rel(skynet,proxy,"Read",$tags="read")
Rel_L(redis,proxy,"get",$tags="read")
Rel_D(mysql,proxy,"query",$tags="read")
Rel_R(proxy,redis,"cache ttl",$tags="read")
Rel(proxy,skynet,"return",$tags="read")


Rel(skynet,proxy,"save",$tags="write")
Rel_U(proxy,mysql,"save",$tags="write")
Rel_R(proxy,redis,"set",$tags="write")

SHOW_LEGEND()
@enduml

旁路模式

@startuml A
!include C4_Dynamic.puml
skinparam linetype ortho
skinparam ranksep 100
skinparam nodesep 100
AddRelTag("read",$textColor="blue",$lineColor="blue",$techn="read")
AddRelTag("write",$textColor="red",$lineColor="red",$techn="write")
Container(redis,"Redis")
Container(mysql,"MySQL")
Container(skynet,"Skynet")
Container(proxy,"Persist Service")

Rel_U(skynet,redis,"save",$tags="write")
Rel_R(redis,proxy,"notify",$tags="write")
Rel_R(proxy,mysql,"save",$tags="write")

Rel_D(redis,skynet,"hit cache",$tags="read")
Rel_D(mysql,skynet,"not cached",$tags="read")
Rel_U(skynet,redis,"set ttl")

SHOW_LEGEND()
@enduml

两种模式的区别

  • 两者都是将 Redis 作为缓存,只是持久化的地方不同。
  • 旁路模式持久化由专门服务进行完成,由 Redis 的消息来保证数据的一致性。
  • 代理模式的代理程序做所有工作,读写不分离。
  • 我还是喜欢后一种模式。笔记可以利用上 Redis 的很多东西也不容易出现单点问题。比如,修改缓存就可以实现存储落地,通知到游戏。