阅读 108

Redis续一

一、Redis 在内存使用上是如何开源节流

Redis 跟其他传统数据库不同,Redis 是一个纯内存的数据库,并且存储了都是一些数据结构的数据,如果不对内存加以控制的话,Redis 很可能会因为数据量过大导致系统的奔溃。

1、Ziplist

当最开始尝试开启一个小数据量的 Hash 结构和一个 Zset 结构时,发现他们在 Redis 里面的真正结构类型是一个 Ziplist。

Ziplist 是一个紧凑的数据结构,每一个元素之间都是连续的内存,如果在 Redis 中,Redis 启用的数据结构数据量很小时,Redis 就会切换到使用紧凑存储的形式来进行压缩存储。

例如,上面的例子,我们采用了 Hash 结构进行存储,Hash 结构是一个二维的结构,是一个典型的用空间换取时间的结构。

但是如果使用的数据量很小,使用二维结构反而浪费了空间,在时间的性能上也并没有得到太大的提升,还不如直接使用一维结构进行存储。

在查找的时候,虽然复杂度是 O(n),但是因为数据量少遍历也非常快,增至比 Hash 结构本身的查询更快。

如果当集合对象的元素不断的增加,或者某个 Value 的值过大,这种小对象存储也会升级生成标准的结构。

Redis 也可以在配置中进行定义紧凑结构和标准结构的转换参数:

2、Quicklist

Quicklist 数据结构是 Redis 在 3.2 才引入的一个双向链表的数据结构,确实来说是一个 Ziplist 的双向链表。

Quicklist 的每一个数据节点是一个 Ziplist,Ziplist 本身就是一个紧凑列表。

假使,Quicklist 包含了 5 个 Ziplist 的节点,每个 Ziplist 列表又包含了 5 个数据,那么在外部看来,这个 Quicklist 就包含了 25 个数据项。

Quicklist 的结构设计简单总结起来,是一个空间和时间的折中方案:

  • 双向链表可以在两端进行 Push 和 Pop 操作,但是它在每一个节点除了保存自身的数据外,还要保存两个指针,增加额外的内存开销。

其次是由于每个节点都是独立的,在内存地址上并不连续,节点多了容易产生内存碎片。

  • Ziplist 本身是一块连续的内存,存储和查询效率很高,但是,它不利于修改操作,每次数据变动时都会引发内存 Realloc,如果 Ziplist 长度很长时,一次 Realloc 会导致大批量数据拷贝。

所以,结合 Ziplist 和双向链表的优点,Quciklist 就孕育而生。

3、对象共享

Redis 在自己的对象系统中构建了一个引用计数方法,通过这个方法程序可以跟踪对象的引用计数信息,除了可以在适当的时候进行对象释放,还可以用来作为对象共享。

举个例子,假使键 A 创建了一个整数值 100 的字符串作为值对象,这个时候键 B 也创建保存同样整数值 100 的字符串对象作为值对象。

那么在 Redis 的操作时:

  • 将数据库键的指针指向一个现有的值对象。
  • 将被共享的值对象引用计数加一。

假使,我们的数据库中指向整数值 100 的键不止键 A 和键 B,而是有几百个,那么 Redis 服务器中只需要一个字符串对象的内存就可以保存原本需要几百个字符串对象的内存才能保存的数据。

二、Redis 是如何实现主从复制

几个定义:

  • runID:服务器运行的 ID。
  • Offset:主服务器的复制偏移量和从服务器复制的偏移量。
  • Replication backlog:主服务器的复制积压缓冲区。

在 Redis 2.8 之后,使用 Psync 命令代替 Sync 命令来执行复制的同步操作。

Psync 命令具有完整重同步和部分重同步两种模式:

  • 完整同步用于处理初次复制情况:完整重同步的执行步骤和 Sync 命令执行步骤一致,都是通过让主服务器创建并发送 RDB 文件,以及向从服务器发送保存在缓冲区的写命令来进行同步。
  • 部分重同步是用于处理断线后重复制情况:当从服务器在断线后重新连接主服务器时,主服务可以将主从服务器连接断开期间执行的写命令发送给从服务器,从服务器只要接收并执行这些写命令,就可以将数据库更新至主服务器当前所处的状态。

完整重同步:

  • Slave 发送 Psync 给 Master,由于是第一次发送,不带上 runID 和 Offset。
  • Master 接收到请求,发送 Master 的 runID 和 Offset 给从节点。
  • Master 生成保存 RDB 文件。
  • Master 发送 RDB 文件给 Slave。
  • 在发送 RDB 这个操作的同时,写操作会复制到缓冲区 Replication Backlog Buffer 中,并从 Buffer 区发送到 Slave。
  • Slave 将 RDB 文件的数据装载,并更新自身数据。

如果网络的抖动或者是短时间的断链也需要进行完整同步就会导致大量的开销,这些开销包括了,Bgsave 的时间,RDB 文件传输的时间,Slave 重新加载 RDB 时间,如果 Slave 有 AOF,还会导致 AOF 重写。

这些都是大量的开销,所以在 Redis 2.8 之后也实现了部分重同步的机制。

部分重同步:

  • 网络发生错误,Master 和 Slave 失去连接。
  • Master 依然向 Buffer 缓冲区写入数据。
  • Slave 重新连接上 Master。
  • Slave 向 Master 发送自己目前的 runID 和 Offset。
  • Master 会判断 Slave 发送给自己的 Offset 是否存在 Buffer 队列中。
  • 如果存在,则发送 Continue 给 Slave;如果不存在,意味着可能错误了太多的数据,缓冲区已经被清空,这个时候就需要重新进行全量的复制。
  • Master 发送从 Offset 偏移后的缓冲区数据给 Slave。
  • Slave 获取数据更新自身数据。

三、Redis 是怎么制定过期删除策略的

当一个键处于过期的状态,其实在 Redis 中这个内存并不是实时就被从内存中进行摘除,而是 Redis 通过一定的机制去把一些处于过期键进行移除,进而达到内存的释放,那么当一个键处于过期,Redis 会在什么时候去删除?

几时被删除存在三种可能性,这三种可能性也代表了 Redis 的三种不同的删除策略。

  • 定时删除:在设置键过去的时间同时,创建一个定时器,让定时器在键过期时间来临,立即执行对键的删除操作。
  • 惰性删除:放任键过期不管,但是每次从键空间获取键时,都会检查该键是否过期,如果过期的话,就删除该键。
  • 定期删除:每隔一段时间,程序都要对数据库进行一次检查,删除里面的过期键,至于要删除多少过期键,由算法而定。

1、定时删除

设置键的过期时间,创建定时器,一旦过期时间来临,就立即对键进行操作。

这种对内存是友好的,但是对 CPU 的时间是最不友好的,特别是在业务繁忙,过期键很多的时候,删除过期键这个操作就会占据很大一部分 CPU 的时间。

要知道 Redis 是单线程操作,在内存不紧张而 CPU 紧张的时候,将 CPU 的时间浪费在与业务无关的删除过期键上面,会对 Redis 的服务器的响应时间和吞吐量造成影响。

另外,创建一个定时器需要用到 Redis 服务器中的时间事件,而当前时间事件的实现方式是无序链表,时间复杂度为 O(n),让服务器大量创建定时器去实现定时删除策略,会产生较大的性能影响,所以,定时删除并不是一种好的删除策略。

2、惰性删除

与定时删除相反,惰性删除策略对 CPU 来说是最友好的,程序只有在取出键的时候才会进行检查,是一种被动的过程。

与此同时,惰性删除对内存来说又是最不友好的,一个键过期,只要不再被取出,这个过期键就不会被删除,它占用的内存也不会被释放。

很明显,惰性删除也不是一个很好的策略,Redis 是非常依赖内存和较好内存的,如果一些长期键长期没有被访问,就会造成大量的内存垃圾,甚至会操成内存的泄漏。

在对执行数据写入时,通过 expireIfNeeded 函数对写入的 Key 进行过期判断。

其中 expireIfNeeded 在内部做了三件事情,分别是:

  • 查看 Key 是否过期。
  • 向 Slave 节点传播执行过去 Key 的动作。
  • 删除过期 Key。

3、定期删除

上面两种删除策略,无论是定时删除和惰性删除,这两种删除方式在单一的使用上都存在明显的缺陷,要么占用太多 CPU 时间,要么浪费太多内存。

定期删除策略是前两种策略的一个整合和折中:

  • 定期删除策略每隔一段时间执行一次删除过期键操作,并通过限制删除操作执行的时间和频率来减少删除操作对 CPU 时间的影响。
  • 通过合理的删除执行的时长和频率,来达到合理的删除过期键。

note: 一个人真正的安全感,来自于内心的自信。

关注下面的标签,发现更多相似文章
评论