孙宇的技术专栏 大数据/机器学习

Redis 基础/持久化

2015-02-13

阅读:


Redis 提供了可以持久化的缓存服务。这是和 memcached 最大的差别。同时它的数据类型又更为丰富。可以应对更复杂的业务场景。

Redis 还具备可以做分布式锁等其他功能,但是如果只是为了分布式锁这些其他功能,完全还有其他中间件,如 ZooKpeer ,并不是非要使用 Redis。

类似微博的粉丝数、关注数、文章数等这些数据,如果从DB查询然后再显示,就会太慢了。通常是存到缓存中。

单线程机制

Redis 是单线程工作模型。只有单个线程,通过跟踪每个 I/O 流的状态,来管理多个 I/O 流。

就是我们的 redis-client 在操作的时候,会产生具有不同事件类型的 Socket。

在服务端,有一段 I/O 多路复用程序,将其置入队列之中。然后,文件事件分派器,依次去队列中取,转发到不同的事件处理器中。这个 I/O 多路复用机制,Redis 还提供了 select、epoll、evport、kqueue 等多路复用函数库。

数据淘汰机制

Redis 采用的是定期删除+惰性删除策略

最简单的一种方式是定时删除,用一个定时器来负责监视 Key,过期则自动删除。虽然内存及时释放,但是十分消耗 CPU 资源。

Redis 默认每个 100ms 检查,是否有过期的 Key,有过期 Key 则删除。Redis 不是每个 100ms 将所有的 Key 检查一次,而是随机抽取进行检查,这样可以避免卡死。

因此,如果只采用定期删除策略,会导致很多 Key 到时间没有删除。于是,惰性删除派上用场。和 memcached 一样,LRU (least recenty use)。在你获取某个 Key 的时候,Redis 会检查一下,这个 Key 如果设置了过期时间,那么是否过期了?如果过期了此时就会删除。

如果定期删除没删除 Key。然后你也没即时去请求 Key,也就是说惰性删除也没生效。这样,Redis的内存会越来越高。那么就应该采用内存淘汰机制

在 redis.conf 中有一行配置:

# maxmemory-policy volatile-lru

该配置就是配内存淘汰策略的。它的可选参数有:

  • noeviction:当内存不足以容纳新写入数据时,新写入操作会报错。(显然不会用这个选项)

  • allkeys-lru:当内存不足以容纳新写入数据时,在键空间中,移除最近最少使用的 Key。(推荐使用)

  • allkeys-random:当内存不足以容纳新写入数据时,在键空间中,随机移除某个 Key。

  • volatile-lru:当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,移除最近最少使用的 Key。这种情况一般是把 Redis 既当缓存,又做持久化存储的时候才用。

  • volatile-random:当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,随机移除某个 Key。

  • volatile-ttl:当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,有更早过期时间的 Key 优先移除。

数据类型

它的数据类型有:

String

最常用的类型。它可以存储多种值,比如把图片、对象序列化后存进来。

一个 String 类型的值最多可以存储 512M 的内容。

我们可以对 String 类型的值进行数字的累加,累减(incr,decr,incrby);字符串拼接(append);向量的随机访问(getrange)。甚至它还允许我们基于 bit 的处理(getbit, setbit)–可以用来实现 bloom filter。

Lists

列表类型。它实际上是多个 String 组成的双向链表。它是按插入顺序排序的。我们可以往列表的头插入数据也可以往尾部插入(lpush,rpush)。当列表不存在时,我们进行插入操作时会新建一个列表。同样,如果列表为空了,它的空间就会被回收。

一个列表最多可以存储 232-1个项,相当于 4294967295,超过 40 亿,应该足够我们使用了。这还只是一个列表。在实际应用中不建议使用这么大的列表。可以拆分成若干个小的。

由于它是双向链表的结构,所以往列表里插入、删除都非常快;从链表的两端读取数据也很快。但如果想访问一个大列表中间的值,那就很慢了。

我们可以用列表来做消息队列;也可以用它来做 top N 的业务(用 ltrim 修剪列表);

还可以利用 lrange 命令,做基于 Redis 的分页功能,性能极佳,用户体验好。

Sets

Sets 是多个 String 组成的无序列表。往 Sets 里添加、删除、检查是否存在,都只需要 O(1) 的时间复杂度,异常快。但它有个特性就是不能包含重复的值。而且 Redis 服务端还提供了 Sets 的求合集、差集、交集等操作,而且速度非常快。

可以用到的场景可以是:存放当天访问的IP,用来计算UV,以及留存,新增用户等(多个 Sets 求差、并集);存放用户在社交媒体上的关注列表,可以轻松的比较某些人共同关注了哪些人等。

一个 Sets 可以存放的最大数量也是 232-1。

Sorted Sets

和 Sets 一样,它也不允许有重复的值,也可以存放 232-1 个值。但不同的是,Sorted Sets 为每个值添加了一个 score 属性,用来进行排序。它还支持通过 score 属性来进行范围查询,效率也非常高;访问集合中间的值也很快,这点和 Lists 不同。但要注意的是 Lists 的值是可重复的,Sets/Sorted Sets 里的值是不可重复的。

通过这些特性,Sorted Sets 可以被用来:

游戏的积分排行榜。这种榜单肯定不会有重复的用户名,另外榜单需要接积分排序,积分更新也很频繁,随时会有新用户添加进来。所以要求能按积分排序及范围查询、插入要快、单数据查询快。这个场景完美贴合 Sorted Sets。

同时它也可以用来对数据进行索引。比如我们的用户信息存在Redis中,我们可以把用户年龄,ID 等信息导入一个 Sorted Sets,通过范围查询我们可以轻松的根据年龄来查询相应的用户ID。

Hashs

Hashs 实际上是 key 和多个 String 值之间的 map 关系。所以它可以用来存储对象的信息。它的访问速度自然不用说,是O(1)。

一个 Hashs 类型的 key 可以有 232-1 个自定义属性值。

缓存写一致

一致性问题是分布式常见问题,还可以分为最终一致性和强一致性。数据库和缓存双写,就必然会存在不一致的问题。

如果对数据有强一致性要求,不能放缓存。我们只能保证最终一致性。使用时的要点有:

  • 采取正确更新策略,先更新数据库,再删缓存。
  • 因为可能存在删除缓存失败的问题,提供一个补偿措施即可,例如利用消息队列。

缓存穿透

缓存穿透,即黑客故意去请求缓存中不存在的数据,导致所有的请求都怼到数据库上,从而数据库连接异常。

缓存穿透解决方案:

  • 利用互斥锁,缓存失效的时候,先去获得锁,得到锁了,再去请求数据库。没得到锁,则休眠一段时间重试。
  • 采用异步更新策略,无论 Key 是否取到值,都直接返回。Value 值中维护一个缓存失效时间,缓存如果过期,异步起一个线程去读数据库,更新缓存。需要做缓存预热(项目启动前,先加载缓存)操作。
  • 提供一个能迅速判断请求是否有效的拦截机制,比如,利用布隆过滤器,内部维护一系列合法有效的 Key。迅速判断出,请求所携带的 Key 是否合法有效。如果不合法,则直接返回。

缓存雪崩,即缓存同一时间大面积的失效,这个时候又来了一波请求,结果请求都怼到数据库上,从而导致数据库连接异常。

缓存雪崩解决方案:

  • 给缓存的失效时间,加上一个随机值,避免集体失效。
  • 使用互斥锁,但是该方案吞吐量明显下降了。
  • 双缓存。我们有两个缓存,缓存 A 和缓存 B。缓存 A 的失效时间为 20 分钟,缓存 B 不设失效时间。自己做缓存预热操作。

然后细分以下几个小点:从缓存 A 读数据库,有则直接返回;A 没有数据,直接从 B 读数据,直接返回,并且异步启动一个更新线程,更新线程同时更新缓存 A 和缓存 B。

并发竞争 Key

如果同时有多个子系统去 Set 一个 Key。

如果对这个 Key 操作,不要求顺序

可以用一个分布式锁,大家去抢锁,抢到锁就做 set 操作即可,比较简单。

如果对这个 Key 操作,要求顺序

假设有一个 key1,系统 A 需要将 key1 设置为 valueA,系统 B 需要将 key1 设置为 valueB,系统 C 需要将 key1 设置为 valueC。

期望按照 key1 的 value 值按照 valueA > valueB > valueC 的顺序变化。这种时候我们在数据写入数据库的时候,需要保存一个时间戳。

假设时间戳如下:

系统A key 1 {valueA  3:00}
系统B key 1 {valueB  3:05}
系统C key 1 {valueC  3:10}

那么,假设这时系统 B 先抢到锁,将 key1 设置为{valueB 3:05}。接下来系统 A 抢到锁,发现自己的 valueA 的时间戳早于缓存中的时间戳,那就不做 set 操作了,返回客户端提示操作失败,以此类推。

还可以利用队列,将 set 方法变成串行访问也可以。

持久化

Redis 提供了多种不同级别的持久化方式:

RDB 持久化可以在指定的时间间隔内生成数据集的时间点快照。 AOF 持久化记录服务器执行的所有写操作命令,并在服务器启动时,通过重新执行这些命令来还原数据集。 AOF 文件中的命令全部以 Redis 协议的格式来保存,新命令会被追加到文件的末尾。 Redis 还可以在后台对 AOF 文件进行重写,使得 AOF 文件的体积不会超出保存数据集状态所需的实际大小。

Redis 可以同时使用 AOF 持久化和 RDB 持久化。 在这种情况下, 当 Redis 重启时, 它会优先使用 AOF 文件来还原数据集, 因为 AOF 文件保存的数据集通常比 RDB 文件所保存的数据集更完整。

RDB

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

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

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

save 60 1000

你也可以通过调用 SAVE 或者 BGSAVE , 手动让 Redis 进行数据集保存操作。

当 Redis 需要保存 dump.rdb 文件时, 服务器执行以下操作:

  1. Redis 调用 fork() ,同时拥有父进程和子进程。

  2. 子进程将数据集写入到一个临时 RDB 文件中。

  3. 当子进程完成对新 RDB 文件的写入时,Redis 用新 RDB 文件替换原来的 RDB 文件,并删除旧的 RDB 文件。

RDB 是一个非常紧凑的文件,它保存了 Redis 在某个时间点上的数据集。 这种文件非常适合用于进行备份: 比如说,你可以在最近的 24 小时内,每小时备份一次 RDB 文件,并且在每个月的每一天,也备份一个 RDB 文件。 这样的话,即使遇上问题,也可以随时将数据集还原到不同的版本。

RDB 非常适用于灾难恢复:它只有一个文件,并且内容都非常紧凑,可以(在加密后)将它传送到别的数据中心。

RDB 在恢复大数据集时的速度比 AOF 的恢复速度要快。

如果你需要避免在服务器故障时丢失数据,那么 RDB 不适合。 虽然 Redis 可以设置不同的保存点来控制保存 RDB 文件的频率, 但是, 因为RDB 文件需要保存整个数据集的状态, 所以它并不是一个快速的操作。 因此可能会至少 5 分钟才保存一次 RDB 文件。 在这种情况下, 一旦发生故障停机, 你就可能会丢失几分钟的数据。

每次保存 RDB 的时候,Redis 都要 fork() 出一个子进程,并由子进程来进行实际的持久化工作。 在数据集比较庞大时, fork() 可能会非常耗时,造成服务器在一定的时间内停止处理客户端。

AOF(append-only file)

快照功能并不是非常耐久: 如果 Redis 因为某些原因而造成故障停机, 那么服务器将丢失最近写入、且仍未保存到快照中的那些数据。

从 1.1 版本开始, Redis 增加了一种完全耐久的持久化方式: AOF 持久化。

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

appendonly yes

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

因为 AOF 的运作方式是不断地将命令追加到文件的末尾, 所以随着写入命令的不断增加, AOF 文件的体积也会变得越来越大。

耐久性

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

有三个选项:

  1. 每次有新命令追加到 AOF 文件时就执行一次 fsync :非常慢,也非常安全。

  2. 每秒 fsync 一次:足够快(和使用 RDB 持久化差不多),并且在故障时只会丢失 1 秒钟的数据。

  3. 从不 fsync :将数据交给操作系统来处理。更快,也更不安全的选择。

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

写时机制

AOF 重写和 RDB 创建快照一样,都巧妙地利用了__写时复制机制__。以下是 AOF 重写的执行步骤:

  1. Redis 执行 fork() ,现在同时拥有父进程和子进程。
  2. 子进程开始将新 AOF 文件的内容写入到临时文件。
  3. 对于所有新执行的写入命令,父进程一边将它们累积到一个内存缓存中,一边将这些改动追加到现有 AOF 文件的末尾: 这样即使在重写的中途发生停机,现有的 AOF 文件也还是安全的。
  4. 当子进程完成重写工作时,它给父进程发送一个信号,父进程在接收到信号之后,将内存缓存中的所有数据追加到新 AOF 文件的末尾。
  5. Redis 原子地用新文件替换旧文件,之后所有命令都会直接追加到新 AOF 文件的末尾。

AOF重写

如果对一个计数器调用了 100 次 INCR , 那么为了保存这个计数器的当前值, AOF 文件就需要使用 100 条记录(entry)。实际上, 只使用一条 SET 命令已经足以保存计数器的当前值了。

为了处理这种情况, Redis 支持一种特性: 在不打断服务客户端的情况下, 对 AOF 文件进行重建。

执行 BGREWRITEAOF 命令, Redis 将生成一个新的 AOF 文件, 这个文件包含重建当前数据集所需的最少命令。

AOF恢复

服务器可能在程序正在对 AOF 文件进行写入时停机, 如果停机造成了 AOF 文件出错(corrupt), 那么 Redis 在重启时会拒绝载入这个 AOF 文件, 从而确保数据的一致性不会被破坏。

当发生这种情况时, 可以用以下方法来修复出错的 AOF 文件:

  1. 为现有的 AOF 文件创建一个备份。
  2. 使用 Redis 附带的 redis-check-aof 程序,对原来的 AOF 文件进行修复。 $ redis-check-aof –fix
  3. (可选)使用 diff -u 对比修复后的 AOF 文件和原始 AOF 文件的备份,查看两个文件之间的不同之处。
  4. 重启 Redis 服务器,等待服务器载入修复后的 AOF 文件,并进行数据恢复。

在 Redis 2.2 或以上版本,可以在不重启的情况下,从 RDB 切换到 AOF :

  1. 为最新的 dump.rdb 文件创建一个备份。
  2. 将备份放到一个安全的地方。
  3. 执行以下两条命令:

     redis-cli> CONFIG SET appendonly yes
        
     redis-cli> CONFIG SET save ""
    
  4. 确保命令执行之后,数据库的键的数量没有改变。
  5. 确保写命令会被正确地追加到 AOF 文件的末尾。

步骤 3 执行的第一条命令开启了 AOF 功能: Redis 会阻塞直到初始 AOF 文件创建完成为止, 之后 Redis 会继续处理命令请求, 并开始将写入命令追加到 AOF 文件末尾。

步骤 3 执行的第二条命令用于关闭 RDB 功能。 这一步是可选的, 如果愿意, 可以同时使用 RDB 和 AOF 这两种持久化功能。

别忘了在 redis.conf 中打开 AOF 功能! 否则的话, 服务器重启之后, 之前通过 CONFIG SET 设置的配置就会被遗忘, 程序会按原来的配置来启动服务器。

两者配合

BGSAVE 执行的过程中, 不可以执行 BGREWRITEAOF 。 反过来说, 在 BGREWRITEAOF 执行的过程中, 也不可以执行 BGSAVE 。这可以防止两个 Redis 后台进程同时对磁盘进行大量的 I/O 操作。

如果 BGSAVE 正在执行, 并且用户显示地调用 BGREWRITEAOF 命令, 那么服务器将向用户回复一个 OK 状态, 并告知用户, BGREWRITEAOF 已经被预定执行: 一旦 BGSAVE 执行完毕, BGREWRITEAOF 就会正式开始。

Redis 对于数据备份是非常友好的, 因为你可以在服务器运行的时候对 RDB 文件进行复制: RDB 文件一旦被创建, 就不会进行任何修改。 当服务器要创建一个新的 RDB 文件时, 它先将文件的内容保存在一个临时文件里面, 当临时文件写入完毕时, 程序才使用 rename 原子地用临时文件替换原来的 RDB 文件。

也就是说, 无论何时, 复制 RDB 文件都是绝对安全的。

建议方案

创建一个定时任务(cron job), 每小时将一个 RDB 文件备份到一个文件夹,标注好时间, 并且每天将一个 RDB 文件备份到另一个文件夹,按时间保存。只保存最近一两周的快照。

至少每天一次, 将 RDB 备份到你的数据中心之外, 或者至少是备份到你运行 Redis 服务器的物理机器之外。


Similar Posts

上一篇 Redis事务

下一篇 Zookeeper

评论