编辑推荐: |
本文主要介绍了Redis高性能原理:Redis为什么这么快? 希望对您的学习有所帮助
。
本文来自于CSDN,由火龙果软件Linda编辑、推荐。 |
|
前言:Redis 为了高性能,从各方各面都进行了优化。学习一门技术,通常只接触了零散的技术点,没有在脑海里建立一个完整的知识框架体系。这样会很吃力,而且会出现一看好像自己会,过后就忘记的懵逼情况。知识系统观其实是至关重要的,从某种程度上说,在解决问题时,拥有了系统观,就意味着你能有依据有章法地定位和解决问题。
一、Redis知识系统观
Redis从应用维度有:缓存使用、集群运用、数据结构的巧妙使用;
Redis从系统维度有:可以归类为三高
高性能:线程模型、网络 IO 模型、数据结构、持久化机制;
高可用:主从复制、哨兵集群;
高拓展:Cluster 分片集群
Redis 为了高性能,从各方各面都进行了优化。
根据官方数据,Redis 的 QPS 可以达到约 100000(每秒请求数),有兴趣的可以参考官方的基准程序测试《How
fast is Redis?》,官方地址:https://redis.io/topics/benchmarks基准测试
横轴是连接数,纵轴是 QPS。此时,这张图反映了一个数量级,希望大家在面试的时候可以正确的描述出来,不要问你的时候,你回答的数量级相差甚远!
二、Redis为什么这么快?
Redis是基于内存操作,需要的时候需要我们手动持久化到硬盘中
Redis高效数据结构,对数据的操作也比较简单
Redis是单线程模型,从而避开了多线程中上下文频繁切换的操作
使用多路I/O复用模型,非阻塞I/O
使用底层模型不同,它们之间底层实现方式以及与客户端之间通信的应用协议不一样,Redis直接自己构建了VM
机制 ,因为一般的系统调用系统函数的话,会浪费一定的时间去移动和请求
1.1、基于内存的操作实现
Redis 是基于内存的数据库,不论读写操作都是在内存上完成的,完全吊打磁盘数据库的速度。Redis之所以可以使用单线程来处理,其中的一个原因是,内存操作对资源损耗较小,保证了处理的高效性。
如此宝贵的内存资源,Redis是怎么维护和管理的呢?
(1) 除了增删改查还有哪些维护性操作
命中率统计:在读取一个键之后,服务器会根据键是否存在来更新服务器的键空间命中次数或键空间不命中次数。
LRU时间更新:在读取一个键之后,服务器会更新键的LRU时间,这个值可以用于计算键的闲置时间。
惰性删除:如果服务器在读取一个键时发现该键已经过期,那么服务器会先删除这个过期键,然后才执行余下的其他操作。
键的dirty标识:如果有客户端使用WATCH命令监视了该键,服务器会将这个键标记为dirty,让事务程序注意到这个键已经被修改过。每次修改都会对dirty加一,用于触发持久化和复制。
数据库通知:如果服务器开启了数据库通知功能,那么在对键进行修改之后,服务器将按配置发送相应的数据库通知。
(2) Redis数据过期策略
Reids中数据过期策略采用定期删除+惰性删除策略。内存和CPU资源都是宝贵的,Redis的过期键删除通过定期删除设定合理的执行时长和执行频率,配合惰性删除兜底的方式,来达到CPU时间占用和内存浪费之间的平衡。
定期删除策略:Redis启用一个定时器定时监视所有key,判断key是否过期,过期的话就删除。这种策略可以保证过期的key最终都会被删除,但是也存在严重的缺点:每次都遍历内存中所有的数据,非常消耗CPU资源,并且当key已过期,但是定时器还处于未唤起状态,这段时间内key任然可以使用。du
惰性删除策略:在获取 key 时,先判断 key 是否过期,如果过期则删除。这种方式存在一个缺点:如果这个
key 一直未被使用,那么它一直在内存中,其实它已经过期了,会浪费大量的空间。
定期删除+惰性删除策略是如何工作的?
这两种策略天然的互补,结合起来之后,定时删除策略就发生了一些改变,不在是每次扫描全部的 key 了,而是随机抽取一部分
key 进行检查,这样就降低了对 CPU 资源的损耗,惰性删除策略互补了为检查到的key,基本上满足了所有要求。
但是有时候就是那么的巧,既没有被定时器抽取到,又没有被使用触发惰性删除,是否会把内存撑爆?回答是不会,当内存不够用时,内存淘汰机制就会上场。
Redis 内存淘汰机制有以下几种策略:
noeviction:当内存不足以容纳新写入数据时,新写入操作会报错。(Redis 默认策略)
allkeys-lru:当内存不足以容纳新写入数据时,在键空间中,移除最近最少使用的 Key。(推荐使用)
allkeys-random:当内存不足以容纳新写入数据时,在键空间中,随机移除某个 Key。
volatile-lru:当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,移除最近最少使用的
Key。这种情况一般是把 Redis 既当缓存,又做持久化存储的时候才用。
volatile-random:当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,随机移除某个
Key。
volatile-ttl:当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,有更早过期时间的
Key 优先移除。
修改内存淘汰机制只需要在 redis.conf 配置文件中配置 maxmemory-policy 参数即可。
值得一提的是,这里的lru和平常我们所熟知的lru还不完全一样,Redis使用的是采样概率的思想,省略了双向链表的内存消耗。Redis
会在每一次处理命令的时候判断是否达到了最大限制,如果达到则使用对应的算法去删除涉及到的Key,这时,我们前面所维护过键的LRU值就会派上用场了。
1.2、高效的数据结构
在 Redis 中存储value,常用的 5 种数据类型和应用场景如下:
String: 缓存、计数器、分布式锁等。
List: 链表、队列、微博关注人时间轴列表等。
Hash: 用户信息、Hash 表等。
Set: 去重、赞、踩、共同好友等。
Zset: 访问量排行榜、点击量排行榜等。
上面的应该叫做 Redis 支持的数据类型,也就是数据的保存形式。针对这 5 种数据类型,Redis底层都运用了哪些高效的数据结构来支持。
数据结构SDS的妙用举例
我们知道Redis的底层是用c语言来编写的,但是,数据结构确没有直接套用C的结构,而是根据Redis的定位自建了一套数据结构,比如说sds(简单动态字符串)。
Redis的String底层数据结构实现并没有直接使用C语言中的字符串,Redis中为了实现方便的扩展,考虑到安全和性能,自己定义了一个结构用来存储字符串,这个数据结构就是:简单动态字符串(Simple
Dynamic String 简称sds),并将 SDS 用作 Redis 的默认字符串。
(1)C语言中的字符串结构
C 语言中传统字符串是使用长度为 N+1 的字符数组来表示长度为 N 的字符串,并且字符串数组的最后一个元素总是空字符
'\0'。如下所示:
(2)SDS定义下的字符串结构
可以看到,相比于C语言来说,也就多了几个字段,分别用来标识空闲空间和当前数据长度,但简直是神来之笔:(这个\0对于用户来说是通用的,系统自动帮我们加上去)
可以O(1)复杂度获取字符串长度:有len字段的存在,无需像C结构一样遍历计数。
杜绝缓存区溢出:C字符串不记录已占用的长度,所以需要提前分配足够空间,一旦空间不够则会溢出。而有free字段的存在,让SDS在执行前可以判断并分配足够空间给程序
减少字符串修改带来的内存重分配次数:有free字段的存在,使SDS有了空间预分配和惰性释放的能力。
对二进制是安全的:二进制可能会有字符和C字符串结尾符 '\0' 冲突,在遍历和获取数据时产生截断异常,而SDS有了len字段,准确了标识了数据长度,不需担心被中间的
'\0' 截断。
(4)SDS 与 C 字符串区别
O(1) 时间复杂度获取字符串长度
C 语言字符串不记录长度信息,需要遍历整个字符串时间复杂度为 O(n),C 字符串遍历时遇到 '\0'
时结束。SDS 中 len 保存这字符串的长度,O(1) 时间复杂度。
空间预分配
SDS 被修改后,程序不仅会为 SDS 分配所需要的必须空间,还会分配额外的未使用空间。
分配规则如下:如果对 SDS 修改后,len 的长度小于 1M,那么程序将分配和 len 相同长度的未使用空间。举个例子,如果
len=10,重新分配后,buf 的实际长度会变为 10(已使用空间)+10(额外空间)+1(空字符)=21。如果对
SDS 修改后 len 长度大于 1M,那么程序将分配 1M 的未使用空间。
惰性空间释放
当对 SDS 进行缩短操作时,程序并不会回收多余的内存空间,而是使用 free 字段将这些字节数量记录下来不释放,后面如果需要
append 操作,则直接使用 free 中未使用的空间,减少了内存的分配。
二进制安全
在 Redis 中不仅可以存储 String 类型的数据,也可能存储一些二进制数据。
二进制数据并不是规则的字符串格式,其中会包含一些特殊的字符如 '\0',在 C 中遇到 '\0' 则表示字符串的结束,但在
SDS 中,标志字符串结束的是 len 属性。
(5)Redis数据类型与底层数据结构
对于链表、hash表和跳表来说,Redis在设计数据结构的时候出发点是一致的,总结起来就是一句话:空间换时间。用牺牲存储空间和微小的计算代价,来换取数据的快速操作。
为了追求速度,每种数据类型使用不同的数据结构支撑,底层数据结构有 6 种:
1.3、单线程模型
Redis6.x之前,一直在说单线程如何如之何的好。那么,具体单线程体现在哪里,又是怎么完成数据读写工作的呢?关于新版本的多线程模型在后面小节单独说,这里先说单线程。
(1)单线程模型
我们要明确的是:Redis 的单线程指的是 Redis 的网络 IO 以及键值对指令读写是由一个线程来执行的。
对于 Redis 的持久化、集群数据同步、异步删除等都是其他线程执行。
所谓单线程是指对数据的所有操作都是由一个线程按顺序挨个执行的,使用单线程好处:
不会因为线程创建导致的性能消耗;
避免上多线程上下文切换引起的 CPU开销;
避免了线程之间的竞争问题,比如添加锁、释放锁、死锁等,不需要考虑各种锁的问题。
代码更清晰,处理逻辑简单。
单线程是否没有充分利用 CPU 资源呢?
官方答案:因为 Redis 是基于内存的操作,使用Redis时,几乎不存在CPU成为瓶颈的情况, Redis主要受限于服务器内存和网络。例如在一个普通的Linux系统上,Redis通过使用pipelining每秒可以处理10万个请求,所以如果应用程序主要使用O(N)或O(log(N))的命令,它几乎不会占用太多CPU。既然单线程容易实现,而且
CPU 不会成为瓶颈,那就顺理成章地采用单线程的方案了。
然而,使用了单线程的处理方式,就意味着到达服务端的请求不可能被立即处理。那么怎么来保证单线程的资源利用率和处理效率呢?
(2) IO多路复用模型和性能优良的事件驱动
Redis 采用 I/O 多路复用技术,并发处理连接。采用了 epoll + 自己实现的简单的事件框架。epoll
中的读、写、关闭、连接都转化成了事件,然后利用 epoll 的多路复用特性,绝不在 IO 上浪费一点时间。
Redis服务端,从整体上来看,其实是一个事件驱动的程序,所有的操作都以事件的方式来进行。
如图所示,Redis的事件驱动架构由套接字、I/O多路复用、文件事件分派器、事件处理器四个部分组成:
套接字(Socket),是对网络中不同主机上的应用进程之间进行双向通信的端点的抽象。
I/O多路复用,通过监视多个描述符,当描述符就绪,则通知程序进行相应的操作,来帮助单个线程高效的处理多个连接请求。
Redis为每个IO多路复用函数都实现了相同的API,因此,底层实现是可以互换的。
Reids默认的IO多路复用机制是epoll,和select/poll等其他多路复用机制相比,epoll具有诸多优点:
事件驱动,Redis设计的事件分为两种,文件事件和时间事件,文件事件是对套接字操作的抽象,而时间事件则是对一些定时操作的抽象。
文件事件:
客户端连接请求(AE_READABLE事件)
客户端命令请求(AE_READABLE事件)和事
服务端命令回复(AE_WRITABLE事件)
时间事件: 分为定时事件和周期性时间;redis的所有时间事件都存放在一个无序链表中,当时间事件执行器运行时,需要遍历链表以确保已经到达时间的事件被全部处理。
可以看到,Redis整个执行方案是通过高效的I/O多路复用件驱动方式加上单线程内存操作来达到优秀的处理效率和极高的吞吐量。
1.4、IO多路复用模型
多路指的是多个 socket 连接,复用指的是复用一个线程。IO多路复用主要有三种技术:select,poll,epoll。epoll
是目前最新最好的IO多路复用技术。
IO多路复用的基本原理是,内核不是监视应用程序本身的连接,而是监视应用程序的文件描述符。当客户端运行时,它将生成具有不同事件类型的套接字。在服务器端,I
/ O 多路复用程序(I / O 多路复用模块)会将消息放入队列(也就是 下图的 I/O 多路复用程序的
socket 队列),然后通过文件事件分派器将其转发到不同的事件处理器。
简单来说:Redis 单线程情况下,内核会一直监听 socket 上的连接请求或者数据请求,一旦有请求到达就交给
Redis 线程处理,这就实现了一个 Redis 线程处理多个 IO 流的效果。
select/epoll 提供了基于事件的回调机制,即针对不同事件的发生,调用相应的事件处理器。所以
Redis 一直在处理事件,提升 Redis 的响应性能。
高性能 IO 多路复用
Redis 线程不会阻塞在某一个特定的监听或已连接套接字上,也就是说,不会阻塞在某一个特定的客户端请求处理上。正因为此,Redis
可以同时和多个客户端连接并处理请求,从而提升并发性
1.5、高效的的 Gossip 通信协议
Redis 采用了 Gossip 协议作为通信协议。Gossip 是一种传播消息的方式,可以类比为瘟疫或者流感的传播方式,使用
Gossip 协议的有:Redis Cluster、Consul、Apache Cassandra
等。Gossip 协议类似病毒扩散的方式,将信息传播到其他的节点,这种协议效率很高,只需要广播到附近节点,然后被广播的节点继续做同样的操作即可。当然这种协议也有一个弊端就是:会存在浪费,哪怕一个节点之前被通知到了,下次被广播后仍然会重复转发。
三、Redis 唯快不破的原理总结
纯内存操作:一般都是简单的存取操作,线程占用的时间很少,时间的花费主要集中在 IO 上,所以读取速度快。
采用单线程模型:保证了每个操作的原子性,也减少了线程的上下文切换和竞争。
Redis 使用IO多路复用模型:非阻塞 IO,使用了单线程来轮询描述符,将数据库的开、关、读、写都转换成了事件,Redis
采用自己实现的事件分离器,效率比较高。
Redis高效数据结构,对数据的操作也比较简单:
(1)整个 Redis 就是一个全局哈希表:他的时间复杂度是 O(1),而且为了防止哈希冲突导致链表过长,Redis
会执行 rehash 操作,扩充 哈希桶数量,减少哈希冲突。并且防止一次性重新映射数据过大导致线程阻塞,采用
渐进式 rehash。巧妙的将一次性拷贝分摊到多次请求过程后总,避免阻塞。
(2)Redis 全程使用 hash 结构:读取速度快,还有一些特殊的数据结构,对数据存储进行了优化,如压缩表,对短数据进行压缩存储,再如,跳表,使用有序的数据结构加快读取的速度。可以根据实际存储的数据类型选择不同编码。
四、Redis6.x的多线程
Redis 6.0.1 于 2020 年 5 月 2 日正式发布了,如 Redis 作者 antirez
所说,这是迄今为止最“企业”化的版本,也是有史以来改动最大的一个 Redis 版本。这个版本提供了诸多令人心动的新特性及功能改进,比如新网络协议RESP3,新的集群代理,ACL等,其中关注度最高的应该是“多线程”了。
Redis的多线程模型,不是传统意义上的多线程并发,而是把socket解析回写的这部分操作并行化,以解决IO上的时间消耗带来的系统瓶颈。
|