0%

《Redis》Redis 理论知识

Redis 为什么使用单线程处理请求

  1. Redis 使用单线程的主要原因是其瓶颈不在 CPU,而在内存和带宽上
  2. 完全基于内存,绝大部分请求是纯粹的内存操作,非常快速。数据存在内存中,类似于 HashMap,HashMap 的优势就是查找和操作的时间复杂度都是 O(1);
  3. 数据结构简单,对数据操作也简单,Redis 中的数据结构是专门进行设计的;
  4. 采用单线程,避免了不必要的上下文切换和竞争条件,也不存在多进程或者多线程导致的切换而消耗 CPU,不用去考虑各种锁的问题,不存在加锁释放锁操作,没有因为可能出现死锁而导致的性能消耗;
  5. 使用多路 I/O 复用模型( select、epoll 等等),非阻塞 IO;

过期数据清理

清理有两种方式:

  1. 只有这个数据被访问,才会判断是否过期,过期了就清理
  2. 主动检查这个数据是否过期,过期了就清理

Redis 结合了两种方式,如果只使用第一种,则有可能导致一些数据不再被访问,即使过期了也不会被清理,导致内存剧增

如果只使用第二种,每个 key 都去检查的话,Redis 又是单线程,那其他任务都会被卡住。

所以 Redis 的策略是:

Redis 会将每个设置了过期时间的 key 放入到一个独立的字典中,每隔一小段时间(默认 100ms)进行一次过期扫描

  1. 随机抽取 20 个 key
  2. 删除这 20 个 key 中过期的 key
  3. 如果过期的 key 比例超过 1/4,就重复步骤 1,继续删除。

为了防止每次扫描过期的 key 比例都超过 1/4,导致不停循环卡死线程,Redis 为每次扫描添加了上限时间,默认是 25ms。超过 25ms 就会停止过期扫描任务去做其他事情

内存淘汰策略

当 Redis 内存不足时,Redis 需要淘汰一些数据,来腾出更多内存使用

Redis 提供了几种内存淘汰机制让用户选择,配置 maxmemory-policy:

  1. noeviction:当内存超出 maxmemory,写入请求会报错,但是删除和读请求可以继续。(使用这个策略,疯了吧)
  2. allkeys-lru:当内存超出 maxmemory,在所有的 key 中,移除最少使用的key。只把 Redis 当缓存时使用这种策略。(推荐)
  3. allkeys-random:当内存超出 maxmemory,在所有的 key 中,随机移除某个 key。(应该没人用吧)
  4. volatile-lru:当内存超出 maxmemory,在设置了过期时间 key 的字典中,移除最少使用的 key。把 Redis 既当缓存,又做持久化的时候使用这种策略。
  5. volatile-random:当内存超出 maxmemory,在设置了过期时间 key 的字典中,随机移除某个key。
  6. volatile-ttl:当内存超出 maxmemory,在设置了过期时间 key 的字典中,优先移除 ttl 小的。

解决 Redis 热点 key 访问压力

  1. 可以为 Redis 添加 slave,通过 slave 承担读压力来缓解
  2. 多级缓存方案(比如在进程内添加一道内存缓存,还可以在 Redis 数据库代理层面加一道缓存)

Redis 分区集群高可用

使用树结构组成 Redis 分区集群。

叶子结点是真正的 Redis 实例,每个实例下面还有一个只负责读的从 Redis 实例(使用 replicaof 从主实例复制数据)

所有的非叶子结点都是虚拟的 Redis 节点

应用启动的时候,需要先与所有的 Redis 实例(包括从实例)建立连接,在使用 Redis 的写指令的时候,通过对 key 进行 hash 类算法,得到所有写实例中的一个叶子结点,然后向这个实例进行写操作。读的原理也是一样的

哨兵实现高可用

一个典型的 sentinel 部署方案

3 个哨兵同时与 Master 和 Slave 建立连接,并且哨兵之间也互相建立了连接

哨兵通过与 Master 和 Slave 的通信,能够清楚每个 Redis 服务的健康状态。

这样,当 Master 发生故障时,哨兵能够知晓 Master 的此种情况,然后通过对 Slave 健康状态、优先级、同步数据状态等的综合判断,选取其中一个 Slave 切换为 Master,并且修改其他 Slave 指向新的 Master 地址。

哨兵不能成为单点,所以也部署成集群,当发生 master 故障时,哨兵之间要选出一个 leader 来做 master 切换操作,所以需要基数个哨兵

如果某单个哨兵认为其监听的 Master 处于下线的状态,这个状态在 Redis 中标记为 S_DOWN,即主观下线。假设 quorum 配置为 2,则当有两个哨兵同时认为一个 Master 处于下线的状态时,会标记该 Master 为 O_DOWN,即客观下线。只有一个 Master 处于客观下线状态时才会开始执行切换

哨兵配置文件中只配了 master 的 ip port,slaves 信息是哨兵通过 master 节点获取到的

性能实践

  1. 如果 Redis 中的数据并不是特别敏感或者可以通过其它方式重写生成数据,可以关闭持久化,如果丢失数据可以通过其它途径补回(持久化会造成主进程阻塞)
  2. 自己制定策略定期检查 Redis 的情况,然后可以手动触发备份、重写数据
  3. 直接关闭持久化,使用 slave 实例做备份
  4. 使用 slave 做读写分离

主从复制原理

Redis 持久化

数据库在进行写操作时到底做了哪些事

1、客户端向服务端发送写操作(数据在客户端的内存中)
2、数据库服务端接收到写请求的数据(数据在服务端的内存中)
3、服务端调用 write(2) 这个系统调用,将数据往磁盘上写(数据在系统内存的缓冲区中, 我们可以通过 POSIX API 提供的 fsync 系列命令强制操作系统将数据从内核区写到磁盘控制器上)
4、操作系统将缓冲区中的数据转移到磁盘控制器上(数据在磁盘缓存中,大多数情况下磁盘缓存是被设置关闭的。或者是只开启为读缓存,这一步就不会发生)
5、磁盘控制器将数据写到磁盘的物理介质中(数据真正落到磁盘上)

Redis 持久化策略

持久化是指将内存中的数据 dump 到磁盘中

RDB 快照(类似于 mongodb 的 Replica Sets)

在生成快照时,将当前进程 fork 出一个子进程,然后在子进程中循环所有的数据,将数据写入到一个 RDB 文件,可以通过 Redis 的 save 指令来配置 RDB 快照生成的时机,比如你可以配置当 10 分钟以内有 100 次写入就生成快照,也可以配置当 1 小时内有 1000 次写入就生成快照,也可以多个规则一起实施。

子进程备份数据时,父进程依然能够继续处理命令,他们是共享内存的,当父进程要写内存时,就会发生内存数据从父进程到子进程的复制(就是 Copy On Write)

RDB 有他的不足,就是一旦数据库出现问题,那么我们的 RDB 文件中保存的数据并不是全新的,从上次 RDB 文件生成到 Redis 停机这段时间的数据全部丢掉了

AOF 日志(是一个追加写入的日志文件)

类似于 mongodb 的 journaling 日志和 Mysql 的 binlog

每一条写命令都生成一条日志,那么 AOF 文件会很大,所以 Redis 提供 AOF rewrite 功能:其生成过程和 RDB 类似,也是 fork 一个进程,直接遍历数据,写入新的 AOF 临时文件。在写入新文件的过程中,所有的写操作日志还是会写到原来老的 AOF 文件中,同时还会记录在内存缓冲区中。

当重完操作完成后,会将所有缓冲区中的日志一次性写入到临时文件中。

然后调用原子性的 rename 命令用新的 AOF 文件取代老的 AOF 文件。

AOF 是一个写文件操作,其目的是将操作日志写到磁盘上,所以它也同样会遇到我们上面说的写操作的 5 个流程。

那么写 AOF 的操作安全性又有多高呢。实际上这是可以设置的,在 Redis 中对 AOF 调用 write(2) 写入后,何时再调用 fsync 将其写到磁盘上,通过 appendfsync 选项来控制,下面 appendfsync 的三个设置项,安全强度逐渐变强

  • 当设置 appendfsync 为 no 的时候,Redis 不会主动调用 fsync 去将 AOF 日志内容同步到磁盘,所以这一切就完全依赖于操作系统的调试了。对大多数 Linux 操作系统,是每 30 秒进行一次 fsync,将缓冲区中的数据写到磁盘上。
  • 当设置 appendfsync 为 everysec 的时候,Redis 会默认每隔一秒进行一次 fsync 调用,将缓冲区中的数据写到磁盘。但是当这一次的 fsync 调用时长超过 1 秒时。Redis 会采取延迟 fsync 的策略,再等一秒钟。也就是在两秒后再进行 fsync,这一次的 fsync 就不管会执行多长时间都会进行
  • 当设置 appendfsync 为 always 时,每一次写操作都会调用一次 fsync,这时数据是最安全的,当然,由于每次都会执行 fsync,所以其性能也会受到影响

知识点

Redis 在利用 RDB 和 AOF 进行恢复的时候,都会读取 RDB 或 AOF 文件,重新加载到内存中。

相对于 MySQL 等数据库的启动时间来说,会长很多,因为 MySQL 本来是不需要将数据加载到内存中的

MySQL 启动后提供服务时,其被访问的热数据也会慢慢加载到内存中,通常我们称之为预热,而在预热完成前,其性能都不会太高。而 Redis 的好处是一次性将数据加载到内存中,一次性预热。这样只要 Redis 启动完成,那么其提供服务的速度都是非常快的

在利用 RDB 和利用 AOF 启动上,其启动时间有一些差别。RDB 的启动时间会更短,原因有两个

  1. RDB 文件中每一条数据只有一条记录,不会像 AOF 日志那样可能有一条数据的多次操作记录。所以每条数据只需要写一次就行了。
  2. 另一个原因是 RDB 文件的存储格式和 Redis 数据在内存中的编码格式是一致的,不需要再进行数据编码工作。在 CPU 消耗上要远小于 AOF 日志的加载

Pipeline 与 Multi

Pipeline

客户端将执行的命令写入到缓冲中,最后由 exec 命令一次性发送给 redis 执行返回

Multi

跟事务类似。客户端将执行的命令依次发送到服务端,但是服务端不会执行命令,而是缓存下来,直到接收到 exec 命令后原子性的执行所有命令,然后结果返回给客户端

区别

  1. Pipeline 选择客户端缓冲,Multi 选择服务端缓冲;
  2. 请求次数的不一致,Multi 需要每个命令都发送一次给服务端,Pipeline 最后一次性发送给服务端,请求次数相对于 Multi 减少
  3. Multi/Exec 可以保证原子性,而 Pipeline/Exec 不保证原子性



微信关注我,及时接收最新技术文章