🍊 mysql数据库集群高可用
使用MySQL自身的功能来搭建的集群,这样的集群,不具备高可用的功能。即如果是MySQL主服务挂了,从服务是没办法自动切换成主服务的。常见的MySQL集群方案有三种: MMM、MHA、MGR。
对主从复制集群中的Master节点进行监控。 自动的对Master进行迁移,通过VIP。 重新配置集群中的其它slave对新的Master进行同步。
🎉 MMM
MMM是一套由Perl语言实现的脚本程序,可以对mysql集群进行监控和故障迁移。他需要两个Master,同一时间只有一个Master对外提供服务,可以说是主备模式。他是通过一个VIP(虚拟IP)的机制来保证集群的高可用。整个集群中,在主节点上会通过一个VIP地址来提供数据读写服务,而当出现故障时,VIP就会从原来的主节点漂移到其他节点,由其他节点提供服务。
优点:
提供了读写VIP的配置,使读写请求都可以达到高可用 工具包相对比较完善,不需要额外的开发脚本 完成故障转移之后可以对MySQL集群进行高可用监控
缺点:
故障简单粗暴,容易丢失事务,建议采用半同步复制方式,减少失败的概率 目前MMM社区已经缺少维护,不支持基于GTID的复制
适用场景:
读写都需要高可用的 基于日志点的复制方式
🎉 MHA
由日本人开发的一个基于Perl脚本写的工具。这个工具专门用于监控主库的状态,当发现master节点故障时,会提升其中拥有新数据的slave节点成为新的master节点,在此期间,MHA会通过其他从节点获取额外的信息来避免数据一致性方面的问题。MHA还提供了mater节点的在线切换功能,即按需切换master-slave节点。MHA能够在30秒内实现故障切换,并能在故障切换过程中,最大程度的保证数据一致性。在淘宝内部,也有一个相似的TMHA产品。
MHA是需要单独部署的,分为Manager节点和Node节点,两种节点。其中Manager节点一般是单独部署的一台机器。而Node节点一般是部署在每台MySQL机器上的。 Node节点得通过解析各个MySQL的日志来进行一些操作。
Manager节点会通过探测集群里的Node节点去判断各个Node所在机器上的MySQL运行是否正常,如果发现某个Master故障了,就直接把他的一个Slave提升为Master,然后让其他Slave都挂到新的Master上去,完全透明。
优点:
MHA除了支持日志点的复制还支持GTID的方式 同MMM相比,MHA会尝试从旧的Master中恢复旧的二进制日志,只是未必每次都能成功。如果希望更少的数据丢失场景,建议使用MHA架构。
缺点:
MHA需要自行开发VIP转移脚本。 MHA只监控Master的状态,未监控Slave的状态
🎉 MGR
MGR:MySQL Group Replication。 是MySQL官方在5.7.17版本正式推出的一种组复制机制。主要是解决传统异步复制和半同步复制的数据一致性问题。
由若干个节点共同组成一个复制组,一个事务提交后,必须经过超过半数节点的决议并通过后,才可以提交。引入组复制,主要是为了解决传统异步复制和半同步复制可能产生数据不一致的问题。MGR依靠分布式一致性协议(Paxos协议的一个变体),实现了分布式下数据的最终一致性,提供了真正的数据高可用方案(方案落地后是否可靠还有待商榷)。
支持多主模式,但官方推荐单主模式:
多主模式下,客户端可以随机向MySQL节点写入数据
单主模式下,MGR集群会选出primary节点负责写请求,primary节点与其它节点都可以进行读请求处理.
优点:
基本无延迟,延迟比异步的小很多 支持多写模式,但是目前还不是很成熟 数据的强一致性,可以保证数据事务不丢失
缺点:
仅支持innodb,且每个表必须提供主键。 只能用在GTID模式下,且日志格式为row格式。
🌟 Redis知识点
🍊 多路复用
🎉 redis的多路复用模式
redis使用模型有:select、poll、epoll。这里简单讲二种。
📝 应用对外提供服务的过程
一个应用程序, 想对外提供服务, 一般都是通过建立套接字监听端口来实现, 也就是socket。
应用对外提供服务的过程:
- 创建套接字
- 绑定端口号
- 开始监听
- 当监听到连接时, 调用系统read去读取内容,但是读取操作是阻塞的。
📝 select
问题:如果主线程处理read,就不能接收其他连接了, 所以只能开新的线程去处理这个事情。而且read操作是调用系统函数, 需要进行进程的切换, 从用户进程切换到系统进程,连接少还好,连接多的话,无疑降低了性能。
解决方案:用户线程批量将要查询的连接发给操作系统,这个过程只发生一次进程的切换,用户线程告诉操作系统,需要哪些数据, 它遍历查找,然后将结果返回给用户线程,这就是select。
操作系统接收到一组文件描述符,然后批量处理这些文件描述符,有顺序的循环检查有没有数据,然后返回结果。无论是监听端口还是建立了连接,程序拿到的都是一个文件描述符,将这些文件描述符批量查询就是了。
📝 epoll
问题:有10万个连接,其中有数据的只有一个,那就回有9999次无效的操作,每次查询都要把所有的都传过去, 10万个就要传10万。
解决方案:直接知道哪些连接是有数据的,然后操作系统通知用户线程,那个连接是可以直接拿到数据的,用户线程就直接通过这个连接去读数据就好了,不需要遍历,这就是epoll。
建立一个需要回调的连接, 将需要监听的文件描述符都扔给操作系统,当有新数据到达时,会直接返回给用户线程。用户线程将监听的列表交给操作系统维护,这样当有新数据来的时候,操作系统知道这是你要的,等你下次来拿的时候,直接给你了,少去了上面的遍历。
📝 多路复用的定义
“多路”指的是多个网络连接,“复用”指的是复用同一个线程。
采用多路 I/O 复用技术可以让单个线程高效的处理多个连接请求(尽量减少网络IO的时间消耗)。
📝 多路复用的举例
redis 需要处理 3 个 IO 请求,同时把 3 个请求的结果返回给客户端,所以总共需要处理 6 个 IO 事件, 由于 Redis服务端对于命令的处理是单线程的,同一时间只能处理一个 IO 事件。于是 redis 需要在合适的时间暂停对某个 IO 事件的处理,转而去处理另一个 IO 事件, 这样 redis 就好比一个开关,当开关拨到哪个 IO 事件这个电路上,就处理哪个 IO 事件,其他 IO 事件就暂停处理了。这就是IO多路复用技术。用一句话总结就是,一个客户端建立好连接后,就可以立刻等待新的客户端连接,而不用阻塞在原客户端的 read 请求上。
📝 多路复用的实现
select, poll, epoll 都是I/O多路复用的具体的实现。epoll性能比其他几者要好。redis中的I/O多路复用的所有功能通过包装常见的select、epoll、evport和kqueue这些I/O多路复用函数库来实现的。 redis的io模型主要是基于epoll实现的,不过它也提供了 select和poll的实现,默认采用epoll。
🔥 过程一:数据未就绪
多个客户端并发请求时,用户线程发起请求的时候,首先会将socket监听列表添加到select中,让select调用操作系统的API,这个过程就是从用户态到内核态,由于当前请求是交给了操作系统去处理,现在的用户线程这时候就空闲了,可以重新接收新的客户端请求。
操作系统等待select调用,当数据到达时,select函数被激活,操作系统将select函数的结果socket监听结果为可读,返回给用户线程,告诉用户线程这个连接可以读取数据了,这个时候用户线程才正式发起read请求,读取数据,这个读取数据的过程也是非阻塞的。
🔥 过程二:数据就绪
read 函数的效果是,如果没有数据到达时(到达网卡并拷贝到了内核缓冲区),立刻返回一个错误值(-1),而不是阻塞地等待。只有当操作系统告诉用户线程数据已经准备就绪的时候,数据从内核缓冲区拷贝到用户缓冲区才通知用户进程调用完成,返回结果,这个过程是阻塞的。
对于用户线程来说,它可以注册多个socket监听,然后不断地调用select读取,操作系统找到用户线程需要的连接,这个连接里面监听到有用户线程所需要的数据,就会激活socket把结果返回给用户线程。
🍊 单线程模型
🎉 为什么redis使用单线程模型还能保证高性能?
第一个是因为redis 是纯内存操作,内存的响应时长是 100 纳秒左右,这是 redis 的 QPS 过万的重要基础。
第二个是因为redis 的核心是基于非阻塞的IO多路复用机制,单线程模型避免了线程切换和竞态产生的消耗,解决了多线程的切换性能损耗问题。
第三个是因为redis底层使用C语言实现,一般来说,C 语言实现的程序"距离"操作系统更近,执行速度相对会更快。
🎉 你是如何理解redis单线程模型的?
Redis 里面的单线程主要是 Redis 的网络 IO 和键值对读写,它是由一个线程来完成的,但是 Redis 的其他功能, 比如说持久化、异步删除、集群数据同步等等,这些其实是由额外的线程执行的,这里的单线程主要是Redis 对外提供键值存储服务来说的。
主要流程是这样的:redis 会将每个客户端都关联一个指令队列,客户端的指令通过队列来按顺序处理,先到先处理,一个客户端指令队列中的指令是按顺序执行的。 redis 的每个客户端都关联一个响应队列,通过响应队列有顺序地将指令的返回结果返回给客户端,并且redis 同一时间每次都只能处理一个客户端队列中的指令或者响应。
🍊 Redis底层数据结构
🎉 简单字符串
先简单了解一下C语言是怎么处理字符串的:
在C语言中,字符串结束的标识是空字符,也就是’’,这会有一个问题,就是字符串的内容可能包括空字符串,这个时候是不是就没办法正确存取字符串的内容了,它有可能中途读取一半就完了。
除此之外,它还不记录字符串的长度,这也会有一系列问题,
如果需要获取字符串的长度通过遍历计数来获取的,这会导致它的时间复杂度会比较高。
如果需要修改字符串,就要重新分配内存,不重新分配的话,字符串长度增大,超出给定的长度,这个时候会造成内存缓冲区溢出,字符串长度减小还会造成内存泄露。
如果需要对两个字符串进行拼接,是通过调用strcat函数来实现的,如果没有给它分配足够长度的内存空间,就会直接导致缓冲区溢出。
既然C语言处理字符串有这么多的弊端,那么Redis它是怎么处理字符串的呢?
Redis专门创建了一种数据结构SDS,什么意思呢?simple dynamic string,简单字符串。
官方代码:
struct sdshdr{ int len; int free; char buf[]; }
这个对象有三个属性:
- len表示字符串的长度
- free表示还有多少长度剩余,就是下面buf数组中还有多少字符串未使用的字节数量
- buf[]表示存储的字符串
问题一:这种数据结构有什么优势呢?跟C语言相比,改进了哪些问题?
长度和内存重新分配问题,C语言是不记录长度,而SDS它有len属性和free属性。
len记录了字符串的长度,直接取值就可以了,不像C语言需要遍历。 如果需要对字符串进行修改的话,也不需要像C语言一样,直接重新分配内存,
它可以通过len 属性检查内存空间是不是需要进行扩展内存,如果字符串长度增加,长度超过了len,就会增加相应的内存,接着修改。
如果字符串长度缩短了,它也不会立马就重新分配内存,而是有一个free属性记录下来,等你后面什么时候用了,重新计算或者分配内存。
结尾标识问题,C语言是以空字符串结尾标识的,而SDS是以len长度作为结尾标识的,避免了C语言无法正确读取字符串的问题。
🎉 链表
Redis的list类型的键值对底层数据结构是由链表构成的,那么链表是什么呢?
它是由一连串节点组成,没有顺序,不是连续的,每个节点由数据和一或两个用来指向上一个或下一个节点位置的链接组成,在每一个节点里存到下一个节点的指针,通过链表中的指针链接次序可以实现逻辑顺序。
链表也分好几种:单向链表、双端链表、双向链表、有序链表以及有迭代器的链表
单向链表:用户的操作(添加、删除、遍历)只能从链表头开始。向一个方向遍历,查找一个节点的时候从第一个节点开始访问下一个节点,一直访问到需要的位置,最后一个节点存储地址的部分指向空值。
双端链表:双端链表相对于单端链表多了一个特性:对最后一个链接点的引用
双向链表:单端链表只能从链表头开始正向遍历,双向链表可以逆向遍历,每个节点需要保存前一个节点和后一个节点的引用
有序链表:插入元素时,将插入的元素与头结点及其后面的结点比较,找到合适的位置插入。
有迭代器的链表:单链表的基本操作中,大部分要用到依次遍历单链表中的每一个元素。当你新增一个对单链表的操作并需要使用遍历时,你就得重新写一个for循环而实现遍历。所以将迭代(遍历)作为一种基本的ADT(抽象数据类型)操作。链表中用于处理遍历、访问和更新的方法封装到一个新的迭代器类中。
🎉 跳跃表
跳跃表:跳跃表基于有序链表的扩展,在链表上建索引,每两个结点提取一个结点到上一级,我们把抽出来的那一级叫作索引,每个跳跃表节点的层高都是1至32之间的随机数。
举例说明:
比如给一个长度为7的有序链表,节点值依次是1->3->4->5。取出所有值为奇数的节点作为关键节点(索引),这个时候要插入一个值是2的新节点,就不需要将节点一个个比较,只要比较1,3,5,确定了值在1和3之间,就可以快速插入。
加一层索引之后,查找一个结点需要遍历的结点个数减少了,虽然增加了50%的额外空间,但是查找效率提高了,同理再加一级索引,这种链表加多级索引的结构,就是跳跃表。
索引是占内存的,原始链表中存储的可能是大的对象,索引结点只要存储关键值和几个指针,并不需要存储对象,当节点本身比较大或者元素数量比较多的时候,优势必然会被放大,而缺点则可以忽略。
问题:当大量的新节点通过逐层比较,最终插入到原链表之后,上层的索引节点会慢慢的不够用,那么这个时候要怎么选取一部分节点提到上一层呢?
抛硬币法:随机决定新节点是否选拔,每向上提拔一层的几率是50%。
原因:跳跃表的删除和添加节点是无法预测的,不能保证索引绝对分步均匀,不过可以让大体趋于均匀。
插入节点的工作流程:跳跃表插入操作的时间复杂度是O(logN),空间复杂度是 O(N)。
- 第一步:新节点和上层索引节点逐个比较,找到原链表的插入位置,时间复杂度为O(logN)
- 第二步:把索引插入到原链表,时间复杂度为O(1)
- 第三步:随机决定新节点是否提升为上一级索引,结果为"正面"则提升,继续抛硬币,结果为"反面"则停止,时间复杂度为O(logN)
删除节点的工作流程:跳跃表删除操作的时间复杂度是O(logN)
- 第一步:自上而下,查找第一次出现节点的索引,并逐层找到每一层对应的节点。时间复杂度为O(logN)
- 第二步:删除每一层查找到的节点,如果该层只剩下1个节点,删除整个一层(原链表除外)。时间复杂度为O(logN)
跳跃表由zskiplistNode和skiplist两个结构组成,zskiplistNode用于表示跳跃表节点,zskiplist用于保存跳跃表节点的相关信息,比如节点的数量,以及指向表头节点和表尾节点的指针等等。
🎉 字典
字典,顾名思义,通过字典(牛津字典等)前面的目录快速定位到所要查找的单词。
在C 语言中没有这种数据结构,所以这种数据结构是Redis自己创造的,字典中的键都是唯一的,通过键可以对值来进行查询或更改。
底层是通过哈希表实现的,而哈希表又基于数组,类似于key-value的结构形式进行存储的,它的值通过哈希函数映射为数组的下标。
那什么是哈希函数呢?不急,我们慢慢道来。
前面我们讲了通过数组的方式存储值,那么数组的值和数组的下标怎么建立关联关系呢?或者说,我们怎么通过数组的下标找到数组的值呢?
在学习 ASCII 编码的时候,我们知道,a可以用97这个数值表示,b可以用98这个数值表示,以此类推,我们就可以通过单个字母用数字表达。
有了字母,那么一个单词由多个字母组成,它又该如何表达呢?
假设我有一本字典,它有10000个单词,我其中一个单词就是ab,使用ASCII编码进行表达。
ab = 97 + 98 = 195
那么存储在数组中的下标为195,这就是字母表达的基本原理,但是如果只是这样还是远远不够的,因为会出现一个数组存储多个单词的情况。
举例说明:假设有个单词有 10 个字母,那么字典的某个单词为 zzzzzzzzzz ,转换为数字:zzzzzzzzzz = 26*10 = 260。
补充说明:这个时候会发现我一本字典里10000个单词,在260这个范围内肯定是不够存储10000个单词的,10000/260=39(38.4补一位),一个数组项它要存储39个单词。
解决方案:为了保证数值的唯一,让每个数组都能够只存储一个单词,进行升级, 将单词表示的数拆开,27 的幂乘以这些位数,有26个可能的字符,以及空格,一共27个。
ab = 97乘以27的一次幂加上98乘以27的零次幂 = 27*97 + 98 = 2717。解决了数组存储多个单词的问题,又引出新的问题数组分配大空间太多了。
举例说明:假设有个单词有 10 个字母,那么字典的某个单词为 zzzzzzzzzz ,转换为数字:zzzzzzzzzz = 26的9次幂 = 7000000000000
补充说明:数组中只有小部分存放了单词,其他空间都是空着的
解决方案:将巨大的整数范围压缩到可接受的数组范围内,可以通过取余解决,一个整数被另一个整数除后的余数。
举例说明:假设要把从0-99的数字(用large表示),压缩为从0-9的数字(用number表示),后者有10个数,所以变量range 的值为10,这个转换的表达式为:
补充说明:number = large % range。当一个整数被 10 整除时,余数是在0-9之间,把从0-99的数压缩为从0-9的数,压缩率为 10 :1。
使用哈希函数向数组插入数据后,这个数组就是哈希表,它的值就是通过上面这种方式映射到数组的下标上的。
这也就是哈希函数的工作模式,它把一个大范围的数字哈希转化成一个小范围的数字,这个小范围的数对应着数组的下标。
但是这种工作模式会有一点问题:把大的数字范围压缩到小的数字范围,会有几个不同的单词哈希化到同一个数组下标,这就是所谓的哈希冲突。
问题:那么如何解决哈希冲突呢?
开放地址法:指定的数组范围大小是存储数据的两倍,有一半的空间是空的。
当冲突产生时,通过(线性探测、二次探测以及再哈希法)方法找到数组的一个空位,把单词填入,不用哈希函数得到数组的下标。
线性探测中,如果哈希函数计算的原始下标是x, 线性探测就是x+1, x+2, x+3, 以此类推,而在二次探测中,探测的过程是x+1,
x+4, x+9, x+16。这二种方式都会有聚集情况。
什么是聚集呢?当哈希表快要满的时候,每插入新的数据,都要频繁的探测插入位置,很多位置都被前面插入的数据所占用了,这称为聚集。
再哈希法:依赖关键字的探测序列,把关键字用不同的哈希函数再做一遍哈希化,用这个结果作为步长,步长在整个探测中是不变的,不过不同的关键字使用不同的步长。
链地址法:数组的每个数据项都创建一个子链表或子数组,那么数组内不直接存放单词,当产生冲突时,新的数据项直接存放到这个数组下标表示的链表中。
整数集合:顾名思义,用来保存整数值类型的集合,保证元素不会重复。
定义:
typedef struct intset{ //编码方式 uint32_t encoding; //集合包含的元素数量 uint32_t length; //保存元素的数组 int8_t contents[]; }intset;
contents数组声明为int8_t类型,但是contents数组并不保存任何int8_t类型的值,真正类型由encoding决定。比如:
- encoding属性的值为INTSET_ENC_INT16,contents是int16_6类型的数组,数组里的每个项是int16_t类型的是整数值。
- encoding属性的值为INTSET_ENC_INT32,contents是int32_t类型的数组,数组里的每个项是int32_t类型的整数值。
- encoding属性的值为INTSET_ENC_INT64,contents是int64_t类型的数组,数组里的每个项是int64_t的整数值。
新增的元素类型比原集合元素类型的长度大的时候,根据新元素类型增加整数集合底层数组的容量,给新元素分配空间,
将底层数组现有的所有元素都转成与新元素相同类型的元素,把转换后的元素放到正确的位置,整个元素顺序是有序的,能极大地节省内存。
🎉 压缩列表
压缩列表,它是特殊编码的连续内存块组成的顺序型数据结构,压缩列表有任意多个节点(entry),每个节点有一个字节数组或者一个整数值。
压缩列表不是用某种算法对数据进行压缩,它将数据按照一定规则编码,放在一块连续的内存区域,目的是节省内存。
压缩列表包含以下:
zlbytes:记录整个压缩列表占用的内存字节数。
zltail:记录压缩列表表尾节点距离压缩列表的初始地址有多少字节。
zllen:记录压缩列表包含的节点数量。
zlend:用来标记压缩列表的末端。
entryX:列表的节点,包含
- previous_entry_ength:记录压缩列表前一个字节的长度。
- encoding:节点的encoding保存的是节点的content的内容类型以及长度。
- content:content区域用于保存节点的内容,节点内容类型和长度由encoding决定。
总结:
简单字符串:SDS作为redis专门为字符串存取开发的数据结构,有获取字符串长度快,杜绝了缓存区的溢出,减少了修改字符串长度时所需的内存重分配次数,二进制安全,兼容部分C函数
链表:用作列表键、发布与订阅、慢查询、监视器等功能实现。
字典:用哈希表实现,字典有两个哈希表,一个正常使用,另一个用于rehash时使用,链地址法解决哈希冲突。
跳跃表:表中的节点按照分值大小进行排序。
整数集合:底层由数组构成,升级特性能尽可能的节省内存。
压缩列表:顺序型数据结构。
🍊 Redis五大数据类型的应用场景
📝 各数据类型应用场景
- 工作中有很多场景经常用到redis, 比如在使用String类型的时候,字符串的长度不能超过512M,可以set存储单个值,也可以把对象转成json字符串存储;还有我们经常说到的分布式锁,就是通过setnx实现的,返回结果是1就说明获取锁成功,返回0就是获取锁失败,这个值已经被设置过。又或者是网站访问次数,需要有一个计数器统计访问次数,就可以通过incr实现。
- 除了字符串类型,还有hash类型,它比string类型操作消耗内存和cpu更小,更节约空间。像我之前做过的电商项目里面,购物车实现场景可以通过hset添加商品,hlen获取商品总数,hdel删除商品,hgetall获取购物车所有商品。另外如果缓存对象的话,修改多个字段就不需要像String类型那样,取出值进行类型转换,然后设值进行类型转换,把它转成字符串缓存进行了。
- 还有列表list这种类型,是简单的字符串列表,按照插入顺序排序,可以添加一个元素到列表的头部或者尾部,它的底层实际上是个链表结构。这种类型更多的是用在文章发布上面,类似微博消息和微信公众号文章,在我之前的项目里面也有用到,比如说我关注了二个媒体,这二个媒体先后发了新闻,我就可以看到先发新闻那家媒体的文章,它可以通过lpush+rpop队列这种数据结构实现先进先出,当然也可以通过lpush+lpop实现栈这种数据结构来到达先进后出的功能。
- 然后就是集合set,底层是字典实现的,查找元素特别快,另外set 数据类型不允许重复,利用这两个特性我们可以进行全局去重,比如在用户注册模块,判断用户名是否注册。可以通过sadd、smembers等命令实现微信抽奖小程序,微信微博点赞,收藏,标签功能。还可以利用交集、并集、差集的特性实现微博微信的关注模型,交集和并集很好理解,差集可以解释一下,就是用第一个集合减去其他集合的并集,剩下的元素,就是差集。举个微博关注模型的例子,我关注了张三和李四,张三关注了李四和王五,李四关注了我和王五。
我进入了张三的主页
查看共同关注的人(李四),取出我关注的人和张三关注的人,二个集合取交集得出结果是李四,就是通过SINTER交集实现的。
查看我可能认识的人(王五),取出我关注的人和张三关注的人,二个集合取并集得出结果是(张三,李四,王五),拿我关注的人(张三,李四)减去并集里的元素,剩下的王五就是我可能认识的人,可以通过并集和差集实现。
查看我关注的人也关注了他(王五),取出我关注的人他们关注的人,(李四,王五)(我,王五)的交集,就是王五。 - 最后就是有序集合zset,有序的集合,可以做范围查找,比如说排行榜,展示当日排行前十。
🍊 Redis五大数据类型实现原理
对于五大数据类型(String,list,Hash,Set,Zset)实现原理,Redis在底层用到了多种数据结构,通过数据结构来实现键值对,将数据结构创建了一个对象redisObject,根据对象的类型type,为对象设置多种不同的数据结构,对象可以执行特定的命令。
本章主要涉及到的知识点有:
- redisObject的属性
- 五大数据类型编码
注意:本章内容每一小节可单独学习,无论先后。