解决高并发高可用和由此带来的数据一致性问题
解决思路:
隐形问题:可重用性,可扩展性,可维护性。
第一层:基础架构
一般指:云平台、操作系统、网络、存储、数据库和编译器等。现在都是云平台。
第二层:中间件与大数据平台
中间件架构:
分布式服务中间件、消息中间件、数据库中间件、缓存中间件、监控系统、工作流引擎和规则引擎等。
大数据架构:
例如开源的Hadoop生态体系,Hive、Spark、Storm、Flink等。
第三层:业务系统架构
通用软件系统:
最常用的办公软件、浏览器、播放器等。
离线业务系统:
基于大数据的BI分析、数据挖掘、报表与可视化等。
大型在线业务系统:
搜索、推荐、IM、电商、游戏、广告、企业ERP或CRM。
这只是通用的划分,有些可能划分不会这么细致
精通一门语言,触类旁通,举一反三。
缓冲I/O:C语言提供的库函数,均以f大头;
直接I/O:linux的系统API(底层也是C编写的)
应用程序内存:通常写代码用malloc/free、new/delete等分配出来的内存。
用户缓冲区:C语言的FILE结构体里面的buffer。
内核缓冲区:Linux操作系统的Page Cache。一个Page一般是4K(没说是Byte还是bit)
对于缓冲IO,一次读操作有3次数据拷贝,写操作有反向的3次数据拷贝
读:磁盘->内核缓冲区->用户缓冲区->应用程序内存
写:应用程序内存->用户缓冲区->内核缓冲区->磁盘
对于直接IO,一次读操作有2次数据拷贝,写操作有反向的2次数据拷贝
读:磁盘->内核缓冲区->应用程序内存
写:应用程序内存->内核缓冲区->磁盘
所以,所谓的“直接IO”其中直接的意思是指没有用户级的缓冲,但操作系统本身的缓冲还是有的。
内存映射文件在直接IO又取消了一次拷贝,将 ”内核缓冲区->应用程序内存“ 这一步取消。
读:磁盘->内核缓冲区
写:内核缓冲区->磁盘
本质上,是使用一个逻辑地址将应用程序内存直接指向内核缓冲区。
原来的数据发送需要将数据搬运到socket缓冲区,但现在直接在socket缓冲区做一层映射,将内核缓冲区的数据映射到socket缓冲区。
一般有四种:
同步阻塞IO
同步非阻塞IO
IO多路复用
在linux中,有三种IO复用方法:select、epoll、poll,epoll效率最高。
异步IO
读写都由操作系统完成。
Reactor:主动模式。由程序不断轮询OS或框架IO是否就绪。
Proactor:被动模式。read、write操作都由操作系统或框架完成,之后再回调给应用程序。
epoll的过程分成三个步骤:
关于LT和ET:
换句话说,ET只会通知一次,一次必须把数据都读完;但LT没有这种限制。一般i倾向于使用LT,相对安全可控。
epoll、select、poll详解:https://www.itqiankun.com/article/select-poll-epoll
I/O多路复用(multiplexing)的本质是通过一种机制(系统内核缓冲I/O数据),让单个进程可以监视多个文件描述符,一旦某个描述符就绪(一般是读就绪或写就绪),能够通知程序进行相应的读写操作
select、poll 和 epoll 都是 Linux API 提供的 IO 复用方式。
select、poll、epoll之间的区别:
\ | select | poll | epoll |
---|---|---|---|
操作方式 | 遍历 | 遍历 | 回调 |
底层实现 | 数组 | 链表 | 哈希表 |
IO效率 | 每次调用都进行线性遍历,时间复杂度为O(n) | 每次调用都进行线性遍历,时间复杂度为O(n) | 事件通知方式,每当fd就绪,系统注册的回调函数就会被调用,将就绪fd放到rdllist里面。时间复杂度O(1) |
最大连接数 | 1024(x86)或 2048(x64) | 无上限 | 无上限 |
fd拷贝 | 每次调用select,都需要把fd集合从用户态拷贝到内核态 | 每次调用poll,都需要把fd集合从用户态拷贝到内核态 | 调用epoll_ctl时拷贝进内核并保存,之后每次epoll_wait不拷贝 |
链接:https://www.jianshu.com/p/722819425dbd
epoll可以理解为event poll(基于事件的轮询)。
epoll本质上是在内核中建立了一个小型的文件系统,用于管理不同的套接字io,来一个就去epoll注册一下,然后一旦就绪会触发内部的callback函数,用于提示fd已就绪,
epoll对文件描述符的操作有两种模式:LT(level trigger)和ET(edge trigger)。LT模式是默认模式,LT模式与ET模式的区别如下:
LT模式:当epoll_wait检测到描述符事件发生并将此事件通知应用程序,应用程序可以不立即处理该事件。下次调用epoll_wait时,会再次响应应用程序并通知此事件。
ET模式:当epoll_wait检测到描述符事件发生并将此事件通知应用程序,应用程序必须立即处理该事件。如果不处理,下次调用epoll_wait时,不会再次响应应用程序并通知此事件。
ET模式在很大程度上减少了epoll事件被重复触发的次数,因此效率要比LT模式高。epoll工作在ET模式的时候,必须使用非阻塞套接口,以避免由于一个文件句柄的阻塞读/阻塞写操作把处理多个文件描述符的任务饿死。
还有一个特点是,epoll使用“事件”的就绪通知方式,通过epoll_ctl注册fd,一旦该fd就绪,内核就会采用类似callback的回调机制来激活该fd,epoll_wait便可以收到通知。
epoll详解:epoll原理详解及epoll反应堆模型 - 知乎 (zhihu.com)
同步IO | 同步阻塞IO | |
同步IO | 同步非阻塞IO | |
同步IO | IO多路复用(同步阻塞) | select epoll poll java的nio |
异步IO | 异步IO | Linux aio windows上的IOCP C++上的asio库 |
总结:
阻塞:如果读写没有就绪或完成,将一直等待;
非阻塞:函数立即返回,然后让程序执行轮询策略;
同步:读写由应用程序完成;
异步:读写由OS完成,完成之后,回调或者事件通知应用程序。
一般有三类:同步阻塞、同步非阻塞、异步IO。IO多路复用也是同步IO,因为读写都是文件完成的。
一个监听线程,N个IO线程,M个worker线程,一般N=CPU核数,M看需要。
监听线程:
负责accept事件的注册和处理。每一个socket连接请求都由它处理,并移交给IO线程。
IO线程:
负责每个socket连接上read write事件的注册和实际的socket读写,把读到的request放入Request队列,交由worker处理。
负责:1. 实际的socket读写;2. 将request交给Worker线程。
Worker线程:
纯粹业务线程,没有socket读写操作。
多线程的好处:
缺点:
因此诞生了并发编程的设计原则:“不要通过共享内存来实现通信,而应通过通信实现共享内存。”即“尽可能通过消息通信,而不是共享内存来实现进程或者线程之间的同步。”
多进程的好处:
nginx是多进程单线程模型。
Redis是单进程单线程(单线程处理请求),利用多核的时候开多个redis实例就可以了
多进程不适合用于IO密集型应用,对于IO密集型的应用,提高IO效率的方法:
内存屏障的意义是:在内存屏障后的语句不会在内存屏障之前的语句执行。
CAS:对比与交换,一般是用版本号。
使用keep-alive字段解决TCP连接复用的问题,使用content-length解决何时关闭TCP连接的问题。
keep-alive和content-Length一起使用。
默认是可复用TCP连接的,所以即使没有keep-alive,也能用。
Chunk机制(content-length的替代):
字段:Transfer-Encoding: chunk。
响应的body分成了一块块的,块之间由间隔符,所有块的结尾由特殊tag。
流水线(pipeline)虽然能提升速度,但也有不小的副作用,所以一般默认关闭。
流水线的问题在于一个包阻塞,会导致后面的包也没接触。
下载时下载到一半连接中断了,客户端可以从上次断的地方继续下载。
Google的SPDY协议。
HTTP/2和HTTP1.1并不是处于平级的位置,而是处在HTTP1.1和TCP之间。现在相当于在HTTP1.1和TCP之间多了一个转换层,这个转换层就是SPDY(HTTP/2)。
为了解决HTTP1.1的队头阻塞问题所设计的核心属性。
二进制分帧:在把这个字符格式的报文给TCP之前转换成二进制,并且分成多个帧(多个数据块)来发送。
SSL——Secure Socket Layer。
TLS——Transport Layer Security,传输层安全协议。
SSL/TLS处于TCP的上面,支撑各种应用层协议。
对称加密->双向非对称加密->单向非对称加密->数字证书与证书认证中心->根证书与CA信任链
截止到单向非对称加密,都可能遭受中间人攻击。
SSL的四次握手:
HTTPS = HTTP + SSL/TLS
所以在HTTP和TCP之间,还有HTTP/2和SSL/TLS可以选
增加了SSL的传输过程的阶段:
因为1和2只在连接建立时做1次,之后只要连接不关闭,每个请求都是直接进入3,所以相比HTTP,性能的增加很小,是常数级别。
TCP解决三个问题:
网络2将军:不管发多少次,永远都不知道最后的那个包是否被对方收到。如果要知道,只能让对方回复ACK,但是对方又不知道这ACK是否收到,只能让对方为这个ACK再回一个ACK。
Quick UDP Internet Connect,基于UDP的多路并发传输协议。
只要使用TCP,就无法解决队头阻塞问题。
QUIC取代了TCP的部分功能(不丢包),实现了SSL的所有功能,取代了HTTP/2的部分功能(多路复用)
冗余(raid5和raid6)
Raid5:
每发送5个数据包,就发送一个冗余包,(R = A+B+C+D+E),所以丢了一个包也能恢复(E = R-A-B-C-D),如果每发送10个数据包就发送一个冗余包,那么十个丢1个包就能恢复。
Raid6:
在上面的基础上,变成两个冗余块。所以5个包可以丢两个包。
A+B+C+D+E=R1
A-B+C-D+E=R2
UDP不需要RTT
与TCP(客户端IP+PORT以及服务端IP+port)不同,使用一个64位的标识标记一个连接。
一般要求达到第三范式。
范式 | 描述 | 反例 |
---|---|---|
第一范式 | 每个字段都是原子的,不能再分解 | |
第二范式 | 1. 表必须有主键,主键可以是单个属性或者几个属性的组合 2. 非主属性必须完全依赖,而不能部分依赖主键 |
在好友关系表中,主键是关注人ID+被关注人ID,但该表中还存储了名字、头像等字段, 这些字段只依赖与组合主键中的某个字段,而不完全依赖主键 |
第三范式 | 没有传递依赖:非主属性必须直接依赖主键 |
分库的问题:
分完之后的问题:
不同的出发点会导致不同的分库方式,应对高并发可以是设置多个从库,业务拆分可以是
必须提供全局唯一的主键生成服务
以电商为例,用用户ID去分还是用订单ID去分。
对于拆分之后其它维度的查询,一般有以下几个方法:
建立一个映射表
建立订单ID和用户ID之间的映射关系。但是很麻烦,要维护一张很大的表
业务双写
同一份数据,两套分库分表。一套用户ID,一套订单ID
异步双写
还是两套表,只是业务单写,然后通过binLog,同步到另外一套表上。
两个维度统一到一个维度:
把订单ID和用户ID统一成一个维度,这样可以按照用户ID分库,然后按订单ID查询的时候取出用户ID,用用户ID去查询。
分库分表之后,如何进行Join操作:
把Join拆成多个单表查询,不让数据库做Join,在软件层面对结果进行拼装;
做宽表,重写轻读
对于不得不用Join的情况,可以另外做一个join表,提前把结果Join好,重写轻读,也是空间换时间
利用搜索引擎
分布式事务很麻烦。最大的问题在于保证ACID。
A——atomicity,原子性
C——consistency,一致性
I——isolation,隔离性
D——durability,持久性
相比K-V结构,B+树可以做到:
磁盘的最小单位是块,InnoDB一块(也叫page)默认是16KB,每个page的id是32位,所以InnoDB的存储上限是64TB(2^32 * 16KB),
一个Page大概装1000个Key(1个Key大概是16Byter,其中8B的key,8B其它字段),意味这B+树有1000个分叉,装叶子节点的话,一个page大概可以装200条记录,一个三层的B+树大概可以存储16GB数据,另外需16MB的内存存储非叶子节点。
所以一般是装16MB的非叶节点数据在内存中,可以支撑2E条记录的查找。
所以建议按主键的自增顺序插入记录,防止引起page的大量迁移。
新建索引B+树,但是叶子节点记录的是数据的主键,可以随意组合新的索引组合。
事务并发导致的几类问题
编号 | 问题 | 描述 |
---|---|---|
1 | 脏读 | 事务A读取了一条记录的值,在做业务逻辑的时候,事务B回滚了该记录,所以A读了一个脏数据 |
2 | 不可重复读 | 在同一个事务里,两次读取同一行记录,但结果不一样,因为另一个事务在对这个数据进行Update操作(没有加锁) |
3 | 幻读 | 在同一个事务里,两次select数据,但结果数目不一样,因为另一个事务在对这个数据进行insert/delete操作(没有加锁) |
4 | 丢失更新 | 两个事务同时修改同一条记录,A的修改被B的修改覆盖了。 |
2-3本质是一样的,区别是2是对数据做update,3是增/删了数据,
InnoDB事务隔离级别
级别 | 名称 | 解决问题 |
---|---|---|
1 | RU,Read Uncommited,读未提交 | 什么都没解决,相当于没有 |
2 | RC,Read Commited,读已提交 | 解决脏读 |
3 | RR,可重复读 | 解决了1-3,InnoDB的默认等级 |
4 | Serialization,串行 | 串行化,完全解决1-4 |
丢失更新是在软件层面保障,比如悲观锁和乐观锁。
几种防丢失更新的方式:
利用单条语句的原子性
悲观锁
很重的锁
乐观锁
CAS机制,保存版本号。
分布式锁
解决分库分表状况下的锁问题
死锁四个条件:
A——atomicity,原子性。
C——consistency,一致性,各种约束条件,比如参照完整性、主键不为空等。C通过上层规则约束,相对简单。
I——isolation,隔离性。
D——durability,持久性。D容易,写到磁盘。
A和I很难,牵扯到并发和崩溃恢复的问题。
内存操作数据 + Write-Ahead Log实现快速操作数据和把数据写入磁盘。
具体到InnoDB中,Write-Ahead Log是Redo Log。在InnoDB中,不光事务修改的数据库表数据是异步刷盘的,连Redo Log的写入本身也是异步的(RedoLog也是先在内存操作,然后再写到磁盘的)。
RedoLog是一个循环使用的块(新的会覆盖旧的)。
跟底层系统息息相关。
最后的解决方法:
读写的并发问题有三种策略
策略 | 解释 |
---|---|
互斥锁 | |
读写锁 | |
CopyOnWrite | 写的时候,把该数据对象拷贝一份,等写完之后,再把数据对象的指针(引用)一次性赋值回去,读的时候读取原始数据。 1. 读和读可以并发 2. 读和写可以并发 3. 写和写理论上可以并发 |
https://zhuanlan.zhihu.com/p/66791480
https://www.php.cn/mysql-tutorials-460111.html
MVCC是“维持一个数据的多个版本,使读写操作没有冲突”的一个抽象概念。换句话说,可能存在一个数据的多个版本
这个概念需要具体功能去实现,这个具体实现就是快照读。(具体实现下面讲)
MVCC解决并发哪些问题?
解决问题如下:
因此有了下面提高并发性能的组合拳:
MVCC的实现原理
它的实现原理主要是:
从以上的描述中我们可以看出来,所谓的MVCC指的就是在使用READ COMMITTD、REPEATABLE READ这两种隔离级别的事务在执行普通的SEELCT操作时访问记录的版本链的过程,这样子可以使不同事务的读-写、写-读操作并发执行,从而提升系统性能。
MVCC解决了快照读和写直接的并发问题,但对于写和写,当前读和写直接的并发,MVCC就无能为力了。
这些锁可以按粒度分(表、行、gap),也可以按模式分(共享、排他、意向)
锁表 | 锁行 | 锁范围 | |
---|---|---|---|
共享S | 表共享锁 | 行共享锁 | |
排他X | 表排他锁 | 行排他锁 | |
意向共享IS | 表意向共享锁 | ||
意向排他IX | 表意向排他锁 | ||
AI | 自增锁 |
表(S锁、X锁)、行(S锁、X锁)
意向锁(IS锁,IX锁)
意向锁是一种不与行级锁冲突表级锁。它的作用是提前向其它人宣布自己即将要对表中某一行上锁,提升判断效率。所以,一个事务要给某张表的某一行加S锁,就必须先获得整张表的IS锁;要给某一张加X锁,就必须先获得整张表的IX锁。
换句话说,IX IS锁只是声明,与其它锁并不互斥。
的
高并发读、高并发写、高并发读写
策略有:
方法有:
本地缓存或Memcached/Redis集中式缓存
缓存可能遇到的问题:
Mysql的Master/Slave(主从)
CDN静态文件加速(动静分离)
方法有:
重写轻读 vs 重读轻写
重写轻读,本质就是“空间换时间“。你不是计算起来耗时,延迟高吗,那我可以提前计算,然后存储起来。取的时候,直接去取。
我们通常对Mysql的用法,都是重读轻写,写的时候,简单;查的时候,做复杂的join计算,返回结果。这样做的好处是容易做到数据的强一致性,不会因为字段冗余,造成数据的不一致。但是性能可能就是问题。
而如果采用重写轻读,怎么做呢?你不是要看Feeds吗,那就为每个人准备一个Feeds,或者说收件箱。某个人发了微博之后,把他的微博扩散到所有人的收件箱,这个扩散是异步的,在后台扩散。这样每个人看自己的Feeds的时候,直接去自己的收件箱取就可以了。
方法有:
本质都是读写分离,即CQRS(Command Query Reponsibility Separation)
CQRS的特征:
本质:对要处理的数据或请求分成多份并行处理
包括有:
数据分片是对要处理的数据进行分片,任务分片是对处理程序本身进行分片,例如流水线
包括有:
写一条记录,可以等数量多了,合并写入。
多进程单线程是为了解决:锁竞争;线程切换开销大,导致线程数无法开很多。
隔离
隔离是指系统或资源分隔开,在系统发生故障时能限定传播范围和影响范围,即发生故障后不会出现滚雪球效应,从而把故障的影响限定在一个范围内。
数据隔离
机器隔离
线程池隔离
信号量隔离
比线程池隔离要更轻量。当信号量达到阈值,线程获取不到该信号量会丢弃请求,而不是阻塞在那等待信号量。
限流
技术层面的限流
业务层面的限流
比如100件产品的秒杀,有2W人抢购,只需要处理前500个请求就可以了。
限流算法
漏桶算法(输出恒定,可适合削峰、应付突发流量)
令牌算法(输入恒定,限制平均速率)
对比
漏桶算法是流出速率保持恒定,令牌算法是流入速率保持恒定。
熔断
一种自我保护,当某个服务已经接近极限的时候,会强制断开,不再提供服务,防止崩溃
降级
兜底方案,尽可能提供最基础的核心业务,其它业务可以不提供。
余春龙《软件架构设计-大型网站技术架构于业务架构融合之道》