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
2.1.2 单键查询更快的数据结构
Redis 使用 Hash 表来作为 Key-Value 数据库的存储数据结构,查询的时间复杂度O(1)
使用hash表可以实现O(1)的查询时间复杂度。
所以,Redis 会对哈希表做 rehash 操作。rehash 也就是增加现有的哈希桶数量,让逐渐增多的 entry 元素能在更多的桶之间分散保存,减少单个桶中的元素数量,从而减少单个桶中的冲突。那具体怎么做呢?
其实,为了使 rehash 操作更高效,Redis 默认使用了两个全局哈希表:哈希表 1 和哈希表 2。
一开始,当你刚插入数据时,默认使用哈希表 1,此时的哈希表 2 并没有被分配空间。
随着数据逐步增多,Redis 开始执行 rehash,这个过程分为三步:
- 给哈希表 2 分配更大的空间,例如是当前哈希表 1 大小的两倍;
- 把哈希表 1 中的数据重新映射并拷贝到哈希表 2 中;
- 释放哈希表 1 的空间。
但是每次rehash都要重新遍历一边,速度很慢。会影响到客户端的响应速度。
为了避免这个问题,Redis 采用了渐进式 rehash。简单来说就是在第二步拷贝数据时,Redis 仍然正常处理客户端请求,每处理一个请求时,从哈希表 1 中的第一个索引位置开始,顺带着将这个索引位置上的所有 entries 拷贝到哈希表 2 中;等处理下一个请求时,再顺带拷贝哈希表 1 中的下一个索引位置的 entries。如下图所示:
渐进式 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 的其他功能,比如持久化、异步删除、集群数据同步等,其实是由额外的线程执行的。
之所以使用单线程处理所有请求。在我们采用多线程后,如果没有良好的系统设计,实际得到的结果,其实是右图所展示的那样。我们刚开始增加线程数时,系统吞吐率会增加,但是,再进一步增加线程时,系统吞吐率就增长迟缓了,有时甚至还会出现下降的情况。
并发访问控制一直是多线程开发中的一个难点问题,如果没有精细的设计,比如说,只是简单地采用一个粗粒度互斥锁,就会出现不理想的结果:即使增加了线程,大部分线程也在等待获取访问共享资源的互斥锁,并行变串行,系统吞吐率并没有随着线程的增加而增加。
那为什么单线程 Redis 能获得高性能?
- Redis 的大部分操作在内存上完成,再加上它采用了高效的数据结构,例如哈希表和跳表,这是它实现高性能的一个重要原因。
- 就是 Redis 采用了多路复用机制,使其在网络 IO 操作中能并发处理大量的客户端请求,实现高吞吐率。
2.2.1 Redis 的多路复用机制
Redis 请求的完整流程
单线程一旦阻塞,就导致 Redis 无法处理其他客户端请求,效率很低。不过,socket 网络模型本身支持非阻塞模式。
Linux 中的 IO 多路复用机制是指一个线程处理多个 IO 流,就是我们经常听到的 select/epoll
机制。在 Redis 只运行单线程的情况下,该机制允许内核中,同时存在多个监听套接字和已连接套接字。
内核会一直监听这些套接字上的连接请求或数据请求。一旦有请求到达,就会交给 Redis 线程处理,这就实现了一个 Redis 线程处理多个 IO 流的效果。
WHY NOT REDIS
既然 Redis 又快又能承受高吞吐量,我们为什么不把它作为主要的存储介质?
1. 成本
以内存作为数据库介质成本太高,同样的存储容量,同样大小的内存成本是硬盘的十倍左右。
2. 数据安全
Redis 由于是内存型数据库,重启之后数据会丢失,所以要保证数据安全就要进行数据的持久化。
Redis 会将操作记录在内存缓冲区当中,然后通过 AOF/RDB 的方式同步到磁盘里。 当出现重启 redis 时,优先考虑AOF,其次 RDB 的方式恢复数据。
2.1 AOF
AOF :记录服务器的所有写操作命令。aof则会将所有命令先写入aof缓冲区,再由缓冲区根据对应的策略(比如是always还是每秒)根据这些配置来写入磁盘文件。
Redis 配置:
- Always:每修改一次数据,就记录一条日志;
- Every Sec:每秒持久化一下日志;
流程:
- 主线程负责写入AOF缓冲区;
- AOF线程负责每秒执行一次同步磁盘操作,并记录最近一次同步时间。
- 主线程负责对比上次 AOF 到同步时间:
- 如果距上次同步成功时间在2秒内,主线程继续操作。
- 如果距上次同步成功时间超过2秒,主线程将会阻塞,直到同步操作完成。
通过对AOF阻塞流程可以发现两个问题:
- everysec配置最多可能丢失2秒数据,不是1秒。
- 如果落盘缓慢,将会导致Redis主线程阻塞影响效率。
AOF重写:
伴随着aof文件越来越大,会定期对aof文件进行重写。合并对同一个key的重复写操作
set key1 A
set key1 B
set key1 C
变成
set key1 C
重写流程如下图:
和 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 进程和写磁盘的时间开销。
比如阿里,就有开发专门的持久化内存型数据库。