Redis 核心篇:唯快不破的秘密(下)

本文涉及的产品
Redis 开源版,标准版 2GB
推荐场景:
搭建游戏排行榜
云数据库 Tair(兼容Redis),内存型 2GB
简介: 接上文。

双端列表


Redis List 数据类型通常被用于队列、微博关注人时间轴列表等场景。不管是先进先出的队列,还是先进后出的栈,双端列表都很好的支持这些特性。


image.png


Redis 的链表实现的特性可以总结如下:


  • 双端:链表节点带有 prev 和 next 指针,获取某个节点的前置节点和后置节点的复杂度都是 O(1)。


  • 无环:表头节点的 prev 指针和表尾节点的 next 指针都指向 NULL,对链表的访问以 NULL 为终点。


  • 带表头指针和表尾指针:通过 list 结构的 head 指针和 tail 指针,程序获取链表的表头节点和表尾节点的复杂度为 O(1)。


  • 带链表长度计数器:程序使用 list 结构的 len 属性来对 list 持有的链表节点进行计数,程序获取链表中节点数量的复杂度为 O(1)。


  • 多态:链表节点使用 void* 指针来保存节点值,并且可以通过 list 结构的 dup、free、match 三个属性为节点值设置类型特定函数,所以链表可以用于保存各种不同类型的值。


后续版本对列表数据结构进行了改造,使用 quicklist 代替了 ziplist 和 linkedlist。


quicklist 是 ziplist 和 linkedlist 的混合体,它将 linkedlist 按段切分,每一段使用 ziplist 来紧凑存储,多个 ziplist 之间使用双向指针串接起来。


image.png


这也是为何 Redis 快的原因,不放过任何一个可以提升性能的细节。


skipList 跳跃表


sorted set 类型的排序功能便是通过「跳跃列表」数据结构来实现。


跳跃表(skiplist)是一种有序数据结构,它通过在每个节点中维持多个指向其他节点的指针,从而达到快速访问节点的目的。


跳跃表支持平均 O(logN)、最坏 O(N)复杂度的节点查找,还可以通过顺序性操作来批量处理节点。


跳表在链表的基础上,增加了多层级索引,通过索引位置的几个跳转,实现数据的快速定位,如下图所示:


image.png


当需要查找 40 这个元素需要经历 三次查找。


整数数组(intset)


当一个集合只包含整数值元素,并且这个集合的元素数量不多时,Redis 就会使用整数集合作为集合键的底层实现。结构如下:


typedef struct intset{
     //编码方式
     uint32_t encoding;
     //集合包含的元素数量
     uint32_t length;
     //保存元素的数组
     int8_t contents[];
}intset;


contents 数组是整数集合的底层实现:整数集合的每个元素都是 contents 数组的一个数组项(item),各个项在数组中按值的大小从小到大有序地排列,并且数组中不包含任何重复项。length 属性记录了整数集合包含的元素数量,也即是 contents 数组的长度。


合理的数据编码


Redis 使用对象(redisObject)来表示数据库中的键值,当我们在 Redis 中创建一个键值对时,至少创建两个对象,一个对象是用做键值对的键对象,另一个是键值对的值对象。


例如我们执行 SET MSG XXX 时,键值对的键是一个包含了字符串“MSG“的对象,键值对的值对象是包含字符串"XXX"的对象。


redisObject


typedef struct redisObject{
    //类型
   unsigned type:4;
   //编码
   unsigned encoding:4;
   //指向底层数据结构的指针
   void *ptr;
    //...
 }robj;


其中 type 字段记录了对象的类型,包含字符串对象、列表对象、哈希对象、集合对象、有序集合对象。


对于每一种数据类型来说,底层的支持可能是多种数据结构,什么时候使用哪种数据结构,这就涉及到了编码转化的问题。


那我们就来看看,不同的数据类型是如何进行编码转化的:


String:存储数字的话,采用 int 类型的编码,如果是非数字的话,采用 raw 编码;


List:List 对象的编码可以是 ziplist 或 linkedlist,字符串长度 < 64 字节且元素个数 < 512 使用 ziplist 编码,否则转化为 linkedlist 编码;


注意:这两个条件是可以修改的,在 redis.conf 中:


list-max-ziplist-entries 512
list-max-ziplist-value 64


Hash:Hash 对象的编码可以是 ziplist 或 hashtable。


当 Hash 对象同时满足以下两个条件时,Hash 对象采用 ziplist 编码:


  • Hash 对象保存的所有键值对的键和值的字符串长度均小于 64 字节。


  • Hash 对象保存的键值对数量小于 512 个。


否则就是 hashtable 编码。


Set:Set 对象的编码可以是 intset 或 hashtable,intset 编码的对象使用整数集合作为底层实现,把所有元素都保存在一个整数集合里面。


保存元素为整数且元素个数小于一定范围使用 intset 编码,任意条件不满足,则使用 hashtable 编码;


Zset:Zset 对象的编码可以是 ziplist 或 zkiplist,当采用 ziplist 编码存储时,每个集合元素使用两个紧挨在一起的压缩列表来存储。


Ziplist 压缩列表第一个节点存储元素的成员,第二个节点存储元素的分值,并且按分值大小从小到大有序排列。


image.png


当 Zset 对象同时满足一下两个条件时,采用 ziplist 编码:


  • Zset 保存的元素个数小于 128。


  • Zset 元素的成员长度都小于 64 字节。


如果不满足以上条件的任意一个,ziplist 就会转化为 zkiplist 编码。注意:这两个条件是可以修改的,在 redis.conf 中:


zset-max-ziplist-entries 128
zset-max-ziplist-value 64


单线程模型


65 哥:为什么 Redis 是单线程的而不用多线程并行执行充分利用 CPU 呢?


我们要明确的是:Redis 的单线程指的是 Redis 的网络 IO 以及键值对指令读写是由一个线程来执行的。 对于 Redis 的持久化、集群数据同步、异步删除等都是其他线程执行。

至于为啥用单线程,我们先了解多线程有什么缺点。


多线程的弊端


使用多线程,通常可以增加系统吞吐量,充分利用 CPU 资源。


但是,使用多线程后,没有良好的系统设计,可能会出现如下图所示的场景,增加了线程数量,前期吞吐量会增加,再进一步新增线程的时候,系统吞吐量几乎不再新增,甚至会下降!


image.png


在运行每个任务之前,CPU 需要知道任务在何处加载并开始运行。也就是说,系统需要帮助它预先设置 CPU 寄存器和程序计数器,这称为 CPU 上下文。


这些保存的上下文存储在系统内核中,并在重新计划任务时再次加载。这样,任务的原始状态将不会受到影响,并且该任务将看起来正在连续运行。


切换上下文时,我们需要完成一系列工作,这是非常消耗资源的操作。


另外,当多线程并行修改共享数据的时候,为了保证数据正确,需要加锁机制就会带来额外的性能开销,面临的共享资源的并发访问控制问题。


引入多线程开发,就需要使用同步原语来保护共享资源的并发读写,增加代码复杂度和调试难度。


单线程又什么好处?


  1. 不会因为线程创建导致的性能消耗;


  1. 避免上下文切换引起的 CPU 消耗,没有多线程切换的开销;


  1. 避免了线程之间的竞争问题,比如添加锁、释放锁、死锁等,不需要考虑各种锁问题。


  1. 代码更清晰,处理逻辑简单。


单线程是否没有充分利用 CPU 资源呢?


官方答案:因为 Redis 是基于内存的操作,CPU 不是 Redis 的瓶颈,Redis 的瓶颈最有可能是机器内存的大小或者网络带宽。既然单线程容易实现,而且 CPU 不会成为瓶颈,那就顺理成章地采用单线程的方案了。原文地址:https://redis.io/topics/faq。


I/O 多路复用模型


Redis 采用 I/O 多路复用技术,并发处理连接。采用了 epoll + 自己实现的简单的事件框架。epoll 中的读、写、关闭、连接都转化成了事件,然后利用 epoll 的多路复用特性,绝不在 IO 上浪费一点时间。


65 哥:那什么是 I/O 多路复用呢?


在解释 IO 多虑复用之前我们先了解下基本 IO 操作会经历什么。


基本 IO 模型


一个基本的网络 IO 模型,当处理 get 请求,会经历以下过程:


  1. 和客户端建立建立 accept;


  1. 从 socket 种读取请求 recv;


  1. 解析客户端发送的请求 parse;\


  1. 执行 get 指令;


  1. 响应客户端数据,也就是 向 socket 写回数据。


其中,bind/listen、accept、recv、parse 和 send 属于网络 IO 处理,而 get 属于键值数据操作。既然 Redis 是单线程,那么,最基本的一种实现是在一个线程中依次执行上面说的这些操作。


关键点就是 accept 和 recv 会出现阻塞,当 Redis 监听到一个客户端有连接请求,但一直未能成功建立起连接时,会阻塞在 accept() 函数这里,导致其他客户端无法和 Redis 建立连接。


类似的,当 Redis 通过 recv() 从一个客户端读取数据时,如果数据一直没有到达,Redis 也会一直阻塞在 recv()。


image.png


阻塞的原因由于使用传统阻塞 IO ,也就是在执行 read、accept 、recv 等网络操作会一直阻塞等待。如下图所示:


image.png


IO 多路复用


多路指的是多个 socket 连接,复用指的是复用一个线程。多路复用主要有三种技术:select,poll,epoll。epoll 是最新的也是目前最好的多路复用技术。


它的基本原理是,内核不是监视应用程序本身的连接,而是监视应用程序的文件描述符。

当客户端运行时,它将生成具有不同事件类型的套接字。在服务器端,I / O 多路复用程序(I / O 多路复用模块)会将消息放入队列(也就是 下图的 I/O 多路复用程序的 socket 队列),然后通过文件事件分派器将其转发到不同的事件处理器。


简单来说:Redis 单线程情况下,内核会一直监听 socket 上的连接请求或者数据请求,一旦有请求到达就交给 Redis 线程处理,这就实现了一个 Redis 线程处理多个 IO 流的效果。


select/epoll 提供了基于事件的回调机制,即针对不同事件的发生,调用相应的事件处理器。所以 Redis 一直在处理事件,提升 Redis 的响应性能。


image.png


Redis 线程不会阻塞在某一个特定的监听或已连接套接字上,也就是说,不会阻塞在某一个特定的客户端请求处理上。正因为此,Redis 可以同时和多个客户端连接并处理请求,从而提升并发性。


唯快不破的原理总结


65 哥:学完之后我终于知道 Redis 为何快的本质原因了,「码哥」你别说话,我来总结!一会我再点赞和分享这篇文章,让更多人知道 Redis 快的核心原理。


  1. 纯内存操作,一般都是简单的存取操作,线程占用的时间很多,时间的花费主要集中在 IO 上,所以读取速度快。


  1. 整个 Redis 就是一个全局 哈希表,他的时间复杂度是 O(1),而且为了防止哈希冲突导致链表过长,Redis 会执行 rehash 操作,扩充 哈希桶数量,减少哈希冲突。并且防止一次性 重新映射数据过大导致线程阻塞,采用 渐进式 rehash。巧妙的将一次性拷贝分摊到多次请求过程后总,避免阻塞。


  1. Redis 使用的是非阻塞 IO:IO 多路复用,使用了单线程来轮询描述符,将数据库的开、关、读、写都转换成了事件,Redis 采用自己实现的事件分离器,效率比较高。


  1. 采用单线程模型,保证了每个操作的原子性,也减少了线程的上下文切换和竞争。


  1. Redis 全程使用 hash 结构,读取速度快,还有一些特殊的数据结构,对数据存储进行了优化,如压缩表,对短数据进行压缩存储,再如,跳表,使用有序的数据结构加快读取的速度。


  1. 根据实际存储的数据类型选择不同编码


下一篇「码哥字节」将> 天下武功,无坚不摧,唯快不破!

相关实践学习
基于Redis实现在线游戏积分排行榜
本场景将介绍如何基于Redis数据库实现在线游戏中的游戏玩家积分排行榜功能。
云数据库 Redis 版使用教程
云数据库Redis版是兼容Redis协议标准的、提供持久化的内存数据库服务,基于高可靠双机热备架构及可无缝扩展的集群架构,满足高读写性能场景及容量需弹性变配的业务需求。 产品详情:https://www.aliyun.com/product/kvstore &nbsp; &nbsp; ------------------------------------------------------------------------- 阿里云数据库体验:数据库上云实战 开发者云会免费提供一台带自建MySQL的源数据库&nbsp;ECS 实例和一台目标数据库&nbsp;RDS实例。跟着指引,您可以一步步实现将ECS自建数据库迁移到目标数据库RDS。 点击下方链接,领取免费ECS&amp;RDS资源,30分钟完成数据库上云实战!https://developer.aliyun.com/adc/scenario/51eefbd1894e42f6bb9acacadd3f9121?spm=a2c6h.13788135.J_3257954370.9.4ba85f24utseFl
相关文章
|
存储 缓存 NoSQL
【Redis】Redis核心知识
Redis可谓后端程序员技术栈中的重中之重了,逢考必问,整理一波。
302 0
【Redis】Redis核心知识
|
运维 NoSQL 小程序
SpringBoot配置文件加密jasypt【数据库配置加密、redis配置加密、核心参数加密】
SpringBoot配置文件加密jasypt【数据库配置加密、redis配置加密、核心参数加密】
374 0
|
存储 消息中间件 缓存
掌握9个核心知识点轻松玩转Redis
掌握9个核心知识点轻松玩转Redis
123 0
|
存储 缓存 JSON
Redis 核心篇:唯快不破的秘密(上)
学习一个技术,通常只接触了零散的技术点,没有在脑海里建立一个完整的知识框架和架构体系,没有系统观。这样会很吃力,而且会出现一看好像自己会,过后就忘记,一脸懵逼。 跟着「码哥字节」一起吃透 Redis,深层次的掌握 Redis 核心原理以及实战技巧。一起搭建一套完整的知识框架,学会全局观去整理整个知识体系。 系统观其实是至关重要的,从某种程度上说,在解决问题时,拥有了系统观,就意味着你能有依据、有章法地定位和解决问题。
183 0
Redis 核心篇:唯快不破的秘密(上)
|
存储 NoSQL 关系型数据库
Redis 面霸篇:高频问题横扫核心知识点 (一)
从高频面试问题跟大家一起横扫 Redis 核心知识点,从根本上理解 Redis ,不做八股文的工具人,做扭转乾坤的大神。
216 0
Redis 面霸篇:高频问题横扫核心知识点 (一)
|
NoSQL Redis
Redis 核心数据结构和应用(下)
常用 5 种数据类型 string, list, set, hash, zset
124 0
Redis 核心数据结构和应用(下)
|
存储 缓存 NoSQL
Redis 核心数据结构和应用(上)
常用 5 种数据类型 string, list, set, hash, zset
131 0
Redis 核心数据结构和应用(上)
|
缓存 运维 NoSQL
重磅下载 | Redis最佳实践与实战指南 源代码核心贡献者带你学习Redis关键技术
本书由七天玩转Redis实训营课程内容整理而成,不仅系统性地介绍Redis的整体架构及在多种场景下的最佳实践经验,而且揭秘阿里云Redis开发规范和运维解法,更有基于Redis的开发实操教程。
37152 0
重磅下载 | Redis最佳实践与实战指南 源代码核心贡献者带你学习Redis关键技术
|
存储 缓存 监控
Redis 在互金核心账务系统中的场景实践
Redis是一款开源的、网络化的、基于内存的、可进行数据持久化KEY-VALUE的存储系统。Redis通过KEY映射VALUE的方式来建立字典来保存数据,支持多类型存储包括STRING、LIST,SET,SORT SET和HASH等,可以在这些数据类型上做很多原子性操作。Redis将数据存储在内存里面,而且它发送给Redis的命令请求不需要经过典型的查询分析器(PARSER)或查询优化器(OPTIMIZER)处理,所以Redis对自身存储的数据执行随机读写的速度是非常快速的。
414 0
Redis 在互金核心账务系统中的场景实践