Redis系列:深刻理解高性能Redis的本质
mhr18 2024-11-28 08:31 18 浏览 0 评论
1 背景
分布式系统绕不开的核心之一的就是数据缓存,有了缓存的支撑,系统的整体吞吐量会有很大的提升。通过使用缓存,我们把频繁查询的数据由磁盘调度到缓存中,保证数据的高效率读写。
当然,除了在内存内运行还远远不够,我们今天就以具有代表性的缓存中间件Redis为例子,分析下,它是如何达到飞起的效率。
2 Redis高效性能分析
Redis之所以能够提供超高的执行效率,主要从以下几个维度来实现的:
- 存储模式:基于内存实现,而非磁盘
- 数据结构:基于不同业务场景的高效数据结构动态字符串(REDIS_STRING):整数(REDIS_ENCODING_INT)、字符串(REDIS_ENCODING_RAW)双端列表(REDIS_ENCODING_LINKEDLIST)压缩列表(REDIS_ENCODING_ZIPLIST)跳跃表(REDIS_ENCODING_SKIPLIST)哈希表(REDIS_HASH)整数集合(REDIS_ENCODING_INTSET)
- 线程模型: Redis 的网络 IO 以及键值对指令读写是由单个线程来执行的,避免了不必要的contextswitch和竞选
- I/O 模型: 基于I/O多路复用模型,非阻塞的I/O模型
- 恰单的数据编码: 根据实际数据类型,选择合理的数据编码
2.1 官网的性能报告
Redis官方站点中,有对Redis性能做了比较详细的压测,可以参考官方这一篇 How fast is Redis?,
在较高的配置基准下(比如 8C 16G +),在连接数为0~10000的时候,最高QPS可达到120000。Redis以超过60000个连接为基准,仍然能够在这些条件下维持50000个q/s,体现了超高的性能。下图中横轴是连接数,纵轴是QPS。
下面这张图为data size 与整体吞吐量之间的趋向关系:
这个大概可以得出一个容量预估,比如你的服务用户量是多少,预估峰值QPS是多少,集群需要配置多少个实例(虽然实例的多少不能线性计算),可以大致推算出去。
2.2 基于内存实现
Redis的读写操作都是在内存中实现了,相对其他的持久化存储(如MySQL、File等,数据持久化在磁盘上),性能会高很多。因为在们在操作数据的时候,需要通过 IO 操作先将数据读取到内存里,增加工作成本。
上面那张图来源于网络,可以看看他的金字塔模型,越往上执行效率越高,价格也就越贵。下面给出每一层的执行耗时对比:
- 寄存器:0.3 ns
- L1高速缓存:0.9 ns
- L2高速缓存:2.8 ns
- L3高速缓存:12.9 ns
- 主存:120 ns
- 本地二级存储(SSD):50~150 us
- 远程二级存储:30 ms
这样可能不直观,我们举个L1和SSD的对比,如果L1耗时1s的话,SSD中差不多要15~45小时。
因为 CPU 内部集成了内存控制器,所以CPU直接控制了内存,给予通信上的最优带宽。
2.3 适配多元场景的高效数据结构
在 Redis 缓存中,常用的主要数据类型有五种,如下:
- 字符串/REDIS_STRING:适用于 缓存、计数、共享Session、IP统计、分布式锁等。
- 列表/REDIS_LIST: 链表、消息队列、栈、有序的对象列表(如朋友圈的点赞顺序列表、评论顺序列表)。
- 哈希表/REDIS_HASH: 购物车信息、用户信息、Hash类型的(key, field, value)存储对象等。
- 集合/REDIS_SET:无序的唯一的键值结构: 好友、关注、粉丝、感兴趣的人集合等。
- 有序集合/REDIS_ZSET:访问排行榜、点赞排行、粉丝数排行等。
上面这5种Redis 支持的数据类型,能够满足不同业务场景下的数据结构需求。而对于这几类数据类型的区分和支持,目的无非也是为了效率,具体的业务中使用恰当的数据结构才能保证得到应有的效率。
这5种数据类型都有一种或者多种数据结构来支撑,底层数据结构有 7 种。关系如下:
限免我们对这些数据结构一个个的分析。
2.3.1 SDS 简单动态字符串
Redis使用简单动态字符串(simple dynamic string,SDS)来表示字符串,Redis中字符串类型包含的数据结构有:整数(R_INT) 、 字符串(R_RAW)。我们以字符串为例子,常规的字符串,如 "Brand",如果要获取他的长度,需要从头开始遍历,直至遇到 \0 空字符代表结尾,如 C字符串。
C 字符串结构与 SDS 字符串结构 对比图 参照如下:
- free属性的值为0,表示这个SDS没有分配任何未使用空间。
- len属性的值为5,表示这个SDS保存了一个5字节长度的字符串。
- buf是一个char类型的数组,存储真实的字符串,数组的前五个字节分别保存了'B'、'r'、'a'、'n'、'd'五个字符,而最后一个字节则保存了空字符'\0',代表结尾。
注意:SDS遵循C字符串的惯例以空字符结尾,保存空字符的1字节不计算在SDS的len属性中。
比起C字符串,SDS具有以下优点:
- 度获取字符串长度时间复杂度为O(1) 。
C字符串不记录自身长度,获取C字符串长度时必须遍历整个字符串计数得到,复杂度是O(N)
SDS字符串自身记录维护len长度属性,获得SDS字符串长度的复杂度是O(1) - 杜绝缓冲区溢出。
C字符串不记录长度,由于两个C字符串在内存存储上紧邻,在执行字符串拼接strcat时,如果不提前分配足够空间,很可能发生修改s1的数据溢出到s2所在的空间中(缓冲区溢出)。
SDS杜绝了缓冲区溢出问题,它记录了长度,当修改SDS字符串之前,API都会检查SDS的空间是否满足修改的要求,不满足API会自动进行空间扩展。 - 空间预分配,减少修改时的内存重分配次数
SDS 被修改后,程序不仅会为 SDS 分配所需要的空间,还会分配额外的未使用空间。这样,Redis可以减少连续执行字符串增长操作所需的内存重分配次数。
具体分配未使用空间如下2种方式:
- 如修改后长度len小于1MB,就分配和len属性相同大小的未使用空间:free=len。
- 如修改后长度len大于等于1MB,就分配1M的未使用空间:free=1MB。
- 惰性空间释放
惰性空间释放,缩短操作时:SDS避免了缩短字符串时所需的内存重分配操作,并为将来可能有的增长操作提供了优化。当SDS做缩短操作,不会立刻使用内存重分配来收回缩短后多出来的字节,而是保持在free属性里。将来如果需要 append 操作,则直接使用 free 中未使用的空间,减少了内存的分配步骤。
另外,SDS也提供了API手动进行释放SDS未使用空间,避免惰性释放策略会造成内存浪费。 - 二进制安全
C字符串的字符必须符合某种编码,除结尾空字符以外,字符串内部不允许有空字符串,存储有局限性。而在 Redis 中,不仅可以存储 String 类型的数据,也可能存储一些二进制数据。
二进制数据并不是规则的字符串格式,其中会包含一些特殊的字符如 '\0'。在 C 中遇到 '\0' 则表示字符串的结束,但SDS不是,它是以len长度标识结尾。 - 兼容部分C字符串函数。
SDS虽然是二进制安全的,但还是秉承C字符串以空字符结尾的特性,很多函数与C字符串一致不需要重写。
2.3.2 zipList 压缩列表
通过上面的数据结构关系图,可以看出,压缩列表是 List 、Hash、 Set 三种数据类型底层实现之一。
当我们的list列表数据量比较少的时候,且存储的数据轻量的(如小整数值、短字符串)时候, Redis 就会通过压缩列表来进行底层实现。
ziplist 是由一系列特殊编码的连续内存块组成的顺序型的数据结构,在列表头有三个字段 zlbytes、zltail 和 zllen,列表中有多个entry,表尾还有一个 zlend,我们来具体拆解下:
- zlbytes:表示列表占用字节数
- zltail:列表尾的偏移量
- zllen:列表尾的偏移量:列表中的 entry 个数
- entry:存储区,可以包含多个节点,每个节点可以存放整数或者字符串。
- zlend:表示列表结束。
参考代码如下:
struct ziplist<T> {
// 列表占用字节数
int32 zlbytes;
// 列表尾的偏移量,用于快速定位到最后一个节点
int32 zltail_offset;
// 列表entry元素个数
int16 zllength;
// 元素内容列表
T[] entries;
// 标志压缩列表的结束,值恒为 0xFF
int8 zlend;
}
如果查找定位首个元素或最后1个元素,可以通过表头zlbytes、zltail_offset元素快速获取,复杂度是 O(1)。但是查找其他元素时,就没有这么高效了,只能逐个查找下去,比如 entry n 的复杂度就是 O(N)。
2.3.3 linklist 双端列表
Redis List 数据类型经常使用在链表、消息队列、栈、有序的对象列表(如朋友圈的点赞顺序列表、评论顺序列表、关注时间线)等场景,无论是队列(先进先出),还是栈(先进后出),双端列表都能很好的支持。
参考代码如下:
typedef struct list {
// 表头
listnode * head;
// 表尾
listnode * tail;
// 链表所包含的节点数量
unsigned long len;
// 函数:复制节点值
void *(*dup)(void *ptr);
// 函数:释放节点值
void (*free)(void *ptr);
// 函数:对比节点值
int (*match)(void *ptr, void *key);
} list;
Redis 的链表实现的特性可以总结如下:
- 双端:链表节点带有 prev 和 next 指针,获取某个节点的前一节点和后一节点的复杂度都是 O(1)。
- 无环:表头节点的 prev 指针和表尾节点的 next 指针都指向 NULL,对链表的访问以 NULL 为终点。
- 表头指针/表尾指针:通过 list 结构的 head 指针和 tail 指针,获取链表的表头节点和表尾节点的复杂度为 O(1)。
- 链表长度计数器:通过 list 结构的 len 属性来对 list 的链表节点进行计数,获取节点数量的复杂度为O(1)。
- 多态:链表节点使用 void* 指针来保存节点值,并通过 list 结构的 dup、free、match 三个属性为节点值设置类型特定函数,所以链表可以用于保存各种不同类型的值。
使用链表的附加空间相对太高,因为64bit系统中指针是8个字节,所以prev和next指针需要占据16个字节,且链表节点在内存中单独分配,会加剧内存的碎片化,影响内存管理效率。
考虑到链表的以上缺点,Redis后续版本对列表数据结构进行改造,使用quicklist代替了ziplist和linkedlist。 作为ziplist 和 linkedlist 的混合体,它将 linkedlist 按段切分,每一段使用 ziplist 来紧凑存储,多个 ziplist 之间使用双向指针串接起来。
这样,性能就的得到了更大的提升。
2.3.4 Hhash 字典
无论何种类型(string、list、hash、set、zset),Redis都是以一个Hash结构的形式来保存键值对的。整体是一个数组,数组中的每个元素都是一个独立的对象,被称为哈系桶,比如图中1 ~ n, 对应的entry保存着实际具体值的指针。
上图中的全局哈希表,它的时间复杂度是 O(1),只需要计算每个键的哈希值,便知道对应的哈希桶位置,定位桶中的 entry ,并找到对应数据。这个执行效率就很高了。
为了解决可能存在的冲突,采用了链式哈希的做法,也就是同一个桶里面的元素使用链表保存。
2.3.5 intset 整数集合
如果你的集合只有整数值元素,并且数量是轻量的,这时候Redis会使用使用整数集合作为Redis集合的底层数据结构。参考如下代码:
typedef struct intset{
// 编码格式
uint32_t encoding;
// 集合中的元素个数
uint32_t length;
// 保存元素数据
int8_t contents[];
} intset;
我们拆解下:
- encoding: 编码方式
- length: 数组中元素个数,也就是数组的整体长度
- contents[]: 整数集合,集合的每个元素都是数组的一个数组项(item)。具有以下特点:按值的大小增序排列不包含任何重复项
2.3.6 skipList 跳跃表
skiplist(即跳跃表)是一种有序数据结构,所以它也是ZSet数据类型中的一种,通过在每个节点中维持多个指向其他节点的指针,达到快速定位的目标。
跳跃表的平均的节点搜索,平均时间复杂度是 O(logN)、最差时间复杂度是 O(N),还可以通过顺序性操作来批量处理节点。 跳跃表是基于链表的改良,在它基础上,增加了多层级索引,通过索引不断跳转,最终定好位到真实的数据项。这个方式是不是让大家想到b+tree,理念上有点接近,如下图所示:
可以看出,需要获取 68 这个元素需要经历3次查找,需要获取 97则需要经历4次查找。
2.4 单线程模型
Redis 的单线程主要是指Redis的网络IO和键值对读写是由一个线程来完成的,Redis在处理客户端的请求时包括获取 (socket 读)、解析、执行、内容返回 (socket 写) 等都由一个顺序串行的主线程处理,这就是所谓的“单线程”。这也是Redis对外提供键值存储服务的主要流程。
但Redis的其他功能, 比如持久化、异步删除、集群数据同步等等,其实是由额外的线程执行的。 可以这么说,Redis工作线程是单线程的。但是,整个Redis来说,是多线程的。
2.4.1 为何是单线程?
那在主流程中使用单线程,主要是出于什么原因呢?
- 整体吞吐量降低
适当的扩增线程,是为了有效的利用cpu的性能,让它跟内存达到一个利用的最优值。但频繁的Redis读写,如果没有对线程进行有效管理,不但对系统的吞吐量没有提升,反而可能导致下降。 - CPU上下文切换
在运行任务的时候,CPU需要把任务加载到CPU寄存器中进行计算,当切换到其他thread时,需要将当前上下文存储在系统内核中,以便后续重新执行计算时再次加载。
就像你做专心做一件事时,频繁切换,频繁被打断,这个代价是非常高的。
如图中,切换上下文时,我们需要完成一系列工作,save context、switch、restore context等,这种操作越频繁越是耗费资源。 - 共享资源的并发控制问题
引入了程序执行顺序的不确定性,带来了并发读写的一系列问题,增加了系统复杂度。同时可能存在线程切换、甚至加锁解锁、死锁造成的性能损耗。 - 内存才是核心关注点
对于 Redis 框架来说, 主要的性能瓶颈是内存或者网络带宽,而非 CPU。
2.4.2 单线程的好处
- 避免线程创建过多导致的性能消耗,反而降低整体吞吐能力。
- 避免上下文切换引起的 CPU 额外的开销。
- 避免了线程之间的竞争问题,如加锁、解锁、死锁等,都会造成性能损耗。
- 无需额外考虑多线程带来的程序复杂度,代码更清晰,处理逻辑简单。
2.4.3 单线程是否有效利用CPU
官方这么说
It’s not very frequent that CPU becomes your bottleneck with Redis, as usually Redis is either memory or network bound. For instance, using pipelining Redis running on an average Linux system can deliver even 1 million requests per second, so if your application mainly uses O(N) or O(log(N)) commands, it is hardly going to use too much CPU.
大概意思是,Redis是完全的纯内存操作,执行速度是非常快的,CPU通常不会是瓶颈,因为大多数请求不会是CPU密集型的。参考
Redis真正的性能瓶颈在网络IO,也就是客户端和服务端之间的网络传输延迟,因此Redis选择了单线程的IO多路复用来实现它的核心网络模型。
2.5 I/O 多路复用模型
服务端网络编程常见的 I/O 模型有四种:同步阻塞IO(Blocking IO)、同步非阻塞IO(Non-blocking IO)、IO多路复用(IO Multiplexing)、异步IO(Asynchronous IO)。
Redis 采用的是 I/O 多路复用技术,并发的去处理连接,它的多路复用程序函数有 select、poll、epoll、kqueue。以 epoll (目前最新的也是最好的多路复用技术)函数为例,当客服端执行 read、write、accept、close 等操作命令时,它会将命令封装成一个个事件,然后利用 epoll 多路复用的特性来避免 I/O 阻塞。
下面我们看看普通 I/O 模型 和 Redis的 I/O 多路复模型的的区别,来分析Redis高频请求下如何保持高效执行。
2.5.1 普通 I/O 模型
先来看一下传统的阻塞 I/O 模型到底是如何工作的:当使用 read 或者 write 对某一个文件描述符(File Descriptor:FD)进行读写时,如果当前 FD 不可读或不可写,整个 Redis 服务就不会对其它的操作作出响应,导致整个服务不可用。
这也就是传统意义上的,也就是我们在编程中使用最多的阻塞模型:
阻塞模型虽然开发中非常常见也非常易于理解,但是由于它会影响其他 FD 对应的服务,所以在需要处理多个客户端任务的时候,往往都不会使用阻塞模型。
2.5.2 I/O 多路复用
多路 复用指的是:多个socket连接复用一个线程。这种模式下,内核不会去监视应用程序的连接,而是监视文件描述符。
当客户端发起请求的时候,会生成不同事件类型的套接字。而在服务端,因为使用了 I/O 多路复用技术,所以不是阻塞式的同步执行,而是将消息放入 socket 队列(参考下图的 I/O Multiplexing module),然后通过 File event Dispatcher 将其转发到不同的事件处理器上,如accept、read、send。
综上,我们得出如下特性:
- 单线程模式,内核持续监听 socket 上的连接及数据请求,一监听就交予Redis线程处理,达到单个线程处理多个I/O 流的效果。
- epoll 提供了基于事件的回调机制。不同事件调用对应的事件处理器。Redis可以持续性的高效处理事件,性能同步提升。
- Redis 不阻塞任一客户端发起的请求,所以可以同时和多个客户端连接并处理请求,聚币并发执行的能力。
3 高性能Redis总结
- 基于内存实现,而非磁盘,大都是简单的存取操作,资源主要消耗在 IO 上,所以读取速度快。
- 数据结构:基于不同业务场景的高效数据结构动态字符串(REDIS_STRING):整数(REDIS_ENCODING_INT)、字符串(REDIS_ENCODING_RAW)双端列表(REDIS_ENCODING_LINKEDLIST)压缩列表(REDIS_ENCODING_ZIPLIST)跳跃表(REDIS_ENCODING_SKIPLIST)哈希表(REDIS_HASH)整数集合(REDIS_ENCODING_INTSET)
- 线程模型:Redis 的网络 IO 以及键值对指令读写是由单个线程来执行的,避免了不必要的contextswitch和竞选
- I/O 模型:基于I/O多路复用模型,非阻塞的I/O模型
- 恰单的数据编码:根据实际数据类型,选择合理的数据编码
- Redis 本身是一个全局 哈希表,他的时间复杂度是 O(1),另外为了防止哈希冲突导致链表过长,执行 rehash 操作进行扩充,减少哈希冲突。
为帮助开发者们提升面试技能、有机会入职BATJ等大厂公司,特别制作了这个专辑——这一次整体放出。
大致内容包括了: Java 集合、JVM、多线程、并发编程、设计模式、Spring全家桶、Java、MyBatis、ZooKeeper、Dubbo、Elasticsearch、Memcached、MongoDB、Redis、MySQL、RabbitMQ、Kafka、Linux、Netty、Tomcat等大厂面试题等、等技术栈!
欢迎大家关注公众号【Java烂猪皮】,回复【666】,获取以上最新Java后端架构VIP学习资料以及视频学习教程,然后一起学习,一文在手,面试我有。
每一个专栏都是大家非常关心,和非常有价值的话题,如果我的文章对你有所帮助,还请帮忙点赞、好评、转发一下,你的支持会激励我输出更高质量的文章,非常感谢!
相关推荐
- 【预警通报】关于WebLogic存在远程代码执行高危漏洞的预警通报
-
近日,Oracle官方发布了2021年1月关键补丁更新公告CPU(CriticalPatchUpdate),共修复了包括CVE-2021-2109(WeblogicServer远程代码执行漏洞)...
- 医院信息系统突发应急演练记录(医院信息化应急演练)
-
信息系统突发事件应急预案演练记录演练内容信息系统突发事件应急预案演练参与人员信息科参与科室:全院各部门日期xxxx-xx-xx时间20:00至24:00地点信息科记录:xxx1、...
- 一文掌握怎么利用Shell+Python实现完美版的多数据源备份程序
-
简介:在当今数字化时代,无论是企业还是个人,数据的安全性和业务的连续性都是至关重要的。数据一旦丢失,可能会造成无法估量的损失。因此,如何有效地对分布在不同位置的数据进行备份,尤其是异地备份,成为了一个...
- docker搭建系统环境(docker搭建centos)
-
Docker安装(CentOS7)1.卸载旧版Docker#检查已安装版本yumlistinstalled|grepdocker#卸载旧版本yumremove-ydocker.x...
- 基础篇:数据库 SQL 入门教程(sql数据库入门书籍推荐)
-
SQL介绍什么是SQLSQL指结构化查询语言,是用于访问和处理数据库的标准的计算机语言。它使我们有能力访问数据库,可与多种数据库程序协同工作,如MSAccess、DB2、Informix、M...
- Java21杀手级新特性!3行代码性能翻倍
-
导语某券商系统用这招,交易延迟从12ms降到0.8ms!本文揭秘Oracle官方未公开的Record模式匹配+虚拟线程深度优化+向量API神操作,代码量直降70%!一、Record模式匹配(代码量↓8...
- 一文读懂JDK21的虚拟线程(java虚拟线程)
-
概述JDK21已于2023年9月19日发布,作为Oracle标准Java实现的一个LTS版本发布,发布了15想新特性,其中虚拟线程呼声较高。虚拟线程是JDK21中引入的一项重要特性,它是一种轻量级的...
- 效率!MacOS下超级好用的Linux虚拟工具:Lima
-
对于MacOS用户来说,搭建Linux虚拟环境一直是件让人头疼的事。无论是VirtualBox还是商业的VMware,都显得过于笨重且配置复杂。今天,我们要介绍一个轻巧方便的纯命令行Linux虚拟工具...
- 所谓SaaS(所谓三维目标一般都应包括)
-
2010年前后,一个科技媒体的主编写一些关于云计算的概念性问题,就可以作为头版头条了。那时候的云计算,更多的还停留在一些概念性的问题上。而基于云计算而生的SaaS更是“养在深闺人未识”,一度成为被IT...
- ORA-00600 「25027」 「x」报错(报错0xc0000001)
-
问题现象:在用到LOB大对象的业务中,进行数据的插入,失败了,在报警文件中报错:ORA-00600:内部错误代码,参数:[25027],[10],[0],[],[],[],[],[...
- 安卓7源码编译(安卓源码编译环境lunch失败,uname命令找不到)
-
前面已经下载好源码了,接下来是下载手机对应的二进制驱动执行编译源码命令下载厂商驱动https://developers.google.com/android/drivers?hl=zh-cn搜索NGI...
- 编译安卓源码(编译安卓源码 电脑配置)
-
前面已经下载好源码了,接下来是下载手机对应的二进制驱动执行编译源码命令下载厂商驱动https://developers.google.com/android/drivers?hl=zh-cn搜索NGI...
- 360 Vulcan Team首战告捷 以17.5万美金强势领跑2019“天府杯“
-
2019年11月16日,由360集团、百度、腾讯、阿里巴巴、清华大学与中科院等多家企业和研究机构在成都联合主办了2019“天府杯”国际网络安全大赛暨2019天府国际网络安全高峰论坛。而开幕当日最激荡人...
- Syslog 日志分析与异常检测技巧(syslog发送日志配置)
-
系统日志包含有助于分析网络设备整体运行状况的重要信息。然而,理解并从中提取有效数据往往颇具挑战。本文将详解从基础命令行工具到专业日志管理软件的全流程分析技巧,助你高效挖掘Syslog日志价值。Gr...
- 从Oracle演进看数据库技术的发展(从oracle演进看数据库技术的发展的过程)
-
数据库技术发展本质上是应用需求驱动与基础架构演进的双向奔赴,如何分析其技术发展的脉络和方向?考虑到oracle数据库仍然是这个领域的王者,以其为例,管中窥豹,对其从Oracle8i到23ai版本的核...
你 发表评论:
欢迎- 一周热门
- 最近发表
- 标签列表
-
- oracle位图索引 (74)
- oracle基目录 (50)
- oracle批量插入数据 (65)
- oracle事务隔离级别 (53)
- oracle主从同步 (55)
- oracle 乐观锁 (51)
- redis 命令 (78)
- php redis (88)
- redis 存储 (66)
- redis 锁 (69)
- 启动 redis (66)
- redis 时间 (56)
- redis 删除 (67)
- redis内存 (57)
- redis并发 (52)
- redis 主从 (69)
- redis 订阅 (51)
- redis 登录 (54)
- redis 面试 (58)
- 阿里 redis (59)
- redis 搭建 (53)
- redis的缓存 (55)
- lua redis (58)
- redis 连接池 (61)
- redis 限流 (51)