浅谈 Redis(一)—— 单机版

175 阅读11分钟

WHY REDIS

为什么用 redis

1. 数据密集型系统对于缓存的期望

  • 查询更快
  • 更高的吞吐量
  • 线程安全

2. Redis如何实现缓存三个主要特点

2.1 如何查询更快

2.1.1 内存数据库

Redis是内存型数据库。数据的读取都在内存中进行。

内存的寻址时间大约为100ns。SSD硬盘的寻址时间大约为100μs。SATA硬盘的寻址时间约为10ms。

ms(毫秒,千分之一秒)、μs(微秒,百万分之一秒)和ns(纳秒,十亿分之一秒)

内存带宽 200GB/s,硬盘带宽 7GB/s

blog.csdn.net/truelove123…

2.1.2 单键查询更快的数据结构

Redis 使用 Hash 表来作为 Key-Value 数据库的存储数据结构,查询的时间复杂度O(1)

img

使用hash表可以实现O(1)的查询时间复杂度。

所以,Redis 会对哈希表做 rehash 操作。rehash 也就是增加现有的哈希桶数量,让逐渐增多的 entry 元素能在更多的桶之间分散保存,减少单个桶中的元素数量,从而减少单个桶中的冲突。那具体怎么做呢?

其实,为了使 rehash 操作更高效,Redis 默认使用了两个全局哈希表:哈希表 1 和哈希表 2。

一开始,当你刚插入数据时,默认使用哈希表 1,此时的哈希表 2 并没有被分配空间。

随着数据逐步增多,Redis 开始执行 rehash,这个过程分为三步:

  1. 给哈希表 2 分配更大的空间,例如是当前哈希表 1 大小的两倍;
  1. 把哈希表 1 中的数据重新映射并拷贝到哈希表 2 中;
  1. 释放哈希表 1 的空间。

但是每次rehash都要重新遍历一边,速度很慢。会影响到客户端的响应速度。

为了避免这个问题,Redis 采用了渐进式 rehash。简单来说就是在第二步拷贝数据时,Redis 仍然正常处理客户端请求,每处理一个请求时,从哈希表 1 中的第一个索引位置开始,顺带着将这个索引位置上的所有 entries 拷贝到哈希表 2 中;等处理下一个请求时,再顺带拷贝哈希表 1 中的下一个索引位置的 entries。如下图所示:

img

渐进式 rehash 执行期间的哈希表操作
因为在进行渐进式 rehash 的过程中, 字典会同时使用 ht[0] 和 ht[1] 两个哈希表, 所以在渐进式 rehash 进行期间, 字典的删除(delete)、查找(find)、更新(update)等操作会在两个哈希表上进行: 比如说, 要在字典里面查找一个键的话, 程序会先在 ht[0] 里面进行查找, 如果没找到的话, 就会继续到 ht[1] 里面进行查找, 诸如此类。

另外, 在渐进式 rehash 执行期间, 新添加到字典的键值对一律会被保存到 ht[1] 里面, 而 ht[0] 则不再进行任何添加操作: 这一措施保证了 ht[0] 包含的键值对数量会只减不增, 并随着 rehash 操作的执行而最终变成空表。

2.1.3 value 的数据结构

redis 的查询可以通过 key 用O(1)的时间复杂度找到 value。redis value 也有很多种数据类型——字符串和集合。尤其是集合类型,它们各种的查询时间/空间复杂度也各不相同。 详见:
Redis Value 的数据结构详解

2.2 如何实现更高的吞吐量及线程安全

我们通常说,Redis 是单线程,主要是指 Redis 的网络 IO 和键值对读写是由一个线程来完成的,这也是 Redis 对外提供键值存储服务的主要流程。但 Redis 的其他功能,比如持久化、异步删除、集群数据同步等,其实是由额外的线程执行的。

之所以使用单线程处理所有请求。在我们采用多线程后,如果没有良好的系统设计,实际得到的结果,其实是右图所展示的那样。我们刚开始增加线程数时,系统吞吐率会增加,但是,再进一步增加线程时,系统吞吐率就增长迟缓了,有时甚至还会出现下降的情况。

image.png

并发访问控制一直是多线程开发中的一个难点问题,如果没有精细的设计,比如说,只是简单地采用一个粗粒度互斥锁,就会出现不理想的结果:即使增加了线程,大部分线程也在等待获取访问共享资源的互斥锁,并行变串行,系统吞吐率并没有随着线程的增加而增加。

那为什么单线程 Redis 能获得高性能?

  • Redis 的大部分操作在内存上完成,再加上它采用了高效的数据结构,例如哈希表和跳表,这是它实现高性能的一个重要原因。
  • 就是 Redis 采用了多路复用机制,使其在网络 IO 操作中能并发处理大量的客户端请求,实现高吞吐率。

2.2.1 Redis 的多路复用机制

Redis 请求的完整流程 req.png

单线程一旦阻塞,就导致 Redis 无法处理其他客户端请求,效率很低。不过,socket 网络模型本身支持非阻塞模式。

Linux 中的 IO 多路复用机制是指一个线程处理多个 IO 流,就是我们经常听到的 select/epoll 机制。在 Redis 只运行单线程的情况下,该机制允许内核中,同时存在多个监听套接字和已连接套接字。 内核会一直监听这些套接字上的连接请求或数据请求。一旦有请求到达,就会交给 Redis 线程处理,这就实现了一个 Redis 线程处理多个 IO 流的效果。

img.png

WHY NOT REDIS

既然 Redis 又快又能承受高吞吐量,我们为什么不把它作为主要的存储介质?

1. 成本

以内存作为数据库介质成本太高,同样的存储容量,同样大小的内存成本是硬盘的十倍左右。

2. 数据安全

Redis 由于是内存型数据库,重启之后数据会丢失,所以要保证数据安全就要进行数据的持久化。

Redis 会将操作记录在内存缓冲区当中,然后通过 AOF/RDB 的方式同步到磁盘里。 当出现重启 redis 时,优先考虑AOF,其次 RDB 的方式恢复数据。

redis同步.png

2.1 AOF

AOF :记录服务器的所有写操作命令。aof则会将所有命令先写入aof缓冲区,再由缓冲区根据对应的策略(比如是always还是每秒)根据这些配置来写入磁盘文件。

Redis 配置:

  • Always:每修改一次数据,就记录一条日志;
  • Every Sec:每秒持久化一下日志;

image.png

流程

  1. 主线程负责写入AOF缓冲区;
  2. AOF线程负责每秒执行一次同步磁盘操作,并记录最近一次同步时间。
  3. 主线程负责对比上次 AOF 到同步时间:
  • 如果距上次同步成功时间在2秒内,主线程继续操作。
  • 如果距上次同步成功时间超过2秒,主线程将会阻塞,直到同步操作完成。

通过对AOF阻塞流程可以发现两个问题:

  • everysec配置最多可能丢失2秒数据,不是1秒。
  • 如果落盘缓慢,将会导致Redis主线程阻塞影响效率。

AOF重写

伴随着aof文件越来越大,会定期对aof文件进行重写。合并对同一个key的重复写操作

set key1 A
set key1 B
set key1 C

变成

set key1 C

重写流程如下图:

image.png

和 AOF 日志由主线程写回不同,重写过程是由后台子进程 bgrewriteaof 来完成的,这也是为了避免阻塞主线程,导致数据库性能下降。

需要注意的是,在重写过程中 Redis 依旧会处理命令,而这时的命令会暂时保存到的 AOF 缓冲区AOF 重写缓冲区两个内存空间,等 bgrewriteaof 重写磁盘中的 AOF 文件完成后,再把AOF 重写缓冲区的内容写入新文件。因为 Redis 原本的将 AOF 日志写入磁盘操作的 AOF线程 也在工作,所以即使重写失败,也不会影响到正常的备份操作。

2.2 RDB

AOF 记录的是具体的操作。用 AOF 方法进行故障恢复的时候,需要逐一把操作日志都执行一遍。如果操作日志非常多,Redis 就会恢复得很缓慢,影响到正常使用。

RDB 即内存快照。所谓内存快照,就是指内存中的数据在某一个时刻的状态记录。这就类似于照片,当你给朋友拍照时,一张照片就能把朋友一瞬间的形象完全记下来。

  • save:可以保证截止到同步时间之前的内存数据被完全同步,但是会使 redis 主线程阻塞。
  • bgsave:创建一个子进程,专门用于写入 RDB 文件,避免了主线程的阻塞,这也是 Redis RDB 文件生成的默认配置。

bgsave

Redis 会借助操作系统提供的写时复制技术(Copy-On-Write, COW),在执行快照的同时,正常处理写操作。 简单来说,bgsave 子进程是由主线程 fork 生成的,可以共享主线程的所有内存数据。bgsave 子进程运行后,开始读取主线程的内存数据,并把它们写入 RDB 文件。

此时,如果主线程对这些数据也都是读操作,那么,主线程和 bgsave 子进程相互不影响。但是,如果主线程要修改一块数据,那么,这块数据就会被复制一份,生成该数据的副本。然后,主线程在这个数据副本上进行修改。同时,bgsave 子进程可以继续把原来的数据(键值对 C)写入 RDB 文件。

这既保证了快照的完整性,也允许主线程同时对数据进行修改,避免了对正常业务的影响。

虽然 bgsave 执行时不阻塞主线程,但是,如果频繁地执行全量快照,也会带来两方面的开销:

  • 频繁将全量数据写入磁盘,会给磁盘带来很大压力,多个快照竞争有限的磁盘带宽,前一个快照还没有做完,后一个又开始做了,容易造成恶性循环。
  • bgsave 子进程需要通过 fork 操作从主线程创建出来。虽然,子进程在创建后不会再阻塞主线程,但是,fork 这个创建过程本身会阻塞主线程,而且主线程的内存越大,阻塞时间越长。如果频繁 fork 出 bgsave 子进程,这就会频繁阻塞主线程了(所以,在 Redis 中如果有一个 bgsave 在运行,就不会再启动第二个 bgsave 子进程)。

2.3 混合使用

使用 RDB 的方式在第一次做完全量快照后,T1 和 T2 时刻如果再做快照,我们只需要将被修改的数据写入快照文件就行。但是,这么做的前提是,我们需要记住哪些数据被修改了。

AOF 正好能满足这个前提。所以Redis 4.0 中提出了一个混合使用 AOF 日志和内存快照的方法。简单来说,内存快照以一定的频率执行,在两次快照之间,使用 AOF 日志记录这期间的所有命令操作。

这样一来,快照不用很频繁地执行,这就避免了频繁 fork 对主线程的影响。而且,AOF 日志也只用记录两次快照间的操作,也就是说,不需要记录所有操作了,因此,就不会出现文件过大的情况了,也可以避免重写开销。

3. 充钱就会变强

近几年开始流行一种 持久型内存 的硬件——断电之后可以保存数据的内存,如:英特尔(inte)Optane DC傲腾。这个就比内存的造价成本更高了。。。但是由于重启之后数据也能保存,在这种存储介质上跑 redis,日志也不用开就可以保证数据的安全性。

如果使用这种内存作为存储介质来运行 Redis:

  • 可以保证数据的安全。重启也不需要担心数据丢失的问题。
  • 可以关掉完全数据备份功能。不需要考虑 fork 进程和写磁盘的时间开销。

比如阿里,就有开发专门的持久化内存型数据库。