first commit

This commit is contained in:
张乾
2024-10-15 21:15:28 +08:00
parent 1b0c35dd30
commit 914d92856f
72 changed files with 3115 additions and 3 deletions

View File

@ -0,0 +1,96 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
18 Redis协议的请求和响应有哪些“套路”可循
你好,我是你的缓存课老师陈波,欢迎进入第 18 课时“Redis 协议分析”的学习本课时主要学习Redis的设计原则、三种响应模式、2种请求格式、5种响应格式。
Redis 协议
Redis 支持 8 种核心数据结构每种数据结构都有一系列的操作指令除此之外Redis 还有事务、集群、发布订阅、脚本等一系列相关的指令。为了方便以一种统一的风格和原则来设计和使用这些指令Redis 设计了 RESP即 Redis Serialization Protocol中文意思是 Redis 序列化协议。RESP 是二进制安全协议,可以供 Redis 或其他任何 Client-Server 使用。在 Redis 内部,还会基于 RESP 进一步扩展细节。
设计原则
Redis 序列化协议的设计原则有三个:
第一是实现简单;
第二是可快速解析;
第三是便于阅读。
Redis 协议的请求响应模型有三种,除了 2 种特殊模式,其他基本都是 ping-pong 模式,即 client 发送一个请求server 回复一个响应,一问一答的访问模式。
2 种特殊模式:
pipeline 模式,即 client 一次连续发送多个请求,然后等待 server 响应server 处理完请求后,把响应返回给 client。
pub/sub 模式。即发布订阅模式client 通过 subscribe 订阅一个 channel然后 client 进入订阅状态静静等待。当有消息产生时server 会持续自动推送消息给 client不需要 client 的额外请求。而且客户端在进入订阅状态后,只可接受订阅相关的命令如 SUBSCRIBE、PSUBSCRIBE、UNSUBSCRIBE 和 PUNSUBSCRIBE除了这些命令其他命令一律失效。
Redis 协议的请求和响应也是有固定套路的。
对于请求指令,格式有 2 种类型。
当你没有 redis-client但希望可以用通用工具 telnet直接与 Redis 交互时Redis 协议虽然简单易于阅读,但在交互式会话中使用,并不容易拼写,此时可以用第一种格式,即 inline cmd 内联命令格式。使用 inline cmd 内联格式,只需要用空格分隔请求指令及参数,简单快速,一个简单的例子如 mget key1 key2\r\n。
第二种格式是 Array 数组格式类型。请求指令用的数组类型,与 Redis 响应的数组类型相同,后面在介绍响应格式类型时会详细介绍。
响应格式
Redis 协议的响应格式有 5 种,分别是:
simple strings 简单字符串类型,以 + 开头,后面跟字符串,以 CRLF即 \r\n结尾。这种类型不是二进制安全类型字符串中不能包含 \r 或者 \n。比如许多响应回复以 OK 作为操作成功的标志,协议内容就是 +OK\r\n 。
Redis 协议将错误作为一种专门的类型,格式同简单字符串类型,唯一不同的是以 -减号开头。Redis 内部实现对 Redis 协议做了进一步规范,减号后面一般先跟 ERR 或者 WRONGTYPE然后再跟其他简单字符串最后以 CRLF回车换行结束。这里给了两个示例client 在解析响应时,一旦发现 - 开头,就知道收到 Error 响应。
Integer 整数类型。整数类型以 开头后面跟字符串表示的数字最后以回车换行结尾。Redis 中许多命令都返回整数,但整数的含义要由具体命令来确定。比如,对于 incr 指令,:后的整数表示变更后的数值;对于 llen 表示 list 列表的长度,对于 exists 指令1 表示 key 存在0 表示 key 不存在。这里给个例子,:后面跟了个 1000然后回车换行结束。
bulk strings 字符串块类型。字符串块分头部和真正字符串内容两部分。字符串块类型的头部, 为 \( 开头,随后跟真正字符串内容的字节长度,然后以 CRLF 结尾。字符串块的头部之后,跟随真正的字符串内容,最后以 CRLF 结束字符串块。字符串块用于表示二进制安全的字符串,最大长度可以支持 512MB。一个常规的例子“\)6\r\nfoobar\r\n”对于空字串可以表示为 “\(0\r\n\r\n”NULL字串 “\)-1\r\n”。
Arrays 数组类型,如果一个命令需要返回多条数据就需要用数组格式类型,另外,前面提到 client 的请求命令也是主要采用这种格式。
Arrays 数组类型,以 * 开头,随后跟一个数组长度 N然后以回车换行结尾然后后面跟随 N 个数组元素,每个数组元素的类型,可以是 Redis 协议中除内联格式外的任何一种类型。
比如一个字符串块的数组实例,*2\r\n\(3\r\nget\r\n\)3\r\nkey\r\n。整数数组实例”*3\r\n:1\r\n:2\r\n:3\r\n”混合数组实例”*3\r\n :1\r\n-Bar\r\n$6\r\n foobar\r\n”空数组”0\r\n”NULL数组”-1\r\n”。
协议分类
Redis 协议主要分为 16 种,其中 8 种协议对应前面我们讲到的 8 种数据类型,你选择了使用什么数据类型,就使用对应的响应操作指令即可。剩下 8 种协议如下所示。
pub-sub 发布订阅协议client 可以订阅 channel持续等待 server 推送消息。
事务协议,事务协议可以用 multi 和 exec 封装一些列指令,来一次性执行。
脚本协议,关键指令是 eval、evalsha 和 script等。
连接协议,主要包括权限控制,切换 DB关闭连接等。
复制协议,包括 slaveof、role、psync 等。
配置协议config set/get 等,可以在线修改/获取配置。
调试统计协议,如 slowlogmonitorinfo 等。
其他内部命令,如 migratedumprestore 等。
Redis client 的使用及改进
由于 Redis 使用广泛,几乎所有主流语言都有对 Redis 开发了对应的 client。以 Java 语言为例,广泛使用的有 Jedis、Redisson 等。对于 Jedis client它的优势是轻量简洁便于集成和改造它支持连接池提供指令维度的操作几乎支持 Redis 的所有指令但它不支持读写分离。Redisson 基于 Netty 实现,非阻塞 IO性能较高而且支持异步请求和连接池还支持读写分离、读负载均衡它内建了 tomcat Session ,支持 spring session 集成,但 redisson 实现相对复杂。
在新项目启动时,如果只是简单的 Redis 访问业务场景,可以直接用 Jedis甚至可以简单封装 Jedis实现 master-slave 的读写分离方案。如果想直接使用读写分离,想集成 spring session 等这些高级特性,也可以采用 redisson。
Redis client 在使用中,需要根据业务及运维的需要,进行相关改进。在 client 访问异常时,可以增加重试策略,在访问某个 slave 异常时,需要重试其他 slave 节点。需要增加对 Redis 主从切换、slave 扩展的支持,比如采用守护线程定期扫描 master、slave 域名,发现 IP 变更,及时切换连接。对于多个 slave 的访问还需要增加负载均衡策略。最后Redis client 还可以与配置中心、Redis 集群管理平台整合,从而实时感知及协调 Redis 服务的访问。
至此,本节课的内容就讲完了。
在这几节课中,你首先学习了 Redis 的特性及基本原理,初步了解了 Redis 的数据类型、主进程/子进程、BIO 线程、持久化、复制、集群等;这些内容会在后续逐一深入学习。
然后,详细学习了 Redis 的数据类型了解了字符串、列表、集合、有序集合、哈希、位图、GEO 地理位置、HyperLogLog 基数统计,这 8 种核心数据类型的功能、特点、主要操作指令及应用场景。
接下来,你还熟悉了 Redis 协议,包括 Redis 协议的设计原则、三种响应模型2 种请求格式和 5 种响应格式。
最后,以 Java 语言为例,你还了解了 Redis client 的对比、选择及改进。
你可以参考这个思维导图,对这些知识点进行回顾和梳理。

View File

@ -0,0 +1,53 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
19 Redis系统架构中各个处理模块是干什么的
你好,我是你的缓存课老师陈波,欢迎进入第 19 课时“Redis 系统架构”的学习。
Redis 系统架构
通过前面的学习,相信你已经掌握了 Redis 的原理、数据类型及访问协议等内容。本课时,我将进一步分析 Redis 的系统架构,重点讲解 Redis 系统架构的事件处理机制、数据管理、功能扩展、系统扩展等内容。
事件处理机制
Redis 组件的系统架构如图所示,主要包括事件处理、数据存储及管理、用于系统扩展的主从复制/集群管理,以及为插件化功能扩展的 Module System 模块。
Redis 中的事件处理模块,采用的是作者自己开发的 ae 事件驱动模型,可以进行高效的网络 IO 读写、命令执行,以及时间事件处理。
其中,网络 IO 读写处理采用的是 IO 多路复用技术,通过对 evport、epoll、kqueue、select 等进行封装,同时监听多个 socket并根据 socket 目前执行的任务,来为 socket 关联不同的事件处理器。
当监听端口对应的 socket 收到连接请求后,就会创建一个 client 结构,通过 client 结构来对连接状态进行管理。在请求进入时,将请求命令读取缓冲并进行解析,并存入到 client 的参数列表。
然后根据请求命令找到 对应的redisCommand 最后根据命令协议对请求参数进一步的解析、校验并执行。Redis 中时间事件比较简单,目前主要是执行 serverCron来做一些统计更新、过期 key 清理、AOF 及 RDB 持久化等辅助操作。
数据管理
Redis 的内存数据都存在 redisDB 中。Redis 支持多 DB每个 DB 都对应一个 redisDB 结构。Redis 的 8 种数据类型,每种数据类型都采用一种或多种内部数据结构进行存储。同时这些内部数据结构及数据相关的辅助信息,都以 kye/value 的格式存在 redisDB 中的各个 dict 字典中。
数据在写入 redisDB 后,这些执行的写指令还会及时追加到 AOF 中追加的方式是先实时写入AOF 缓冲,然后按策略刷缓冲数据到文件。由于 AOF 记录每个写操作,所以一个 key 的大量中间状态也会呈现在 AOF 中,导致 AOF 冗余信息过多,因此 Redis 还设计了一个 RDB 快照操作,可以通过定期将内存里所有的数据快照落地到 RDB 文件,来以最简洁的方式记录 Redis 的所有内存数据。
Redis 进行数据读写的核心处理线程是单线程模型为了保持整个系统的高性能必须避免任何kennel 导致阻塞的操作。为此Redis 增加了 BIO 线程,来处理容易导致阻塞的文件 close、fsync 等操作,确保系统处理的性能和稳定性。
在 server 端存储内存永远是昂贵且短缺的Redis 中,过期的 key 需要及时清理,不活跃的 key 在内存不足时也可能需要进行淘汰。为此Redis 设计了 8 种淘汰策略,借助新引入的 eviction pool进行高效的 key 淘汰和内存回收。
功能扩展
Redis 在 4.0 版本之后引入了 Module System 模块,可以方便使用者,在不修改核心功能的同时,进行插件化功能开发。使用者可以将新的 feature 封装成动态链接库Redis 可以在启动时加载,也可以在运行过程中随时按需加载和启用。
在扩展模块中,开发者可以通过 RedisModule_init 初始化新模块,用 RedisModule_CreateCommand 扩展各种新模块指令,以可插拔的方式为 Redis 引入新的数据结构和访问命令。
系统扩展
Redis作者在架构设计中对系统的扩展也倾注了大量关注。在主从复制功能中psyn 在不断的优化,不仅在 slave 闪断重连后可以进行增量复制,而且在 slave 通过主从切换成为 master 后,其他 slave 仍然可以与新晋升的 master 进行增量复制,另外,其他一些场景,如 slave 重启后,也可以进行增量复制,大大提升了主从复制的可用性。使用者可以更方便的使用主从复制,进行业务数据的读写分离,大幅提升 Redis 系统的稳定读写能力。
通过主从复制可以较好的解决 Redis 的单机读写问题,但所有写操作都集中在 master 服务器,很容易达到 Redis 的写上限,同时 Redis 的主从节点都保存了业务的所有数据,随着业务发展,很容易出现内存不够用的问题。
为此Redis 分区无法避免。虽然业界大多采用在 client 和 proxy 端分区,但 Redis 自己也早早推出了 cluster 功能并不断进行优化。Redis cluster 预先设定了 16384 个 slot 槽,在 Redis 集群启动时,通过手动或自动将这些 slot 分配到不同服务节点上。在进行 key 读写定位时,首先对 key 做 hash并将 hash 值对 16383 ,做 按位与运算,确认 slot然后确认服务节点最后再对 对应的 Redis 节点,进行常规读写。如果 client 发送到错误的 Redis 分片Redis 会发送重定向回复。如果业务数据大量增加Redis 集群可以通过数据迁移,来进行在线扩容。

View File

@ -0,0 +1,116 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
20 Redis如何处理文件事件和时间事件
上一课时,我们学习了 Redis 的系统架构,接下来的几个课时我将带你一起对这些模块和设计进行详细分析。首先,我将分析 Redis 的事件驱动模型。
Redis 事件驱动模型
事件驱动模型
Redis 是一个事件驱动程序,但和 Memcached 不同的是Redis 并没有采用 libevent 或 libev 这些开源库而是直接开发了一个新的事件循环组件。Redis 作者给出的理由是尽量减少外部依赖而自己开发的事件模型也足够简洁、轻便、高效也更易控制。Redis 的事件驱动模型机制封装在 aeEventLoop 等相关的结构体中,网络连接、命令读取执行回复,数据的持久化、淘汰回收 key 等,几乎所有的核心操作都通过 ae 事件模型进行处理。
Redis 的事件驱动模型处理 2 类事件:
文件事件,如连接建立、接受请求命令、发送响应等;
时间事件,如 Redis 中定期要执行的统计、key 淘汰、缓冲数据写出、rehash等。
文件事件处理
Redis 的文件事件采用典型的 Reactor 模式进行处理。Redis 文件事件处理机制分为 4 部分:
连接 socket
IO 多路复用程序
文件事件分派器
事件处理器
文件事件是对连接 socket 操作的一个抽象。当端口监听 socket 准备 accept 新连接,或者连接 socket 准备好读取请求、写入响应、关闭时就会产生一个文件事件。IO 多路复用程序负责同时监听多个 socket当这些 socket 产生文件事件时,就会触发事件通知,文件分派器就会感知并获取到这些事件。
虽然多个文件事件可能会并发出现,但 IO 多路复用程序总会将所有产生事件的 socket 放入一个队列中,通过这个队列,有序的把这些文件事件通知给文件分派器。
IO多路复用
Redis 封装了 4 种多路复用程序,每种封装实现都提供了相同的 API 实现。编译时,会按照性能和系统平台,选择最佳的 IO 多路复用函数作为底层实现,选择顺序是,首先尝试选择 Solaries 中的 evport如果没有就尝试选择 Linux 中的 epoll否则就选择大多 UNIX 系统都支持的 kqueue这 3 个多路复用函数都直接使用系统内核内部的结构,可以服务数十万的文件描述符。
如果当前编译环境没有上述函数,就会选择 select 作为底层实现方案。select 方案的性能较差,事件发生时,会扫描全部监听的描述符,事件复杂度是 O(n)并且只能同时服务有限个文件描述符32 位机默认是 1024 个64 位机默认是 2048 个,所以一般情况下,并不会选择 select 作为线上运行方案。Redis 的这 4 种实现,分别在 ae_evport、ae_epoll、ae_kqueue 和 ae_select 这 4 个代码文件中。
文件事件收集及派发器
Redis 中的文件事件分派器是 aeProcessEvents 函数。它会首先计算最大可以等待的时间,然后利用 aeApiPoll 等待文件事件的发生。如果在等待时间内,一旦 IO 多路复用程序产生了事件通知,则会立即轮询所有已产生的文件事件,并将文件事件放入 aeEventLoop 中的 aeFiredEvents 结构数组中。每个 fired event 会记录 socket 及 Redis 读写事件类型。
这里会涉及将多路复用中的事件类型,转换为 Redis 的 ae 事件驱动模型中的事件类型。以采用 Linux 中的 epoll 为例,会将 epoll 中的 EPOLLIN 转为 AE_READABLE 类型,将 epoll 中的 EPOLLOUT、EPOLLERR 和 EPOLLHUP 转为 AE_WRITABLE 事件。
aeProcessEvents 在获取到触发的事件后,会根据事件类型,将文件事件 dispatch 派发给对应事件处理函数。如果同一个 socket同时有读事件和写事件Redis 派发器会首先派发处理读事件,然后再派发处理写事件。
文件事件处理函数分类
Redis 中文件事件函数的注册和处理主要分为 3 种。
连接处理函数 acceptTcpHandler
Redis 在启动时,在 initServer 中对监听的 socket 注册读事件,事件处理器为 acceptTcpHandler该函数在有新连接进入时会被派发器派发读任务。在处理该读任务时会 accept 新连接,获取调用方的 IP 及端口,并对新连接创建一个 client 结构。如果同时有大量连接同时进入Redis 一次最多处理 1000 个连接请求。
readQueryFromClient 请求处理函数
连接函数在创建 client 时,会对新连接 socket 注册一个读事件,该读事件的事件处理器就是 readQueryFromClient。在连接 socket 有请求命令到达时IO 多路复用程序会获取并触发文件事件然后这个读事件被派发器派发给本请求的处理函数。readQueryFromClient 会从连接 socket 读取数据,存入 client 的 query 缓冲,然后进行解析命令,按照 Redis 当前支持的 2 种请求格式,及 inline 内联格式和 multibulk 字符块数组格式进行尝试解析。解析完毕后client 会根据请求命令从命令表中获取到对应的 redisCommand如果对应 cmd 存在。则开始校验请求的参数,以及当前 server 的内存、磁盘及其他状态,完成校验后,然后真正开始执行 redisCommand 的处理函数,进行具体命令的执行,最后将执行结果作为响应写入 client 的写缓冲中。
命令回复处理器 sendReplyToClient
当 redis需要发送响应给client时Redis 事件循环中会对client的连接socket注册写事件这个写事件的处理函数就是sendReplyToClient。通过注册写事件将 client 的socket与 AE_WRITABLE 进行间接关联。当 Client fd 可进行写操作时,就会触发写事件,该函数就会将写缓冲中的数据发送给调用方。
Redis 中的时间事件是指需要在特定时间执行的事件。多个 Redis 中的时间事件构成 aeEventLoop 中的一个链表,供 Redis 在 ae 事件循环中轮询执行。
Redis 当前的主要时间事件处理函数有 2 个:
serverCron
moduleTimerHandler
Redis 中的时间事件分为 2 类:
单次时间,即执行完毕后,该时间事件就结束了。
周期性事件,在事件执行完毕后,会继续设置下一次执行的事件,从而在时间到达后继续执行,并不断重复。
时间事件主要有 5 个属性组成。
事件 IDRedis 为时间事件创建全局唯一 ID该 ID 按从小到大的顺序进行递增。
执行时间 when_sec 和 when_ms精确到毫秒记录该事件的到达可执行时间。
时间事件处理器 timeProc在时间事件到达时Redis 会调用相应的 timeProc 处理事件。
关联数据 clientData在调用 timeProc 时,需要使用该关联数据作为参数。
链表指针 prev 和 next它用来将时间事件维护为双向链表便于插入及查找所要执行的时间事件。
时间事件的处理是在事件循环中的 aeProcessEvents 中进行。执行过程是:
首先遍历所有的时间事件。
比较事件的时间和当前时间,找出可执行的时间事件。
然后执行时间事件的 timeProc 函数。
执行完毕后,对于周期性时间,设置时间新的执行时间;对于单次性时间,设置事件的 ID为 -1后续在事件循环中下一次执行 aeProcessEvents 的时候从链表中删除。

View File

@ -0,0 +1,31 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
21 Redis读取请求数据后如何进行协议解析和处理
你好,我是你的缓存课老师陈波,欢迎进入第 21 课时“Redis 协议解析及处理”的学习。上一课时,我们学习了 Redis 事件驱动模型,接下来,看一下 Redis 是如何进行协议解析及处理的。
Redis 协议解析及处理
协议解析
上一课时讲到,请求命令进入,触发 IO 读事件后。client 会从连接文件描述符读取请求,并存入 client 的 query buffer 中。client 的读缓冲默认是 16KB读取命令时如果发现请求超过 1GB则直接报异常关闭连接。
client 读取完请求命令后,则根据 query buff 进行协议解析。协议解析时,首先查看协议的首字符。如果是 *,则解析为字符块数组类型,即 MULTIBULK。否则请求解析为 INLINE 类型。
INLINE 类型是以 CRLF 结尾的单行字符串,协议命令及参数以空格分隔。解析过程参考之前课程里分析的对应协议格式。协议解析完毕后,将请求参数个数存入 client 的 argc 中,将请求的具体参数存入 client 的 argv 中。
协议执行
请求命令解析完毕,则进入到协议执行部分。协议执行中,对于 quit 指令,直接返回 OK设置 flag 为回复后关闭连接。
对于非 quit 指令,以 client 中 argv[0] 作为命令,从 server 中的命令表中找到对应的 redisCommand。如果没有找到 redisCommand则返回未知 cmd 异常。如果找到 cmd则开始执行 redisCommand 中的 proc 函数,进行具体命令的执行。在命令执行完毕后,将响应写入 client 的写缓冲。并按配置和部署,将写指令分发给 aof 和 slaves。同时更新相关的统计数值。

View File

@ -0,0 +1,37 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
25 Redis是如何处理容易超时的系统调用的
本课时我们主要学习通过 BIO 线程解决处理容易超时的系统调用问题,以及 BIO 线程处理的任务与处理流程等内容。
BIO 线程简介
Redis 在运行过程中,不可避免的会产生一些运行慢的、容易引发阻塞的任务,如将内核中的文件缓冲同步到磁盘中、关闭文件,都会引发短时阻塞,还有一些大 key如一些元素数高达万级或更多的聚合类元素在删除时由于所有元素需要逐一释放回收整个过程耗时也会比较长。而 Redis 的核心处理线程是单进程单线程模型所有命令的接受与处理、数据淘汰等都在主线程中进行这些任务处理速度非常快。如果核心单线程还要处理那些慢任务在处理期间势必会阻塞用户的正常请求导致服务卡顿。为此Redis 引入了 BIO 后台线程,专门处理那些慢任务,从而保证和提升主线程的处理能力。
Redis 的 BIO 线程采用生产者-消费者模型。主线程是生产者生产各种慢任务然后存放到任务队列中。BIO 线程是消费者,从队列获取任务并进行处理。如果生产者生产任务过快,队列可用于缓冲这些任务,避免负荷过载或数据丢失。如果消费者处理速度很快,处理完毕后就可以安静的等待,不增加额外的性能开销。再次,有新任务时,主线程通过条件变量来通知 BIO 线程,这样 BIO 线程就可以再次执行任务。
BIO 处理任务
Redis 启动时,会创建三个任务队列,并对应构建 3 个 BIO 线程,三个 BIO 线程与 3 个任务队列之间一一对应。BIO 线程分别处理如下 3 种任务。
close 关闭文件任务。rewriteaof 完成后,主线程需要关闭旧的 AOF 文件,就向 close 队列插入一个旧 AOF 文件的关闭任务。由 close 线程来处理。
fysnc 任务。Redis 将 AOF 数据缓冲写入文件内核缓冲后,需要定期将系统内核缓冲数据写入磁盘,此时可以向 fsync 队列写入一个同步文件缓冲的任务,由 fsync 线程来处理。
lazyfree 任务。Redis 在需要淘汰元素数大于 64 的聚合类数据类型时,如列表、集合、哈希等,就往延迟清理队列中写入待回收的对象,由 lazyfree 线程后续进行异步回收。
BIO 处理流程
BIO 线程的整个处理流程如图所示。当主线程有慢任务需要异步处理时。就会向对应的任务队列提交任务。提交任务时,首先申请内存空间,构建 BIO 任务。然后对队列锁进行加锁,在队列尾部追加新的 BIO 任务,最后尝试唤醒正在等待任务的 BIO 线程。
BIO 线程启动时或持续处理完所有任务,发现任务队列为空后,就会阻塞,并等待新任务的到来。当主线程有新任务后,主线程会提交任务,并唤醒 BIO 线程。BIO 线程随后开始轮询获取新任务,并进行处理。当处理完所有 BIO 任务后,则再次进入阻塞,等待下一轮唤醒。

View File

@ -0,0 +1,43 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
26 如何大幅成倍提升Redis处理性能
本课时我们主要学习如何通过 Redis 多线程来大幅提升性能,涉及主线程与 IO 线程、命令处理流程,以及多线程方案的优劣等内容。
主线程
Redis 自问世以来,广受好评,应用广泛。但相比, Memcached 单实例压测 TPS 可以高达百万,线上可以稳定跑 20~40 万而言Redis 的单实例压测 TPS 不过 10~12 万,线上一般最高也就 2~4 万,仍相差一个数量级。
Redis 慢的主要原因是单进程单线程模型。虽然一些重量级操作也进行了分拆,如 RDB 的构建在子进程中进行,文件关闭、文件缓冲同步,以及大 key 清理都放在 BIO 线程异步处理,但还远远不够。线上 Redis 处理用户请求时,十万级的 client 挂在一个 Redis 实例上,所有的事件处理、读请求、命令解析、命令执行,以及最后的响应回复,都由主线程完成,纵然是 Redis 各种极端优化,巧妇难为无米之炊,一个线程的处理能力始终是有上限的。当前服务器 CPU 大多是 16 核到 32 核以上Redis 日常运行主要只使用 1 个核心,其他 CPU 核就没有被很好的利用起来Redis 的处理性能也就无法有效地提升。而 Memcached 则可以按照服务器的 CPU 核心数,配置数十个线程,这些线程并发进行 IO 读写、任务处理,处理性能可以提高一个数量级以上。
IO 线程
面对性能提升困境,虽然 Redis 作者不以为然,认为可以通过多部署几个 Redis 实例来达到类似多线程的效果。但多实例部署则带来了运维复杂的问题,而且单机多实例部署,会相互影响,进一步增大运维的复杂度。为此,社区一直有种声音,希望 Redis 能开发多线程版本。
因此Redis 即将在 6.0 版本引入多线程模型,当前代码在 unstable 版本中6.0 版本预计在明年发版。Redis 的多线程模型,分为主线程和 IO 线程。
因为处理命令请求的几个耗时点,分别是请求读取、协议解析、协议执行,以及响应回复等。所以 Redis 引入 IO 多线程并发地进行请求命令的读取、解析以及响应的回复。而其他的所有任务如事件触发、命令执行、IO 任务分发,以及其他各种核心操作,仍然在主线程中进行,也就说这些任务仍然由单线程处理。这样可以在最大程度不改变原处理流程的情况下,引入多线程。
命令处理流程
Redis 6.0 的多线程处理流程如图所示。主线程负责监听端口,注册连接读事件。当有新连接进入时,主线程 accept 新连接,创建 client并为新连接注册请求读事件。
当请求命令进入时,在主线程触发读事件,主线程此时并不进行网络 IO 的读取,而将该连接所在的 client 加入待读取队列中。Redis 的 Ae 事件模型在循环中,发现待读取队列不为空,则将所有待读取请求的 client 依次分派给 IO 线程,并自旋检查等待,等待 IO 线程读取所有的网络数据。所谓自旋检查等待,也就是指主线程持续死循环,并在循环中检查 IO 线程是否读完,不做其他任何任务。只有发现 IO 线程读完所有网络数据,才停止循环,继续后续的任务处理。
一般可以配置多个 IO 线程,比如配置 4~8 个,这些 IO 线程发现待读取队列中有任务时,则开始并发处理。每个 IO 线程从对应列表获取一个任务,从里面的 client 连接中读取请求数据,并进行命令解析。当 IO 线程完成所有的请求读取,并完成解析后,待读取任务数变为 0。主线程就停止循环检测开始依次执行 IO 线程已经解析的所有命令,每执行完毕一个命令,就将响应写入 client 写缓冲,这些 client 就变为待回复 client这些待回复 client 被加入待回复列表。然后主线程将这些待回复 client轮询分配给多个 IO 线程。然后再次自旋检测等待。
然后 IO 线程再次开始并发执行,将不同 client 的响应缓冲写给 client。当所有响应全部处理完后待回复的任务数变为 0主线程结束自旋检测继续处理后续的任务以及新的读请求。
Redis 6.0 版本中新引入的多线程模型,主要是指可配置多个 IO 线程,这些线程专门负责请求读取、解析,以及响应的回复。通过 IO 多线程Redis 的性能可以提升 1 倍以上。
多线程方案优劣
虽然多线程方案能提升1倍以上的性能但整个方案仍然比较粗糙。首先所有命令的执行仍然在主线程中进行存在性能瓶颈。然后所有的事件触发也是在主线程中进行也依然无法有效使用多核心。而且IO 读写为批处理读写,即所有 IO 线程先一起读完所有请求,待主线程解析处理完毕后,所有 IO 线程再一起回复所有响应,不同请求需要相互等待,效率不高。最后在 IO 批处理读写时,主线程自旋检测等待,效率更是低下,即便任务很少,也很容易把 CPU 打满。整个多线程方案比较粗糙,所以性能提升也很有限,也就 1~2 倍多一点而已。要想更大幅提升处理性能,命令的执行、事件的触发等都需要分拆到不同线程中进行,而且多线程处理模型也需要优化,各个线程自行进行 IO 读写和执行,互不干扰、等待与竞争,才能真正高效地利用服务器多核心,达到性能数量级的提升。

View File

@ -0,0 +1,53 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
27 Redis是如何进行主从复制的
本课时我们主要学习 Redis 复制原理,以及复制分析等内容。
Redis 复制原理
为了避免单点故障,数据存储需要进行多副本构建。同时由于 Redis 的核心操作是单线程模型的,单个 Redis 实例能处理的请求 TPS 有限。因此 Redis 自面世起,基本就提供了复制功能,而且对复制策略不断进行优化。
通过数据复制Redis 的一个 master 可以挂载多个 slave而 slave 下还可以挂载多个 slave形成多层嵌套结构。所有写操作都在 master 实例中进行master 执行完毕后,将写指令分发给挂在自己下面的 slave 节点。slave 节点下如果有嵌套的 slave会将收到的写指令进一步分发给挂在自己下面的 slave。通过多个 slaveRedis 的节点数据就可以实现多副本保存,任何一个节点异常都不会导致数据丢失,同时多 slave 可以 N 倍提升读性能。master 只写不读,这样整个 master-slave 组合,读写能力都可以得到大幅提升。
master 在分发写请求时,同时会将写指令复制一份存入复制积压缓冲,这样当 slave 短时间断开重连时,只要 slave 的复制位置点仍然在复制积压缓冲,则可以从之前的复制位置点之后继续进行复制,提升复制效率。
主库 master 和从库 slave 之间通过复制 id 进行匹配,避免 slave 挂到错误的 master。Redis 的复制分为全量同步和增量同步。Redis 在进行全量同步时master 会将内存数据通过 bgsave 落地到 rdb同时将构建 内存快照期间 的写指令,存放到复制缓冲中,当 rdb 快照构建完毕后master 将 rdb 和复制缓冲队列中的数据全部发送给 slaveslave 完全重新创建一份数据。这个过程,对 master 的性能损耗较大slave 构建数据的时间也比较长,而且传递 rdb 时还会占用大量带宽对整个系统的性能和资源的访问影响都比较大。而增量复制master 只发送 slave 上次复制位置之后的写指令,不用构建 rdb而且传输内容非常有限对 master、slave 的负荷影响很小,对带宽的影响可以忽略,整个系统受影响非常小。
在 Redis 2.8 之前Redis 基本只支持全量复制。在 slave 与 master 断开连接,或 slave 重启后,都需要进行全量复制。在 2.8 版本之后Redis 引入 psync增加了一个复制积压缓冲在将写指令同步给 slave 时,会同时在复制积压缓冲中也写一份。在 slave 短时断开重连后上报master runid 及复制偏移量。如果 runid 与 master 一致,且偏移量仍然在 master 的复制缓冲积压中,则 master 进行增量同步。
但如果 slave 重启后master runid 会丢失,或者切换 master 后runid 会变化,仍然需要全量同步。因此 Redis 自 4.0 强化了 psync引入了 psync2。在 pysnc2 中,主从复制不再使用 runid而使用 replid即复制id 来作为复制判断依据。同时 Redis 实例在构建 rdb 时,会将 replid 作为 aux 辅助信息存入 rbd。重启时加载 rdb 时即可得到 master 的复制 id。从而在 slave 重启后仍然可以增量同步。
在 psync2 中Redis 每个实例除了会有一个复制 id 即 replid 外,还有一个 replid2。Redis 启动后,会创建一个长度为 40 的随机字符串,作为 replid 的初值,在建立主从连接后,会用 master的 replid 替换自己的 replid。同时会用 replid2 存储上次 master 主库的 replid。这样切主时即便 slave 汇报的复制 id 与新 master 的 replid 不同,但和新 master 的 replid2 相同,同时复制偏移仍然在复制积压缓冲区内,仍然可以实现增量复制。
Redis 复制分析
在设置 master、slave 时,首先通过配置或者命令 slaveof no one 将节点设置为主库。然后其他各个从库节点,通过 slaveof \(master_ip \)master_port将其他从库挂在到 master 上。同样方法,还可以将 slave 节点挂载到已有的 slave 节点上。在准备开始数据复制时slave 首先会主动与 master 创建连接,并上报信息。具体流程如下。
slave 创建与 master 的连接后,首先发送 ping 指令,如果 master 没有返回异常,而是返回 pong则说明 master 可用。如果 Redis 设置了密码slave 会发送 auth $masterauth 指令,进行鉴权。当鉴权完毕,从库就通过 replconf 发送自己的端口及 IP 给 master。接下来slave 继续通过 replconf 发送 capa eof capa psync2 进行复制版本校验。如果 master 校验成功。从库接下来就通过 psync 将自己的复制 id、复制偏移发送给 master正式开始准备数据同步。
主库接收到从库发来的 psync 指令后则开始判断可以进行数据同步的方式。前面讲到Redis 当前保存了复制 idreplid 和 replid2。如果从库发来的复制 id与 master 的复制 id即 replid 和 replid2相同并且复制偏移在复制缓冲积压中则可以进行增量同步。master 发送 continue 响应,并返回 master 的 replid。slave 将 master 的 replid 替换为自己的 replid并将之前的复制 id 设置为 replid2。之后master 则可继续发送,复制偏移位置 之后的指令,给 slave完成数据同步。
如果主库发现从库传来的复制 id 和自己的 replid、replid2 都不同或者复制偏移不在复制积压缓冲中则判定需要进行全量复制。master 发送 fullresync 响应,附带 replid 及复制偏移。然后, master 根据需要构建 rdb并将 rdb 及复制缓冲发送给 slave。
对于增量复制slave 接下来就等待接受 master 传来的复制缓冲及新增的写指令,进行数据同步。
而对于全量同步slave 会首先进行,嵌套复制的清理工作,比如 slave 当前还有嵌套的 子slave则该 slave 会关闭嵌套 子slave 的所有连接并清理自己的复制积压缓冲。然后slave 会构建临时 rdb 文件,并从 master 连接中读取 rdb 的实际数据,写入 rdb 中。在写 rdb 文件时,每写 8M就会做一个 fsync操作 刷新文件缓冲。当接受 rdb 完毕则将 rdb 临时文件改名为 rdb 的真正名字。
接下来slave 会首先清空老数据,即删除本地所有 DB 中的数据,并暂时停止从 master 继续接受数据。然后slave 就开始全力加载 rdb 恢复数据,将数据从 rdb 加载到内存。在 rdb 加载完毕后slave 重新利用与 master 的连接 socket创建与 master 连接的 client并在此注册读事件可以开始接受 master 的写指令了。此时slave 还会将 master 的 replid 和复制偏移设为自己的复制 id 和复制偏移 offset并将自己的 replid2 清空因为slave 的所有嵌套 子slave 接下来也需要进行全量复制。最后slave 就会打开 aof 文件,在接受 master 的写指令后,执行完毕并写入到自己的 aof 中。
相比之前的 syncpsync2 优化很明显。在短时间断开连接、slave 重启、切主等多种场景只要延迟不太久复制偏移仍然在复制积压缓冲均可进行增量同步。master 不用构建并发送巨大的 rdb可以大大减轻 master 的负荷和网络带宽的开销。同时slave 可以通过轻量的增量复制,实现数据同步,快速恢复服务,减少系统抖动。
但是psync 依然严重依赖于复制缓冲积压,太大会占用过多内存,太小会导致频繁的全量复制。而且,由于内存限制,即便设置相对较大的复制缓冲区,在 slave 断开连接较久时,仍然很容易被复制缓冲积压冲刷,从而导致全量复制。

View File

@ -0,0 +1,135 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
28 如何构建一个高性能、易扩展的Redis集群
通过上一课时的学习,我们知道复制功能可以 N 倍提升 Redis 节点的读性能,而集群则可以通过分布式方案来 N 倍提升 Redis 的写性能。除了提升性能之外Redis 集群还可以提供更大的容量,提升资源系统的可用性。
Redis 集群的分布式方案主要有 3 种。分别是 Client 端分区方案Proxy 分区方案,以及原生的 Redis Cluster 分区方案。
Client 端分区
Client 端分区方案就是由 Client 决定数据被存储到哪个 Redis 分片,或者由哪个 Redis 分片来获取数据。它的核心思想是通过哈希算法将不同的 key 映射到固定的 Redis 分片节点上。对于单个 key 请求Client 直接对 key 进行哈希后,确定 Redis 分片,然后进行请求。而对于一个请求附带多个 key 的场景Client 会首先将这些 key 按哈希分片进行分类,从而将一个请求分拆为多个请求,然后再分别请求不同的哈希分片节点。
Client 通过哈希算法将数据进行分布,一般采用的哈希算法是取模哈希、一致性哈希和区间分布哈希。前两种哈希算法之前的课程已有详细分析,此处不在赘述。对于区间分布哈希,实际是一种取模哈希的变种,取模哈希是哈希并取模计算后,按哈希值来分配存储节点,而区间哈希是在哈希计算后,将哈希划分为多个区间,然后将这些区间分配给存储节点。如哈希后分 1024 个哈希点,然后将 0~511 作为分片 1将 512~1023 作为分片 2。
对于 Client 端分区,由于 Redis 集群有多个 master 分片,同时每个 master 下挂载多个 slave每个 Redis 节点都有独立的 IP 和端口。如果 master 异常需要切换 master或读压力过大需要扩展新的 slave这些都会涉及集群存储节点的变更需要 Client 端做连接切换。
为了避免 Client 频繁变更 IP 列表,可以采用 DNS 的方式来管理集群的主从。对 Redis 集群的每个分片的主和从均采用不同 DNS 域名。Client 通过域名解析的方式获取域名下的所有 IP然后来访问集群节点。由于每个分片 master 下有多个 slaveClient 需要在多个 slave 之间做负载均衡。可以按照权重建立与 slave 之间的连接,然后访问时,轮询使用这些连接依次访问,即可实现按权重访问 slave 节点。
在 DNS 访问模式下Client 需要异步定时探测主从域名,如果发现 IP 变更,及时与新节点建立连接,并关闭老连接。这样在主库故障需要切换时,或者从库需要增加减少时,任何分片的主从变化,只需运维或管理进程改一下 DNS 下的 IP 列表,业务 Client 端不需要做任何配置变更,即可正常切换访问。
Client 端分区方案的优点在于分区逻辑简单配置简单Client 节点之间和 Redis 节点之间均无需协调,灵活性强。而且 Client 直接访问对应 Redis 节点,没有额外环节,性能高效。但该方案扩展不便。在 Redis 端,只能成倍扩展,或者预先分配足够多的分片。在 Client 端,每次分片后,业务端需要修改分发逻辑,并进行重启。
Proxy 端分区
Proxy 端分区方案是指 Client 发送请求给 Proxy 请求代理组件Proxy 解析 Client 请求,并将请求分发到正确的 Redis 节点,然后等待 Redis 响应,最后再将结果返回给 Client 端。
如果一个请求包含多个 keyProxy 需要将请求的多个 key按分片逻辑分拆为多个请求然后分别请求不同的 Redis 分片接下来等待Redis响应在所有的分拆响应到达后再进行聚合组装最后返回给 Client。在整个处理过程中Proxy 代理首先要负责接受请求并解析,然后还要对 key 进行哈希计算及请求路由,最后还要将结果进行读取、解析及组装。如果系统运行中,主从变更或发生扩缩容,也只需由 Proxy 变更完成,业务 Client 端基本不受影响。
常见的 Proxy 端分区方案有 2 种,第一种是基于 Twemproxy 的简单分区方案第二种是基于Codis 的可平滑数据迁移的分区方案。
Twemproxy 是 Twitter 开源的一个组件,支持 Redis 和 Memcached 协议访问的代理组件。在讲分布式 Memecached 实战时我曾经详细介绍了它的原理和实现架构此处不再赘述。总体而言Twemproxy 实现简单、稳定性高,在一些访问量不大且很少发生扩缩的业务场景中,可以很好的满足需要。但由于 Twemproxy 是单进程单线程模型的,对包含多个 key 的 mutli 请求,由于需要分拆请求,然后再等待聚合,处理性能较低。而且,在后端 Redis 资源扩缩容,即增加或减少分片时,需要修改配置并重启,无法做到平滑扩缩。而且 Twemproxy 方案默认只有一个代理组件,无管理后端,各种运维变更不够便利。
而 Codis 是一个较为成熟的分布式 Redis 解决方案。对于业务 Client 访问,连接 Codis-proxy 和连接单个 Redis 几乎没有区别。Codis 底层除了会自动解析分发请求之外,还可以在线进行数据迁移,使用非常方便。
Codis 系统主要由 Codis-server、Codis-proxy、Codis-dashboard、Zookeeper 等组成。
Codis-server 是 Codis 的存储组件,它是基于 Redis 的扩展,增加了 slot 支持和数据迁移功能,所有数据存储在预分配的 1024 个 slot 中,可以按 slot 进行同步或异步数据迁移。
Codis-proxy 处理 Client 请求,解析业务请求,并路由给后端的 Codis-server group。Codis 的每个 server group 相当于一个 Redis 分片,由 1 个 master 和 N 个从库组成。
Zookeeper 用于存储元数据,如 Proxy 的节点,以及数据访问的路由表。除了 ZookeeperCodis 也支持 etcd 等其他组件,用于元数据的存储和通知。
Codis-dashboard 是 Codis 的管理后台可用于管理数据节点、Proxy 节点的加入或删除还可用于执行数据迁移等操作。Dashboard 的各项变更指令通过 Zookeeper 进行分发。
Codis 提供了功能较为丰富的管理后台,可以方便的对整个集群进行监控及运维。
Proxy 端分区方案的优势,是 Client 访问逻辑和 Redis 分布逻辑解耦,业务访问便捷简单。在资源发生变更或扩缩容时,只用修改数量有限的 Proxy 即可,数量庞大的业务 Client 端不用做调整。
但 Proxy 端分区的方案,访问时请求需要经过 Proxy 中转,访问多跳了一级,性能会存在损耗,一般损耗会达到 5~15% 左右。另外多了一个代理层,整个系统架构也会更复杂。
Redis Cluster 分区
Redis 社区版在 3.0 后开始引入 Cluster 策略,一般称之为 Redis-Cluster 方案。Redis-Cluster 按 slot 进行数据的读写和管理,一个 Redis-Cluster 集群包含 16384 个 slot。每个 Redis 分片负责其中一部分 slot。在集群启动时按需将所有 slot 分配到不同节点,在集群系统运行后,按 slot 分配策略,将 key 进行 hash 计算,并路由到对应节点 访问。
随着业务访问模型的变化Redis 部分节点可能会出现压力过大、访问不均衡的现象,此时可以将 slot 在 Redis 分片节点内部进行迁移以均衡访问。如果业务不断发展数据量过大、TPS过高还可以将 Redis 节点的部分 slot 迁移到新节点,增加 Redis-Cluster 的分片,对整个 Redis 资源进行扩容,已提升整个集群的容量及读写能力。
在启动 Redis 集群时,在接入数据读写前,可以通过 Redis 的 Cluster addslots 将 16384 个 slot 分配给不同的 Redis 分片节点,同时可以用 Cluster delslots 去掉某个节点的 slot用 Cluster flushslots 清空某个节点的所有 slot 信息,来完成 slot 的调整。
Redis Cluster 是一个去中心化架构,每个节点记录全部 slot 的拓扑分布。这样 Client 如果把 key 分发给了错误的 Redis 节点Redis 会检查请求 key 所属的 slot如果发现 key 属于其他节点的 slot会通知 Client 重定向到正确的 Redis 节点访问。
Redis Cluster 下的不同 Redis 分片节点通过 gossip 协议进行互联,使用 gossip 的优势在于该方案无中心控制节点这样更新不会受到中心节点的影响可以通过通知任意一个节点来进行管理通知。不足就是元数据的更新会有延时集群操作会在一定的时延后才会通知到所有Redis。由于 Redis Cluster 采用 gossip 协议进行服务节点通信,所以在进行扩缩容时,可以向集群内任何一个节点,发送 Cluster meet 指令将新节点加入集群然后集群节点会立即扩散新节点到整个集群。meet 新节点操作的扩散,只需要有一条节点链能到达集群各个节点即可,无需 meet 所有集群节点,操作起来比较便利。
在 Redis-Cluster 集群中key 的访问需要 smart client 配合。Client 首先发送请求给 Redis 节点Redis 在接受并解析命令后,会对 key 进行 hash 计算以确定 slot 槽位。计算公式是对 key 做 crc16 哈希,然后对 16383 进行按位与操作。如果 Redis 发现 key 对应的 slot 在本地,则直接执行后返回结果。
如果 Redis 发现 key 对应的 slot 不在本地,会返回 moved 异常响应,并附带 key 的 slot以及该 slot 对应的正确 Redis 节点的 host 和 port。Client 根据响应解析出正确的节点 IP 和端口,然后把请求重定向到正确的 Redis即可完成请求。为了加速访问Client 需要缓存 slot 与 Redis 节点的对应关系,这样可以直接访问正确的节点,以加速访问性能。
Redis-Cluster 提供了灵活的节点扩缩容方案,可以在不影响用户访问的情况下,动态为集群增加节点扩容,或下线节点为集群缩容。由于扩容在线上最为常见,我首先来分析一下 Redis-Cluster 如何进行扩容操作。
在准备对 Redis 扩容时,首先准备待添加的新节点,部署 Redis配置 cluster-enable 为 true并启动。然后运维人员通过client连接上一个集群内的 Redis 节点,通过 cluster meet 命令将新节点加入到集群,该节点随后会通知集群内的其他节点,有新节点加入。因为新加入的节点还没有设置任何 slot所以不接受任何读写操作。
然后,将通过 cluster setslot \(slot importing 指令,在新节点中,将目标 slot 设为 importing 导入状态。再将 slot 对应的源节点,通过 cluster setslot \)slot migrating 将源节点的 slot 设为 migrating 迁移导出状态。
接下来,就从源节点获取待迁移 slot 的 key通过 cluster getkeysinslot \(slot \)count 命令,从 slot 中获取 N 个待迁移的 key。然后通过 migrate 指令,将这些 key 依次逐个迁移或批量一次迁移到目标新节点。对于迁移单个 key使用指令 migrate \(host \)port \(key \)dbid timeout如果一次迁移多个 key在指令结尾加上 keys 选项,同时将多个 key 放在指令结尾即可。持续循环前面 2 个步骤,不断获取 slot 里的 key然后进行迁移最终将 slot 下的所有数据都迁移到目标新节点。最后通过 cluster setslot 指令将这个 slot 指派给新增节点。setslot 指令可以发给集群内的任意一个节点这个节点会将这个指派信息扩散到整个集群。至此slot 就迁移到了新节点。如果要迁移多个 slot可以继续前面的迁移步骤最终将所有需要迁移的 slot 数据搬到新节点。
这个新迁移 slot 的节点属于主库,对于线上应用,还需要增加从库,以增加读写能力及可用性,否则一旦主库崩溃,整个分片的数据就无法访问。在节点上增加从库,需要注意的是,不能使用非集群模式下的 slaveof 指令,而要使用 cluster replication才能完成集群分片节点下的 slave 添加。另外对于集群模式slave 只能挂在分片 master 上slave 节点自身不能再挂载 slave。
缩容流程与扩容流程类似,只是把部分节点的 slot 全部迁移走,然后把这些没有 slot 的节点进行下线处理。在下线老节点之前,需要注意,要用 cluster forget 通知集群集群节点要从节点信息列表中将目标节点移除同时会将该节点加入到禁止列表1 分钟之内不允许再加入集群。以防止在扩散下线节点时,又被误加入集群。
Redis 社区官方在源代码中也提供了 redis-trib.rb作为 Redis Cluster 的管理工具。该工具用 Ruby 开发所以在使用前需要安装相关的依赖环境。redis-trib 工具通过封装前面所述的 Redis 指令,从而支持创建集群、检查集群、添加删除节点、在线迁移 slot 等各种功能。
Redis Cluster 在 slot 迁移过程中获取key指令以及迁移指令逐一发送并执行不影响 Client 的正常访问。但在迁移单条或多条 key 时Redis 节点是在阻塞状态下进行的也就是说Redis 在迁移 key 时,一旦开始执行迁移指令,就会阻塞,直到迁移成功或确认失败后,才会停止该 key 的迁移从而继续处理其他请求。slot 内的 key 迁移是通过 migrate 指令进行的。
在源节点接收到 migrate \(host \)port \(key \)destination-db 的指令后,首先 slot 迁移的源节点会与迁移的目标节点建立 socket 连接,第一次迁移,或者迁移过程中,当前待迁移的 DB 与前一次迁移的 DB 不同,在迁移数据前,还需要发送 select $dbid 进行切换到正确的 DB。
然后,源节点会轮询所有待迁移的 key/value。获取 key 的过期时间,并将 value 进行序列化,序列化过程就是将 value 进行 dump转换为类 rdb 存储的二进制格式。这个二进制格式分 3 部分。第一部分是 value 对象的 type。第二部分是 value 实际的二进制数据;第三部分是当前 rdb 格式的版本,以及该 value 的 CRC64 校验码。至此,待迁移发送的数据准备完毕,源节点向目标节点,发送 restore-asking 指令将过期时间、key、value 的二进制数据发送给目标节点。然后同步等待目标节点的响应结果。
目标节点对应的client收到指令后如果有 select 指令,就首先切换到正确的 DB。接下来读取并处理 resotre-asking 指令,处理 restore-asking 指令时,首先对收到的数据进行解析校验,获取 key 的 ttl校验 rdb 版本及 value 数据 cc64 校验码,确认无误后,将数据存入 redisDb设置过期时间并返回响应。
源节点收到目标节点处理成功的响应后。对于非 copy 类型的 migrate会删除已迁移的 key。至此key 的迁移就完成了。migrate 迁移指令,可以一次迁移一个或多个 key。注意整个迁移过程中源节点在发送 restore-asking 指令后,同步阻塞,等待目标节点完成数据处理,直到超时或者目标节点返回响应结果,收到结果后在本地处理完毕后序事件,才会停止阻塞,才能继续处理其他事件。所以,单次迁移的 key 不能太多,否则阻塞时间会较长,导致 Redis 卡顿。同时,即便单次只迁移一个 key如果对应的 value 太大,也可能导致 Redis 短暂卡顿。
在 slot 迁移过程中,不仅其他非迁移 slot 的 key 可以正常访问,即便正在迁移的 slot它里面的 key 也可以正常读写,不影响业务访问。但由于 key 的迁移是阻塞模式,即在迁移 key 的过程中,源节点并不会处理任何请求,所以在 slot 迁移过程中,待读写的 key 只有三种存在状态。
尚未被迁移,后续会被迁走;
已经被迁移;
这个 key 之前并不存在集群中,是一个新 key。
slot 迁移过程中,对节点里的 key 处理方式如下。
对于尚未被迁移的 key即从 DB 中能找到该 key不管这个 key 所属的 slot 是否正在被迁移,都直接在本地进行读写处理。
对于无法从 DB 中找到 value 的 key但key所属slot正在被迁移包括已迁走或者本来不存在的 key 两种状态Redis 返回 ask 错误响应,并附带 slot 迁移目标节点的 host 和 port。Client 收到 ask 响应后,将请求重定向到 slot 迁移的新节点,完成响应处理。
对于无法从 DB 中找到 value 的 key且 key 所在的 slot 不属于本节点,说明 Client 发送节点有误,直接返回 moved 错误响应,也附带上 key 对应节点的 host 和 port由 Client 重定向请求。
对于 Redis Cluster 集群方案,由社区官方实现,并有 Redis-trib 集群工具,上线和使用起来比较便捷。同时它支持在线扩缩,可以随时通过工具查看集群的状态。但这种方案也存在不少弊端。首先,数据存储和集群逻辑耦合,代码逻辑复杂,容易出错。
其次Redis 节点要存储 slot 和 key 的映射关系,需要额外占用较多内存,特别是对 value size 比较小、而key相对较大的业务影响更是明显。
再次key 迁移过程是阻塞模式,迁移大 value 会导致服务卡顿。而且,迁移过程,先获取 key再迁移效率低。最后Cluster 模式下,集群复制的 slave 只能挂载到 master不支持 slave 嵌套,会导致 master 的压力过大,无法支持那些,需要特别多 slave、读 TPS 特别大的业务场景。

View File

@ -0,0 +1,67 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
33 如何为秒杀系统设计缓存体系?
本课时我们具体讲解如何为秒杀系统设计缓存体系。
秒杀系统分析
互联网电商为了吸引人气经常会对一些商品进行低价秒杀售卖活动。比如几年前小米的不定期新品发售又如当前每年定期举行双11、双12中的特价商品售卖。秒杀售卖时大量消费者蜂拥而至给电商带来了极大的人气也给电商背后的服务系统带来了超高的并发访问负荷。
在不同电商、不同的秒杀活动,秒杀系统售卖的商品、销售策略大不相同,但秒杀背后的秒杀系统却有很大的相似性,基本都有以下这些共同特点。
首先,秒杀业务简单,每个秒杀活动售卖的商品是事先定义好的,这些商品有明确的类型和数量,卖完即止。
其次,秒杀活动定时上架,而且会提供一个秒杀入口,消费者可以在活动开始后,通过这个入口进行抢购秒杀活动。
再次,秒杀活动由于商品售价低廉,广泛宣传,购买者远大于商品数,开始售卖后,会被快速抢购一空。
最后,由于秒杀活动的参与者众多,远超日常访客数量,大量消费者涌入秒杀系统,还不停的刷新访问,短时间内给系统带来超高的并发流量,直到活动结束,流量消失。
分析了秒杀系统的特点,很容易发现,秒杀系统实际就是一个有计划的低价售卖活动,活动期间会带来 N 倍爆发性增长的瞬时流量,活动后,流量会快速消失。因此,秒杀活动会给后端服务带来如下的技术挑战。
首先,秒杀活动持续时间短,但访问冲击量大,秒杀系统需要能够应对这种爆发性的类似攻击的访问模型。
其次,业务的请求量远远大于售卖量,大部分是最终无法购买成功的请求,秒杀系统需要提前规划好处理策略;
而且,由于业务前端访问量巨大,系统对后端数据的访问量也会短时间爆增,需要对数据存储资源进行良好设计。
另外,秒杀活动虽然持续时间短,但活动期间会给整个业务系统带来超大负荷,业务系统需要制定各种策略,避免系统过载而宕机。
最后,由于售卖活动商品价格低廉,存在套利空间,各种非法作弊手段层出,需要提前规划预防策略。
秒杀系统设计
在设计秒杀系统时,有两个设计原则。
首先,要尽力将请求拦截在系统上游,层层设阻拦截,过滤掉无效或超量的请求。因为访问量远远大于商品数量,所有的请求打到后端服务的最后一步,其实并没有必要,反而会严重拖慢真正能成交的请求,降低用户体验。
其次,要充分利用缓存,提升系统的性能和可用性。
秒杀系统专为秒杀活动服务,售卖商品确定,因此可以在设计秒杀商品页面时,将商品信息提前设计为静态信息,将静态的商品信息以及常规的 CSS、JS、宣传图片等静态资源一起独立存放到 CDN 节点,加速访问,且降低系统访问压力。
在访问前端也可以制定种种限制策略,比如活动没开始时,抢购按钮置灰,避免抢先访问,用户抢购一次后,也将按钮置灰,让用户排队等待,避免反复刷新。
用户所有的请求进入秒杀系统前,通过负载均衡策略均匀分发到不同 Web 服务器,避免节点过载。在 Web 服务器中,首先进行各种服务预处理,检查用户的访问权限,识别并发刷订单的行为。同时在真正服务前,也要进行服务前置检查,避免超售发生。如果发现售出数量已经达到秒杀数量,则直接返回结束。
秒杀系统在处理抢购业务逻辑时,除了对用户进行权限校验,还需要访问商品服务,对库存进行修改,访问订单服务进行订单创建,最后再进行支付、物流等后续服务。这些依赖服务,可以专门为秒杀业务设计排队策略,或者额外部署实例,对秒杀系统进行专门服务,避免影响其他常规业务系统。
在秒杀系统设计中,最重要的是在系统开发之初就进行有效分拆。首先分拆秒杀活动页面的内容,将静态内容分拆到 CDN动态内容才通过接口访问。其次要将秒杀业务系统和其他业务系统进行功能分拆尽量将秒杀系统及依赖服务独立分拆部署避免影响其他核心业务系统。
由于秒杀的参与者远大于商品数,为了提高抢购的概率,时常会出现一些利用脚本和僵尸账户并发频繁调用接口进行强刷的行为,秒杀系统需要构建访问记录缓存,记录访问 IP、用户的访问行为发现异常访问提前进行阻断及返回。同时还需要构建用户缓存并针对历史数据分析提前缓存僵尸强刷专业户方便在秒杀期间对其进行策略限制。这些访问记录、用户数据通过缓存进行存储可以加速访问另外对用户数据还进行缓存预热避免活动期间大量穿透。
在业务请求处理时,所有操作尽可能由缓存交互完成。由于秒杀商品较少,相关信息全部加载到内存,把缓存暂时当作存储用,并不会带来过大成本负担。
为秒杀商品构建商品信息缓存并对全部目标商品进行预热加载。同时对秒杀商品构建独立的库存缓存加速库存检测。这样通过秒杀商品列表缓存进行快速商品信息查询通过库存缓存可以快速确定秒杀活动进程方便高效成交或无可售商品后的快速检测及返回。在用户抢购到商品后要进行库存事务变更进行库存、订单、支付等相关的构建和修改这些操作可以尽量由系统只与缓存组件交互完成初步处理。后续落地等操作必须要入DB库的操作可以先利用消息队列机记录成交事件信息然后再逐步分批执行避免对 DB 造成过大压力。
总之,在秒杀系统中,除了常规的分拆访问内容和服务,最重要的是尽量将所有数据访问进行缓存化,尽量减少 DB 的访问,在大幅提升系统性能的同时,提升用户体验。

View File

@ -0,0 +1,79 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
34 如何为海量计数场景设计缓存体系?
在上一课时我们讲解了如何为秒杀系统进行缓存设计,在本课时我们将具体讲解如何为海量计数场景设计缓存服务。
计数常规方案
计数服务在互联网系统中非常常见用户的关注粉丝数、帖子数、评论数等都需要进行计数存储。计数的存储格式也很简单key 一般是用户 uid 或者帖子 id 加上后缀value 一般是 8 字节的 long 型整数。
最常见的计数方案是采用缓存 + DB 的存储方案。当计数变更时,先变更计数 DB计数加 1然后再变更计数缓存修改计数存储的 Memcached 或 Redis。这种方案比较通用且成熟但在高并发访问场景支持不够友好。在互联网社交系统中有些业务的计数变更特别频繁比如微博 feed 的阅读数,计数的变更次数和访问次数相当,每秒十万到百万级以上的更新量,如果用 DB 存储,会给 DB 带来巨大的压力DB 就会成为整个计数服务的瓶颈所在。即便采用聚合延迟更新 DB 的方案,由于总量特别大,同时请求均衡分散在大量不同的业务端,巨大的写压力仍然是 DB 的不可承受之重。因此这种方案只适合中小规模的计数服务使用。
在 Redis 问世并越来越成熟后,很多互联网系统会直接把计数全部存储在 Redis 中。通过 hash 分拆的方式,可以大幅提升计数服务在 Redis 集群的写性能,通过主从复制,在 master 后挂载多个从库,利用读写分离,可以大幅提升计数服务在 Redis 集群的读性能。而且 Redis 有持久化机制,不会丢数据,在很多大中型互联网场景,这都是一个比较适合的计数服务方案。
在互联网移动社交领域,由于用户基数巨大,每日发表大量状态数据,且相互之间有大量的交互动作,从而产生了海量计数和超高并发访问,如果直接用 Redis 进行存储,会带来巨大的成本和性能问题。
海量计数场景
以微博为例,系统内有大量的待计数对象。如从用户维度,日活跃用户 2 亿+,月活跃用户接近 5 亿。从 Feed 维度,微博历史 Feed 有数千亿条,而且每日新增数亿条的新 Feed。这些用户和 Feed 不但需要进行计数,而且需要进行多个计数。比如,用户维度,每个用户需要记录关注数、粉丝数、发表 Feed 数等。而从 Feed 维度,每条 Feed 需要记录转发数、评论数、赞、阅读等计数。
而且,在微博业务场景下,每次请求都会请求多个对象的多个计数。比如查看用户时,除了获取该用户的基本信息,还需要同时获取用户的关注数、粉丝数、发表 Feed 数。获取微博列表时,除了获取 Feed 内容,还需要同时获取 Feed 的转发数、评论数、赞数,以及阅读数。因此,微博计数服务的总访问量特别大,很容易达到百万级以上的 QPS。
因此,在海量计数高并发访问场景,如果采用缓存 + DB 的架构,首先 DB 在计数更新就会存在瓶颈,其次,单个请求一次请求数十个计数,一旦缓存 miss穿透到 DBDB 的读也会成为瓶颈。因为 DB 能支撑的 TPS 不过 3000~6000 之间,远远无法满足高并发计数访问场景的需要。
采用 Redis 全量存储方案,通过分片和主从复制,读写性能不会成为主要问题,但容量成本却会带来巨大开销。
因为,一方面 Redis 作为通用型存储来存储计数,内存存储效率低。以存储一个 key 为 long 型 id、value 为 4 字节的计数为例Redis 至少需要 65 个字节左右,不同版本略有差异。但这个计数理论只需要占用 12 个字节即可。内存有效负荷只有 1265=18.5%。如果再考虑一个 long 型 id 需要存 4 个不同类型的 4 字节计数,内存有效负荷只有 (8+16)/(65*4)= 9.2%。
另一方面Redis 所有数据均存在内存,单存储历史千亿级记录,单份数据拷贝需要 10T 以上,要考虑核心业务上 1 主 3 从,需要 40T 以上的内存,再考虑多 IDC 部署,轻松占用上百 T 内存。就按单机 100G 内存来算,计数服务就要占用上千台大内存服务器。存储成本太高。
海量计数服务架构
为了解决海量计数的存储及访问的问题,微博基于 Redis 定制开发了计数服务系统,该计数服务兼容 Redis 协议,将所有数据分别存储在内存和磁盘 2 个区域。首先,内存会预分配 N 块大小相同的 Table 空间,线上一般每个 Table 占用 1G 字节,最大分配 10 个左右的 Table 空间。首先使用 Table0当存储填充率超过阀值就使用 Table1依次类推。每个 Table 中key 是微博 idvalue 是自定义的多个计数。
微博的 id 按时间递增,因此每个内存 Table 只用存储一定范围内的 id 即可。内存 Table 预先按设置分配为相同 size 大小的 key-value 槽空间。每插入一个新 key就占用一个槽空间当槽位填充率超过阀值就滚动使用下一个 Table当所有预分配的 Table 使用完毕,还可以根据配置,继续从内存分配更多新的 Table 空间。当内存占用达到阀值,就会把内存中 id 范围最小的 Table 落盘到 SSD 磁盘。落盘的 Table 文件称为 DDB。每个内存 Table 对应落盘为 1 个 DDB 文件。
计数服务会将落盘 DDB 文件的索引记录在内存,这样当查询需要从内存穿透到磁盘时,可以直接定位到磁盘文件,加快查询速度。
计数服务可以设置 Schema 策略,使一个 key 的 value 对应存储多个计数。每个计数占用空间根据 Schema 确定,可以精确到 bit。key 中的各个计数,设置了最大存储空间,所以只能支持有限范围内的计数。如果计数超过设置的阀值,则需要将这个 key 从 Table 中删除,转储到 aux dict 辅助词典中。
同时每个 Table 负责一定范围的 id由于微博 id 随时间增长而非逐一递增Table 滚动是按照填充率达到阀值来进行的。当系统发生异常时,或者不同区域网络长时间断开重连后,在老数据修复期间,可能在之前的 Table 中插入较多的计数 key。如果旧 Table 插入数据量过大,超过容量限制,或者持续搜索存储位置而不得,查找次数超过阀值,则将新 key 插入到 extend dict 扩展词典中。
微博中的 feed 一般具有明显的冷热区分,并且越新的 feed 越热,访问量越大,越久远的 feed 越冷。新的热 key 存放内存 Table老的冷 key 随所在的 Table 被置换到 DDB 文件。当查询 DDB 文件中的冷 key 时,会采用多线程异步并行查询,基本不影响业务的正常访问。同时,这些冷 key 从 DDB 中查询后,会被存放到 LRU 中,从而方便后续的再次访问。
计数服务的内存数据快照仍然采用前面讲的 RDB + 滚动 AOF 策略。RDB 记录构建时刻对应的 AOF 文件 id 及 pos 位置。全量复制时master 会将磁盘中的 DDB 文件,以及内存数据快照对应的 RDB 和 AOF 全部传送给 slave。
在之后的所有复制就是全增量复制slave 在断开连接,再次重连 master 时,汇报自己同步的 AOF 文件 id 及位置master 将对应文件位置之后的内容全部发送给 slave即可完成同步。
计数服务中的内存 Table 是一个一维开放数据,每个 key-value 按照 Schema 策略占用相同的内存。每个 key-value 内部key 和多个计数紧凑部署。首先 8 字节放置 long 型 key然后按Schema 设置依次存放各个计数。
key 在插入及查询时,流程如下。
首先根据所有 Table 的 id 范围,确定 key 所在的内存 Table。
然后再根据 double-hash 算法计算 hash用 2 个 hash 函数分别计算出 2 个 hash 值,采用公示 h1+N*h2 来定位查找。
在对计数插入或变更时,如果查询位置为空,则立即作为新值插入 key/value否则对比 key如果 key 相同,则进行计数增减;如果 key 不同,则将 N 加 1然后进入到下一个位置继续进行前面的判断。如果查询的位置一直不为空且 key 不同,则最多查询设置的阀值次数,如果仍然没查到,则不再进行查询。将该 key 记录到 extend dict 扩展词典中。
在对计数 key 查找时,如果查询的位置为空,说明 key 不存在,立即停止。如果 key 相同,返回计数,否则 N 加 1继续向后查询如果查询达到阀值次数没有遇到空且 key 不同,再查询 aux dict 辅助字典 和 extend dict 扩展字典,如果也没找到该 key则说明该 key 不存在,即计数为 0。
海量计数服务收益
微博计数服务,多个计数按 Schema 进行紧凑存储,共享同一个 key每个计数的 size 按 bit 设计大小,没有额外的指针开销,内存占用只有 Redis 的 10% 以下。同时,由于 key 的计数 size 固定,如果计数超过阀值,则独立存储 aux dict 辅助字典中。
同时由于一个 key 存储多个计数,同时这些计数一般都需要返回,这样一次查询即可同时获取多个计数,查询性能相比每个计数独立存储的方式提升 3~5 倍。