Go

Go协程简单用法

对于IO密集型计算可以采用go协程进行处理,使用这三个协程的操作,同时处理可以有效降低耗时。

go学习之- cas的理解

https://blog.csdn.net/chenxun_2010/article/details/103598252

make和new的区别

  1. new 和 make 都用于分配内存;
  2. new 和 make 都是在堆上分配内存;
  3. new 对指针类型分配内存,返回值是分配类型的指针,new不能直接对 slice 、map、channel 分配内存;
  4. make 仅用于 slice、map和 channel 的初始化,返回值为类型本身,而不是指针;

PHP

面试题

声明一个变量占几个字节

16个字节

php是怎么实现弱类型

zval_struct中的zval_value底层会声明联合体(字符串、bool、int强类型声明)

zend string写时复制

当一个变量赋值给另一个变量时,不会直接在存储中复制一份,只会新增一个引用,指向变量的地址空间;当变量发生变更时,才会在存储中复制一份新的。
优点:这样能够节省空间,避免空间的浪费。

php垃圾回收机制

变量在zval的变量容器中结构

zval中,除了存储变量的类型和值之外,还有is_ref字段和refcount字段
1、is_ref:是个bool值,用来区分变量是否属于引用集合。
2、refcount:计数器,表示指向这个zval变量容器的变量个数。

垃圾回收算法(根缓存区满时)
  1. 对每个根缓冲区中的根zval按照深度优先遍历算法遍历所有能遍历到的zval,并将每个zval的refcount减1,同时为了避免对同一zval多次减1(因为可能不同的根能遍历到同一个zval),每次对某个zval减1后就对其标记为“已减”。
  2. 再次对每个缓冲区中的根zval深度优先遍历,如果某个zval的refcount不为0,则对其加1,否则保持其为0。
  3. 清空根缓冲区中的所有根(注意是把这些zval从缓冲区中清除而不是销毁它们),然后销毁所有refcount为0的zval,并收回其内存。

php底层数组

php解决哈希冲突的方式是使用了链接法,所以php数组是由哈希表+链表实现,准确来说,是由哈希表+双向链表实现

内部结构-哈希表

HashTable结构体主要用来存放哈希表的基本信息

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
typedef struct _hashtable { 
uint nTableSize; // hash Bucket的大小,即哈希表的容量,最小为8,以2x增长。
uint nTableMask; // nTableSize-1 , 索引取值的优化
uint nNumOfElements; // hash Bucket中当前存在的元素个数,count()函数会直接返回此值
ulong nNextFreeElement; // 下一个可使用的数字键值
Bucket *pInternalPointer; // 当前遍历的指针(foreach比for快的原因之一)
Bucket *pListHead; // 存储整个哈希表的头元素指针
Bucket *pListTail; // 存储整个哈希表的尾元素指针
Bucket **arBuckets; // 存储hash数组
dtor_func_t pDestructor; // 在删除元素时执行的回调函数,用于资源的释放
zend_bool persistent; //指出了Bucket内存分配的方式。如果persisient为TRUE,则使用操作系统本身的内存分配函数为Bucket分配内存,否则使用PHP的内存分配函数。
unsigned char nApplyCount; // 标记当前hash Bucket被递归访问的次数(防止多次递归)
zend_bool bApplyProtection;// 标记当前hash桶允许不允许多次访问,不允许时,最多只能递归3次
#if ZEND_DEBUG
int inconsistent;
#endif
} HashTable;

Bucket结构体则用于保存数据的具体内容

1
2
3
4
5
6
7
8
9
10
11
12
typedef struct bucket {
ulong h; // 对char *key进行hash后的值,或者是用户指定的数字索引值
uint nKeyLength; // hash关键字的长度,如果数组索引为数字,此值为0
void *pData; // 指向value,一般是用户数据的副本,如果是指针数据,则指向pDataPtr
void *pDataPtr; // 如果是指针数据,此值会指向真正的value,同时上面pData会指向此值
struct bucket *pListNext; // 指向整个哈希表的该单元的下一个元素
struct bucket *pListLast; // 指向整个哈希表的该单元的上一个元素
struct bucket *pNext; // 指向由于哈希冲突导致存放在同一个单元的链表中的下一个元素
struct bucket *pLast; // 指向由于哈希冲突导致存放在同一个单元的链表中的上一个元素
// 保存当前值所对于的key字符串,这个字段只能定义在最后,实现变长结构体
char arKey[1];
} Bucket;

PHP-FPM

CGI协议

为了解决不同的语言解释器(如php、python解释器)与webserver的通信,于是出现了cgi协议。只要你按照cgi协议去编写程序,就能实现语言解释器与webwerver的通信。如php-cgi程序。
缺点:每来一个请求都要去fork一个进程处理,浪费资源

fast-cgi协议

出现了cgi的改良版本,fast-cgi。fast-cgi每次处理完请求后,不会kill掉这个进程,而是保留这个进程,使这个进程可以一次处理多个请求。这样每次就不用重新fork一个进程了,大大提高了效率。

FPM是什么

php-fpm即php-Fastcgi Process Manager.
php-fpm是 FastCGI 的实现,并提供了进程管理的功能。
进程包含 master 进程和 worker 进程两种进程。
master 进程只有一个,负责监听端口,接收来自 Web Server 的请求,而 worker 进程则一般有多个(具体数量根据实际需要配置),每个进程内部都嵌入了一个 PHP 解释器,是 PHP 代码真正执行的地方。

master进程进行强制kill掉,worker进程能不能继续服务?
kill -9 (master来不及通知worker进程挂掉) worker进程能够正常提供服务
kill 会通知worker进程挂掉 worker进程不能提供服务

PHP-FPM 三种模式

  1. pm=static

静态。始终保持一个固定数量的子进程,这个数由(pm.max_childrem)定义,这种方式很不灵活,通常不是默认的

  1. pm=dynamic

动态。子进程的数量在下面配置的基础上动态设置:pm.max_children、pm.start_servers、pm.min_spare_servers、pm.max_spare_servers。

启动时,会创建固定数量的子进程(由pm.start_servers控制)可以理解成最小子进程树,而最大子进程数由pm.max_childrem参数控制,这样的话子进程数在会在最大和最小区间变化。

闲置的子进程由另外2个配置控制,分别是pm.min_spare_servers和pm.max_spare_servers,也就是闲置的子进程也有最大和最小数量限制,而如果闲置的子进程超出pm.max_spare_servers则会被杀掉。

这种模式非常灵活,也通常是默认选项。但是,dynamic模式为了最大化的优化服务器响应,会造成更多内存使用,因为这种模式只会杀掉最大闲置进程数(pm.max_spare_servers)的闲置进程,比如最大闲置进程是30,最大进程是50,然后网站经历一个访问高峰,此时50个子进程全部忙碌,0个闲置进程,然后过了高峰期,可能没有一个请求,于是可能由50个闲置进程,但是此时PHP-FPM会杀掉20个子进程,始终剩下30个闲置进程等待请求,这可能就是为什么过了高峰期后即便请求书大量减少服务器内存使用却没有大量减少,也可能是为什么有些时候重启服务器会好些,因为重启后,PHP-FPM子进程数会变成最小闲置进程数,而不是之前最大闲置进程数。

  1. pm=ondemand

进程有需要时才产生,由dynamic相反,pm=start_servers在服务启动时即启动。

这种模式把内存放到第一位,它的工作模式很简单,每个闲置进程在持续闲置了pm.process_idle_timeout秒后就会被杀掉,有了这个模式,到了服务器低峰期内存会自动降下来,如果服务器长时间没有请求,就只有一个PHP-FPM主进程,当然弊端是,遇到高峰期或者如果pm.process_idle_timeout的值太短的话,无法避免服务器重复创建进程的问题,因此pm=dynamic和pm=ondemand谁更合适视情况而定。

PHP-FPM 生命周期

以nginx服务器为例,在web模式下,生命周期流程如下

SAPI运行PHP都经过下面几个阶段:
1、模块初始化阶段(module init):
这个阶段主要进行php框架、zend引擎的初始化操作。这个阶段一般是在SAPI启动时执行一次,对于FPM而言,就是在fpm的master进行启动时执行的。php加载每个扩展的代码并调用其模块初始化例程(MINIT),进行一些模块所需变量的申请,内存分配等。

2、请求初始化阶段(request init):
当一个页面请求发生时,在请求处理前都会经历的一个阶段。对于fpm而言,是在worker进程accept一个请求并读取、解析完请求数据后的一个阶段。在这个阶段内,SAPI层将控制权交给PHP层,PHP初始化本次请求执行脚本所需的环境变量。

3、php脚本执行阶段
php代码解析执行的过程。Zend引擎接管控制权,将php脚本代码编译成opcodes并顺次执行

4、请求结束阶段(request shutdown):
请求处理完后就进入了结束阶段,PHP就会启动清理程序。这个阶段,将flush输出内容、发送http响应内容等,然后它会按顺序调用各个模块的RSHUTDOWN方法。 RSHUTDOWN用以清除程序运行时产生的符号表,也就是对每个变量调用unset函数。

5、模块关闭阶段(module shutdown):
该阶段在SAPI关闭时执行,与模块初始化阶段对应,这个阶段主要是进行资源的清理、php各模块的关闭操作,同时,将回调各扩展的module shutdown钩子函数。这是发生在所有请求都已经结束之后,例如关闭fpm的操作。(这个是对于CGI和CLI等SAPI,没有“下一个请求”,所以SAPI立刻开始关闭。)

Kafka

Kafka

Topic和Partition

Topic

在 kafka 中,topic 是一个存储消息的逻辑概念,可以认为是一个消息集合。每条消息发送到 kafka 集群的消息都有一个类别。物理上来说,不同的 topic 的消息是分开存储的,每个 topic 可以有多个生产者向它发送消息,也可以有多个消费者去消费其中的消息。

Partition:

每个 topic 可以划分多个分区(每个 Topic 至少有一个分区),同一 topic 下的不同分区包含的消息是不同的。每个消息在被添加到分区时,都会被分配一个 offset(称之为偏移量),它是消息在此分区中的唯一编号,kafka 通过 offset保证消息在分区内的顺序,offset 的顺序不跨分区,即 kafka只保证在同一个分区内的消息是有序的。下图中,对于名字为 test 的 topic,做了 3 个分区,分别是p0、p1、p2.

➢ 每一条消息发送到 broker 时,会根据 partition 的规则选择存储到哪一个 partition。如果 partition 规则设置合理,那么所有的消息会均匀的分布在不同的partition中,这样就有点类似数据库的分库分表的概念,把数据做了分片处理。

kafka 消息分发策略:

  消息是 kafka 中最基本的数据单元,在 kafka 中,一条消息由 key、value 两部分构成,在发送一条消息时,我们可以指定这个 key,那么 producer 会根据 key 和 partition 机制来判断当前这条消息应该发送并存储到哪个 partition 中。我们可以根据需要进行扩展 producer 的 partition 机制   

消息默认的分发机制:

  默认情况下,kafka 采用的是 hash 取模的分区算法。如果Key 为 null,则会随机分配一个分区。这个随机是在这个参数”metadata.max.age.ms”的时间范围内随机选择一个。对于这个时间段内,如果 key 为 null,则只会发送到唯一的分区。这个值在默认情况下是 10 分钟更新一次。关 于 Metadata ,简单理解就是Topic/Partition 和 broker 的映射关系,每一个 topic 的每一个 partition,需要知道对应的 broker 列表是什么,leader是谁、follower 是谁。这些信息都是存储在 Metadata 这个类里面。   

谁来执行 Rebalance 以及管理 consumer 的 group 呢?

Kafka 提供了一个角色:coordinator(协调员) 来执行对于 consumer group 的管理,当 consumer group 的第一个 consumer 启动的时候,它会去和 kafka server(broker) 确定谁是它们组的 coordinator。之后该 group 内的所有成员都会和该 coordinator 进行协调通信。consumer group 如何确定自己的 coordinator 是谁呢? 消费 者 向 kafka 集 群 中 的 任 意 一 个 broker 发 送 一 个GroupCoordinatorRequest 请求,服务端会返回一个负载最小的 broker 节 点 的 id , 并 将 该 broker 设 置 为coordinator。在 rebalance 之前,需要保证 coordinator 是已经确定好了的,整个 rebalance 的过程分为两个步骤 ,一个是JoinGroup 的过程,在这个过程之后会进入一个Synchronizing Group State 阶段。那么这两个阶段都做了什么呢?

JoinGroup 的过程:
  表示加入到 consumer group 中,在这一步中,所有的成员都会向 coordinator 发送 joinGroup 的请求。一旦所有成员都发送了 joinGroup 请求,那么 coordinator 会选择一个 consumer 担任 leader 角色,并把组成员信息和订阅信息发送消费者。下图就是描述了这么一个过程,并且请求与响应中携带的一些重要的信息。

  •  protocol_metadata: 序列化后的消费者的订阅信息
  • leader_id: 消费组中的消费者,coordinator 会选择一个座位 leader,对应的就是 member_id
  • member_metadata 对应消费者的订阅信息
  • members:consumer group 中全部的消费者的订阅信息
  • generation_id:年代信息,类似于 zookeeper 的时候的 epoch 是一样的,对于每一轮 rebalance ,generation_id 都会递增。主要用来保护 consumer group。隔离无效的 offset 提交。也就是上一轮的consumer 成员无法提交 offset 到新的 consumer group 中。

Synchronizing Group State 阶段:
进入了 Synchronizing Group State阶段,主要逻辑是向 GroupCoordinator 发 送SyncGroupRequest 请求,并且处理 SyncGroupResponse响应,简单来说,就是 leader 将消费者对应的 partition 分配方案同步给 consumer group 中的所有 consumer,每个消费者都会向 coordinator 发送 syncgroup 请求,不过只有 leader 节点会发送分配方案,其他消费者只是打打酱油而已。当 leader 把方案发给 coordinator 以后,coordinator 会把结果设置到 SyncGroupResponse 中。这样所有成员都知道自己应该消费哪个分区。

消息的存储:

首先我们需要了解的是,kafka 是使用日志文件的方式来保存生产者和发送者的消息,每条消息都有一个 offset 值来表示它在分区中的偏移量。Kafka 中存储的一般都是海量的消息数据,为了避免日志文件过大,Log 并不是直接对应在一个磁盘上的日志文件,而是对应磁盘上的一个目录,这个目录的命名规则是_比如创建一个名为 firstTopic 的 topic,其中有 3 个 partition,那么在 kafka 的数据目录(/tmp/kafka-log,这里可以通过server.properties中的log.dirs=/tmp/kafka-logs去修改)中就有 3 个目录,firstTopic-0~3多个分区在集群中的分配 如果我们对于一个 topic,在集群中创建多个 partition,那么 partition 是如何分布的呢?

1.将所有 N Broker 和待分配的 i 个 Partition 排序
2.将第 i 个 Partition 分配到第(i mod n)个 Broker 上

幂等性:

  所谓的幂等,简单说就是对接口的多次调用所产生的结果和调用一次是一致的。在0.11.0.0版本引入了创建幂等性Producer的功能。仅需要设置props.put(“enable.idempotence”,true),或props.put(ProducerConfig.ENABLE_IDEMPOTENCE_CONFIG,true)。enable.idempotence设置成true后,Producer自动升级成幂等性Producer。Kafka会自动去重。Broker会多保存一些字段。当Producer发送了相同字段值的消息后,Broker能够自动知晓这些消息已经重复了。作用范围:

只能保证单分区上的幂等性,即一个幂等性Producer能够保证某个主题的一个分区上不出现重复消息。
只能实现单回话上的幂等性,这里的会话指的是Producer进程的一次运行。当重启了Producer进程之后,幂等性不保证。

Redis

应用场景

  1. 缓存(数据缓存、会话缓存、全页面缓存(FPC))
  2. 计数器
  3. 查找表
  4. 消息队列(List)
  5. 分布式锁

redis是单线程还是多线程

  1. 无论什么版本,工作线程就是一个
  2. 6.x高版本出现了IO多线程
  3. 单线程,满足redis的串行原子性,只不过IO多线程,把输入、输出放到更多的线程去并行,好处如下:执行时间缩短,更快;

6.x之前的方式

6.x之后的方式

Redis单线程为什么这么快?

  1. 纯内存操作
  2. 核心是基于非阻塞的IO多路复用机制
  3. 单线程避免了多线程的频繁上下文切换带来的性能问题。

Redis存在线程安全的问题吗? 为什么

redis可以保障内部串行
外界使用的时候要保障,业务上要自行保障顺序~

缓存穿透、缓存击穿、缓存雪崩

缓存穿透

描述:

缓存穿透是指缓存和数据库中都没有的数据,而用户不断发起请求。由于缓存是不命中时被动写的,并且出于容错考虑,如果从存储层查不到数据则不写入缓存,这将导致这个不存在的数据每次请求都要到存储层去查询,失去了缓存的意义。

在流量大时,可能DB就挂掉了,要是有人利用不存在的key频繁攻击我们的应用,这就是漏洞。

如发起为id为“-1”的数据或id为特别大不存在的数据。这时的用户很可能是攻击者,攻击会导致数据库压力过大。
解决方案
接口层增加校验,如用户鉴权校验,id做基础校验,id<=0的直接拦截;

  1. 从缓存取不到的数据,在数据库中也没有取到,这时也可以将key-value对写为key-null,缓存有效时间可以设置短点,如30秒(设置太长会导致正常情况也没法使用)。这样可以防止攻击用户反复用同一个id暴力攻击
  2. 布隆过滤器。bloomfilter就类似于一个hash set,用于快速判某个元素是否存在于集合中,其典型的应用场景就是快速判断一个key是否存在于某容器,不存在就直接返回。布隆过滤器的关键就在于hash算法和容器大小,
  3. 加互斥锁

缓存击穿

描述:

缓存击穿是指缓存中没有但数据库中有的数据(一般是缓存时间到期),这时由于并发用户特别多,同时读缓存没读到数据,又同时去数据库去取数据,引起数据库压力瞬间增大,造成过大压力。

特点:
  1. 热点key过期
  2. 数据库里有大量的并发
  3. redis没有缓存

解决方案

  1. 设置热点数据永远不过期。
  2. 接口限流与熔断,降级。重要的接口一定要做好限流策略,防止用户恶意刷接口,同时要降级准备,当接口中的某些 服务 不可用时候,进行熔断,失败快速返回机制。
  3. 加互斥锁

缓存雪崩

描述:

缓存雪崩是指缓存中数据大批量到过期时间,而查询数据量巨大,引起数据库压力过大甚至down机。和缓存击穿不同的是, 缓存击穿指并发查同一条数据,缓存雪崩是不同数据都过期了,很多数据都查不到从而查数据库。

解决方案:

  1. 缓存数据的过期时间设置随机,防止同一时间大量数据过期现象发生。
  2. 如果缓存数据库是分布式部署,将热点数据均匀分布在不同搞得缓存数据库中。
  3. 设置热点数据永远不过期。

Redis中connect与pconnect区别?

1.首先先介绍下connect和pconnect的区别。
connect:脚本结束之后连接就释放了。

2.pconnect:脚本结束之后连接不释放,连接保持在php-fpm进程中。
所以使用pconnect代替connect,可以减少频繁建立redis连接的消耗。

缓存如何回收的

  1. 后台在轮询,分段分批的删除哪些过期的key
  2. 请求的时候判断时候已经过期了
    尽量的把内存无用的空间回收回来

缓存是如何淘汰的

  1. 内存空间不足的情况下会进行淘汰
  2. 淘汰机制里有不允许淘汰的
  3. lru
  4. 设置过过期的key的集合中

如何进行缓存预热

  1. 提前把数据塞入redis,你知道哪些是热数据吗?
  2. 开发逻辑上也要规避差集,否则会造成击穿、穿透、雪崩(实时加锁)

数据库与缓存不一致如何解决

  1. 分布式事务进行解决
  2. redis是缓存,更倾向于稍微的有时差
  3. 减少对DB的操作(一般是读)
  4. 终极方案 通过canal监听binlog,异步更新缓存。

简述redis的主从不一致的问题

  1. redis的确默认是弱一致性
  2. 锁不能用主从

redis持久化方式

  1. RDB,AOF:主从同步也算持久化
  2. 高版本:开启AOF,AOF是通过执行日志得到全部内存数据的方式,但是追求性能
    1. 体积变大,重复无效的指令 重写,后台用线程把内存中的kv生成指令写入新的aof
    2. 把重写的方式换成直接RDB放到aof文件的头部

分布式锁

基于Redis的实现方式

1、选用Redis实现分布式锁原因:

  1. Redis有很高的性能;
  2. Redis命令对此支持较好,实现起来比较方便

2、使用命令介绍:

  1. SETNX
  2. EXPIRE
  3. DELETE
    在使用Redis实现分布式锁的时候,主要就会使用到这三个命令。

3、实现思想:

  1. 获取锁的时候,使用setnx加锁,并使用expire命令为锁添加一个超时时间,超过该时间则自动释放锁,锁的value值为一个随机生成的UUID,通过此在释放锁的时候进行判断。
  2. 获取锁的时候还设置一个获取的超时时间,若超过这个时间则放弃获取锁。
  3. 释放锁的时候,通过UUID判断是不是该锁,若是该锁,则执行delete进行锁释放

数据库

Mysql 四大特性(ACID)

原子性

事务必须是原子工作单元;对于其数据修改,要么全都执行,要么全都不执行。通常,与某个事务关联的操作具有共同的目标,并且是相互依赖的。如果系统只执行这些操作的一个子集,则可能会破坏事务的总体目标。原子性消除了系统处理操作子集的可能性。

一致性

事务在完成时,必须使所有的数据都保持一致状态。在相关数据库中,所有规则都必须应用于事务的修改,以保持所有数据的完整性。事务结束时,所有的内部数据结构(如 B 树索引或双向链表)都必须是正确的。某些维护一致性的责任由应用程序开发人员承担,他们必须确保应用程序已强制所有已知的完整性约束。例如,当开发用于转帐的应用程序时,应避免在转帐过程中任意移动小数点。

隔离性

由并发事务所作的修改必须与任何其它并发事务所作的修改隔离。事务查看数据时数据所处的状态,要么是另一并发事务修改它之前的状态,要么是另一事务修改它之后的状态,事务不会查看中间状态的数据。这称为可串行性,因为它能够重新装载起始数据,并且重播一系列事务,以使数据结束时的状态与原始事务执行的状态相同。当事务可序列化时将获得最高的隔离级别。在此级别上,从一组可并行执行的事务获得的结果与通过连续运行每个事务所获得的结果相同。由于高度隔离会限制可并行执行的事务数,所以一些应用程序降低隔离级别以换取更大的吞吐量。

持久性

事务完成之后,它对于系统的影响是永久性的。该修改即使出现致命的系统故障也将一直保持。

隔离级别

Read Uncommitted(读取未提交内容)

在该隔离级别,所有事务都可以看到其他未提交事务的执行结果。本隔离级别很少用于实际应用,因为它的性能也不比其他级别好多少。读取未提交的数据,也被称之为脏读(Dirty Read)。

Read Committed(读取提交内容)

这是大多数数据库系统的默认隔离级别(但不是MySQL默认的)。它满足了隔离的简单定义:一个事务只能看见已经提交事务所做的改变。这种隔离级别 也支持所谓的不可重复读(Nonrepeatable Read),因为同一事务的其他实例在该实例处理其间可能会有新的commit,所以同一select可能返回不同结果。

Repeatable Read(可重读)

这是MySQL的默认事务隔离级别,它确保同一事务的多个实例在并发读取数据时,会看到同样的数据行。不过理论上,这会导致另一个棘手的问题:幻读 (Phantom Read)。简单的说,幻读指当用户读取某一范围的数据行时,另一个事务又在该范围内插入了新行,当用户再读取该范围的数据行时,会发现有新的“幻影” 行。InnoDB和Falcon存储引擎通过多版本并发控制(MVCC,Multiversion Concurrency Control)机制解决了该问题。

Serializable(可串行化)

这是最高的隔离级别,它通过强制事务排序,使之不可能相互冲突,从而解决幻读问题。简言之,它是在每个读的数据行上加上共享锁。在这个级别,可能导致大量的超时现象和锁竞争。

这四种隔离级别采取不同的锁类型来实现,若读取的是同一个数据的话,就容易发生问题。例如

  • 脏读(Drity Read):某个事务已更新一份数据,另一个事务在此时读取了同一份数据,由于某些原因,前一个RollBack了操作,则后一个事务所读取的数据就会是不正确的。
  • 不可重复读(Non-repeatable read):在一个事务的两次查询之中数据不一致,这可能是两次查询过程中间插入了一个事务更新的原有的数据。
  • 幻读(Phantom Read):在一个事务的两次查询中数据笔数不一致,例如有一个事务查询了几列(Row)数据,而另一个事务却在此时插入了新的几列数据,先前的事务在接下来的查询中,就会发现有几列数据是它先前所没有的。

在MySQL中,实现了这四种隔离级别,分别有可能产生问题如下所示:

隔离级别 脏读 不可重复读 幻读
Read Uncommitted(读取未提交内容) ✔️ ✔️ ✔️
Read Committed(读取提交内容) ✔️ ✔️
Repeatable Read(可重读) ✔️
Serializable(可串行化)

MVCC

MVCC,全称 Multi-Version Concurrency Control ,即多版本并发控制。MVCC 是一种并发控制的方法,一般在数据库管理系统中,实现对数据库的并发访问,在编程语言中实现事务内存。

地址:https://blog.csdn.net/SnailMann/article/details/94724197

什么是当前读和快照读?

  • 当前读
    像 select lock in share mode (共享锁), select for update; update; insert; delete (排他锁)这些操作都是一种当前读,为什么叫当前读?就是它读取的是记录的最新版本,读取时还要保证其他并发事务不能修改当前记录,会对读取的记录进行加锁

  • 快照读
    像不加锁的 select 操作就是快照读,即不加锁的非阻塞读;快照读的前提是隔离级别不是串行级别,串行级别下的快照读会退化成当前读;之所以出现快照读的情况,是基于提高并发性能的考虑,快照读的实现是基于多版本并发控制,即 MVCC ,可以认为 MVCC 是行锁的一个变种,但它在很多情况下,避免了加锁操作,降低了开销;既然是基于多版本,即快照读可能读到的并不一定是数据的最新版本,而有可能是之前的历史版本

    MVCC 能解决什么问题,好处是?

数据库并发场景有三种,分别为:

  • 读-读:不存在任何问题,也不需要并发控制
  • 读-写:有线程安全问题,可能会造成事务隔离性问题,可能遇到脏读,幻读,不可重复读
  • 写-写:有线程安全问题,可能会存在更新丢失问题,比如第一类更新丢失,第二类更新丢失

好处

  • 在并发读写数据库时,可以做到在读操作时不用阻塞写操作,写操作也不用阻塞读操作,提高了数据库并发读写的性能
  • 同时还可以解决脏读,幻读,不可重复读等事务隔离问题,但不能解决更新丢失问题
小结

MVCC + 悲观锁
MVCC 解决读写冲突,悲观锁解决写写冲突
MVCC + 乐观锁
MVCC 解决读写冲突,乐观锁解决写写冲突

一个SQL语句的执行过程

客户端-连接器-查询缓存-分析器-优化器-执行器-存储引擎

连接器

长连接
概念

长连接是相对于短连接来说的。长连接指在一个连接上可以连续发送多个数据包,在连接保持期间,如果有数据包发送,需要双方发送链路检测包。mysql的长连接如果长期闲置,mysql会8小时后自动断开该连接。

短连接
概念

是指双方有数据交互时,就建立一个连接,数据发送完成后,则断开此连接,即每次连接只完成一项业务的发送。

短连接 VS 长连接
  1. 建立连接的过程是复杂的,在使用中尽量减少建立连接的操作,尽量使用长连接。
  2. 如果全部使用长连接,会导致Mysql内存涨的特别快,导致内存占用大,被系统强行杀掉了。
如何解决长连接
  1. 定期断开长连接
  2. 执行一个比较大的操作后,通过执行mysql_reset_connection来初始化连接资源。这个过程不需要重新连接和权限验证。(mysql 5.7以上的版本)

查询缓存

查询缓存是什么

SQL文本与查询结果的映射

缓存条件
  1. 查询SQL语句完全相同(空格和大小写严格校验)
  2. 开启查询缓存

query_cache_type 0 不使用查询缓存 1 始终使用查询缓存 2 按需使用查询缓存

1.query_cache_type值为1时,不想使用缓存中查询的数据

SELECT SQL_NO_CACHE * FROM my_table WHERE condition;

2.query_cache_type值为2,要使用缓存的话,需要使用SQL_CACHE开关参数:

SELECT SQL_CACHE * FROM my_table WHERE condition;

缓存失效时机

在表结构或者数据发生改变时,查询的数据不再有效。(INSERT、UPDATE、DELETE、ALERT等)
Mysql8.0版本将查询缓存的整个模块删除。

解析器

分为词法分析和语法分析
词法分析采用Lex词法分析器
Lex
Lex是Unix环境下非常著名的工具,主要功能是生成一个词法分析器(scanner)的C源码,描述规则采用正则表达式。

语法分析采用

yacc是一个典型的语法解析器。yacc生成的编译器主要是用C语言写成的语法解析器(Parser),需要与词法解析器Lex一起使用,再把两部份产生出来的C程序一并编译。

select id, name from table1 where id=1

语句被解析为

  1. sql_command = SQLCOM_SELECT

  2. where子句 :select_lex->where

  3. table列表:select_lex->table_list

  4. 字段列表:select_lex->item_list

  5. table_list 保存表名

  6. where
    where
    |–>FUNC_ITEM
    |–>FIELD_ITEM(“id”)
    |–>INT_ITEM(1)

  7. item_list

    item_list:
    |–>Item_field(“id”)
    |–>Item_field(“name”)

优化器

优化步骤
  1. 根据语法树及统计统计,构建初始表访问数组(init_plan_arrays)
  2. 根据表访问数组,计算每个表的最佳访问路径(find_best_ref),同时保存当前最优执行计划(COST最小)
  3. 如果找到更优的执行计划则更新最优执行计划,否则优化结束。
举例

select * from table1 join table2 using(id) where table1.c = 10 and table2.d = 20;

两种方案

  1. 从表1里面取出c=10的记录的ID值,再根据ID值关联到t2,再判断t2里面的d值是否等于20。
  2. 也可以先从表2中取出记录d=20的ID值,再根据ID值关联到t1,在判断t1的c值是否等于10。

InnoDB与MyISAM区别

地址:https://blog.csdn.net/qq_45076180/article/details/115111803

MyISAM存储引擎

MyISAM无论是主键索引还是非主键索引都是非聚集索引(叶子节点不包含所有数据记录),索引文件和数据文件是分离的,跨文件查询速度比较慢。

InnoDB存储引擎

InnoDB的主键索引就是聚集索引(叶子节点包含了完整的数据记录,查询速度较快),聚集索引在innoDB表中只有一个。

InnoDB的非主键索引就是非聚集索引(叶子节点存储的是主键的id,用于回表查询,回表查询导致速度较慢)
非主键索引包括 二级索引、复合索引 等等

InnoDB与MyISAM的区别

  1. InnoDB 支持事务,MyISAM 不支持。这是 MySQL 将默认存储引擎从 MyISAM 变成 InnoDB 的重要原因之一
  2. InnoDB 最小的锁粒度是行锁,MyISAM 最小的锁粒度是表锁。个更新语句会锁住整张表,导致其他查询和更新都会被阻塞,因此并发访问受限
  3. InnoDB 是聚集索引,MyISAM 是非聚集索引
  4. InnoDB 支持外键, MyISAM 不支持
  5. InnoDB 不保存表的具体行数,执行 select count(*) from table 时需要全表扫描。而MyISAM 用一个变量保存了整个表的行数,执行上述语句时只需要读出该变量即可,速度很快

聚簇索引和非聚簇索引

  1. 非聚簇索引

    • Myisam 索引与数据的关系
    • Myisam 索引指向行所在磁盘的位置
    • 数据都有自己的地址
    • 数据和索引相互独立
  2. 聚簇索引

    • 主键索引 既存索引值,又在叶子中存储行的数据
    • 如果没有主键(primary key),则会Unique key做主键
    • 如果没有unique,则系统生成一个内部的rowid做主键
    • 像innodb中,主键的索引结构中既存储了主键值,又存储了行数据的这样的结构c称为“聚簇索引”
  3. 聚簇索引和非聚簇索引的优缺点

    优势:根据主键查询条目比较少,不用回行(数据就在主键节点下)
    劣势: 如果碰到不规则数据插入时会造成频繁的页分裂。

####索引覆盖
索引覆盖是指查询的列恰好是索引的一部分,那么查询只需要在索引文件上进行,不需要回行到磁盘在查找数据,这样查询速度非常快

理想的索引

  1. 查询频繁
  2. 区分度高
  3. 长度小
  4. 尽可能覆盖常用字段

Mysql建表、列选择注意点

建表原则

  1. 定长与变长相分离
  2. 常用字段和不常用字段要分离
  3. 1对多,需要关联统计的字段上,添加冗余字段(空间和时间上的转换)

列选择原则

  1. 字段类型优先级 整形 > date,time > enum,char > varchar > blob,text

    time 定长,运算快,节省空间,考虑时区,写sql时不方便 where > ‘2005-10-12’;
    enum 能起到约束值的目的,内部用整形来存储
    char 定长 需要考虑字符集和(排序校对集)
    varchar 不定长 要考虑字符集的转换与排序时校对集,速度慢
    text/Blob 无法使用内存临时表(排序等操作只能在磁盘上进行)

  2. 够用就行,不要慷慨

    原因:大的字段浪费内存,影响速度
    以年龄为例,tinyint unsigned not null 可以存储255岁,足够使用 ,用int浪费了3个字节
    以varchar(10) 和 varchar(300) 存储的内容相同,但是在表的联查上varchar(300)要花费更多的内存。

  3. 尽量避免使用NULL

    原因:NULL 不利于索引 要用特殊字节来标注

架构

分布式系统CAP理论

概念

一个分布式系统最多只能同时满足一致性(Consistency)、可用性(Availability)和分区容错性(Partition tolerance)这三项中的两项

定义

Consistency 一致性

一致性是因为多个数据拷贝下并发读写才有的问题,因此理解时一定要注意结合考虑多个数据拷贝下并发读写的场景。

Availability 可用性
可用性指“Reads and writes always succeed”,即服务在正常响应时间内一直可用。
好的可用性主要是指系统能够很好的为用户服务,不出现用户操作失败或者访问超时等用户体验不好的情况。可用性通常情况下可用性和分布式数据冗余,负载均衡等有着很大的关联。

Partition Tolerance分区容错性
分区容错性指“the system continues to operate despite arbitrary message loss or failure of part of the system”,即分布式系统在遇到某节点或网络分区故障的时候,仍然能够对外提供满足一致性或可用性的服务。

CAP权衡

  1. CA without P:如果不要求P(不允许分区),则C(强一致性)和A(可用性)是可以保证的。但其实分区不是你想不想的问题,而是始终会存在,因此CA的系统更多的是允许分区后各子系统依然保持CA。
  2. CP without A:如果不要求A(可用),相当于每个请求都需要在Server之间强一致,而P(分区)会导致同步时间无限延长,如此CP也是可以保证的。很多传统的数据库分布式事务都属于这种模式。
  3. AP wihtout C:要高可用并允许分区,则需放弃一致性。一旦分区发生,节点之间可能会失去联系,为了高可用,每个节点只能用本地数据提供服务,而这样会导致全局数据的不一致性。现在众多的NoSQL都属于此类。

Raft算法详解

Raft算法概述

将一致性分解为多个子问题:Leader选举(Leader election)、日志同步(Log replication)、安全性(Safety)、日志压缩(Log compaction)、成员变更(Membership change。
Raft将系统中的角色分为领导者(Leader)、跟从者(Follower)和候选人(Candidate):

Leader:接受客户端请求,并向Follower同步请求日志,当日志同步到大多数节点上后告诉Follower提交日志。
Follower:接受并持久化Leader同步的日志,在Leader告之日志可以提交之后,提交日志。
Candidate:Leader选举过程中的临时角色。

Raft要求系统在任意时刻最多只有一个Leader,正常工作期间只有Leader和Followers。

Raft算法角色状态转换如下

Leader选举

Raft 使用心跳(heartbeat)触发Leader选举。当服务器启动时,初始化为Follower。Leader向所有Followers周期性发送heartbeat。如果Follower在选举超时时间内没有收到Leader的heartbeat,就会等待一段随机的时间后发起一次Leader选举。
Follower将其当前term加一然后转换为Candidate。它首先给自己投票并且给集群中的其他服务器发送 RequestVote RPC (RPC细节参见八、Raft算法总结)。结果有以下三种情况:

  • 赢得了多数的选票,成功选举为Leader;
  • 收到了Leader的消息,表示有其它服务器已经抢先当选了Leader;
  • 没有服务器赢得多数的选票,Leader选举失败,等待选举时间超时后发起下一次选举。

日志同步

Leader选出后,就开始接收客户端的请求。Leader把请求作为日志条目(Log entries)加入到它的日志中,然后并行的向其他服务器发起 AppendEntries RPC (RPC细节参见八、Raft算法总结)复制日志条目。当这条日志被复制到大多数服务器上,Leader将这条日志应用到它的状态机并向客户端返回执行结果。

某些Followers可能没有成功的复制日志,Leader会无限的重试 AppendEntries RPC直到所有的Followers最终存储了所有的日志条目。

日志由有序编号(log index)的日志条目组成。每个日志条目包含它被创建时的任期号(term),和用于状态机执行的命令。如果一个日志条目被复制到大多数服务器上,就被认为可以提交(commit)了。

分布式锁

基于Redis的实现方式

1、选用Redis实现分布式锁原因:

  1. Redis有很高的性能;
  2. Redis命令对此支持较好,实现起来比较方便

2、使用命令介绍:

  1. SETNX
  2. EXPIRE
  3. DELETE
    在使用Redis实现分布式锁的时候,主要就会使用到这三个命令。

3、实现思想:

  1. 获取锁的时候,使用setnx加锁,并使用expire命令为锁添加一个超时时间,超过该时间则自动释放锁,锁的value值为一个随机生成的UUID,通过此在释放锁的时候进行判断。
  2. 获取锁的时候还设置一个获取的超时时间,若超过这个时间则放弃获取锁。
  3. 释放锁的时候,通过UUID判断是不是该锁,若是该锁,则执行delete进行锁释放

基于数据库的实现方式

基于数据库的实现方式的核心思想是
在数据库中创建一个表,表中包含方法名等字段,并在方法名字段上创建唯一索引,想要执行某个方法,就使用这个方法名向表中插入数据,成功插入则获取锁,执行完成后删除对应的行数据释放锁。
使用基于数据库的这种实现方式很简单,但是对于分布式锁应该具备的条件来说,它有一些问题需要解决及优化:

  1. 因为是基于数据库实现的,数据库的可用性和性能将直接影响分布式锁的可用性及性能,所以,数据库需要双机部署、数据同步、主备切换;
  2. 不具备可重入的特性,因为同一个线程在释放锁之前,行数据一直存在,无法再次成功插入数据,所以,需要在表中新增一列,用于记录当前获取到锁的机器和线程信息,在再次获取锁的时候,先查询表中机器和线程信息是否和当前机器和线程相同,若相同则直接获取锁;
  3. 没有锁失效机制,因为有可能出现成功插入数据后,服务器宕机了,对应的数据没有被删除,当服务恢复后一直获取不到锁,所以,需要在表中新增一列,用于记录失效时间,并且需要有定时任务清除这些失效的数据;
  4. 不具备阻塞锁特性,获取不到锁直接返回失败,所以需要优化获取逻辑,循环多次去获取。
  5. 在实施的过程中会遇到各种不同的问题,为了解决这些问题,实现方式将会越来越复杂;依赖数据库需要一定的资源开销,性能问题需要考虑。

基于ZooKeeper的实现方式

ZooKeeper是一个为分布式应用提供一致性服务的开源组件,它内部是一个分层的文件系统目录树结构,规定同一个目录下只能有一个唯一文件名。基于ZooKeeper实现分布式锁的步骤如下:

  1. 创建一个目录mylock;
  2. 线程A想获取锁就在mylock目录下创建临时顺序节点;
  3. 获取mylock目录下所有的子节点,然后获取比自己小的兄弟节点,如果不存在,则说明当前线程顺序号最小,获得锁;
  4. 线程B获取所有节点,判断自己不是最小节点,设置监听比自己次小的节点;
  5. 线程A处理完,删除自己的节点,线程B监听到变更事件,判断自己是不是最小的节点,如果是则获得锁。

优点:具备高可用、可重入、阻塞锁特性,可解决失效死锁问题。
缺点:因为需要频繁的创建和删除节点,性能上不如Redis方式。

高并发下接口幂等性解决方案

概念

在编程中.一个幂等操作的特点是其任意多次执行所产生的影响均与一次执行的影响相同。幂等函数,或幂等方法,是指可以使用相同参数重复执行,并能获得相同结果的函数。这些函数不会影响系统状态,也不用担心重复执行会对系统造成改变。例如,“getUsername()和setTrue()”函数就是一个幂等函数. 更复杂的操作幂等保证是利用唯一交易号(流水号)实现.

幂等性场景

  1. 查询操作:查询一次和查询多次,在数据不变的情况下,查询结果是一样的。select是天然的幂等操作;

  2. 删除操作:删除操作也是幂等的,删除一次和多次删除都是把数据删除。(注意可能返回结果不一样,删除的数据不存在,返回0,删除的数据多条,返回结果多个) ;

  3. 唯一索引:防止新增脏数据。比如:支付宝的资金账户,支付宝也有用户账户,每个用户只能有一个资金账户,怎么防止给用户创建资金账户多个,那么给资金账户表中的用户ID加唯一索引,所以一个用户新增成功一个资金账户记录。要点:唯一索引或唯一组合索引来防止新增数据存在脏数据(当表存在唯一索引,并发时新增报错时,再查询一次就可以了,数据应该已经存在了,返回结果即可);

  4. token机制:防止页面重复提交。

    1. 原理上通过session token来实现的(也可以通过redis来实现)。当客户端请求页面时,服务器会生成一个随机数Token,并且将Token放置到session当中,然后将Token发给客户端(一般通过构造hidden表单)。
    2. 下次客户端提交请求时,Token会随着表单一起提交到服务器端。
    3. 服务器端第一次验证相同过后,会将session中的Token值更新下,若用户重复提交,第二次的验证判断将失败,因为用户提交的表单中的Token没变,但服务器端session中Token已经改变了。
  5. 悲观锁

    1. 获取数据的时候加锁获取。select * from table_xxx where id=’xxx’ for update; 注意:id字段一定是主键或者唯一索引,不然是锁表,会死人的;悲观锁使用时一般伴随事务一起使用,数据锁定时间可能会很长,根据实际情况选用;
  6. 乐观锁——乐观锁只是在更新数据那一刻锁表,其他时间不锁表,所以相对于悲观锁,效率更高。乐观锁的实现方式多种多样可以通过version或者其他状态条件:

    1. 通过版本号实现update table_xxx set name=#name#,version=version+1 where version=#version#如下图(来自网上);
    2. 通过条件限制 update table_xxx set avai_amount=avai_amount-#subAmount# where avai_amount-#subAmount# >= 0要求:quality-#subQuality# >= ,这个情景适合不用版本号,只更新是做数据安全校验,适合库存模型,扣份额和回滚份额,性能更高;
  7. 分布式锁

    1. 如果是分布是系统,构建全局唯一索引比较困难,例如唯一性的字段没法确定,这时候可以引入分布式锁,通过第三方的系统(redis或zookeeper),在业务系统插入数据或者更新数据,获取分布式锁,然后做操作,之后释放锁,这样其实是把多线程并发的锁的思路,引入多多个系统,也就是分布式系统中得解决思路。要点:某个长流程处理过程要求不能并发执行,可以在流程执行之前根据某个标志(用户ID+后缀等)获取分布式锁,其他流程执行时获取锁就会失败,也就是同一时间该流程只能有一个能执行成功,执行完成后,释放分布式锁(分布式锁要第三方系统提供);
  8. select + insert

    1. 并发不高的后台系统,或者一些任务JOB,为了支持幂等,支持重复执行,简单的处理方法是,先查询下一些关键数据,判断是否已经执行过,在进行业务处理,就可以了。注意:核心高并发流程不要用这种方法;
  9. 状态机幂等

    1. 在设计单据相关的业务,或者是任务相关的业务,肯定会涉及到状态机(状态变更图),就是业务单据上面有个状态,状态在不同的情况下会发生变更,一般情况下存在有限状态机,这时候,如果状态机已经处于下一个状态,这时候来了一个上一个状态的变更,理论上是不能够变更的,这样的话,保证了有限状态机的幂等。注意:订单等单据类业务,存在很长的状态流转,一定要深刻理解状态机,对业务系统设计能力提高有很大帮助
  10. 对外提供接口的api如何保证幂等

    1. 如银联提供的付款接口:需要接入商户提交付款请求时附带:source来源,seq序列号;source+seq在数据库里面做唯一索引,防止多次付款(并发时,只能处理一个请求) 。
    2. 重点:对外提供接口为了支持幂等调用,接口有两个字段必须传,一个是来源source,一个是来源方序列号seq,这个两个字段在提供方系统里面做联合唯一索引,这样当第三方调用时,先在本方系统里面查询一下,是否已经处理过,返回相应处理结果;没有处理过,进行相应处理,返回结果。注意,为了幂等友好,一定要先查询一下,是否处理过该笔业务,不查询直接插入业务系统,会报错,但实际已经处理了。

网络基础

网络基础

TCP

简述

  1. 面向连接
  2. 可靠的传输协议

浏览器访问 www.baidu.com 的过程

  1. 浏览器向DNS服务器发出解析域名的请求;
  2. DNS服务器将”www.baidu.com"域名解析为对应的IP地址,并返回给浏览器;
  3. 浏览器与百度服务器进行三次握手,建立TCP连接;
  4. 浏览器发出HTTP请求报文;
  5. 服务器回复HTTP响应报文;
  6. 浏览器解析响应报文,渲染HTML内容,并显示在页面上;
  7. 收发报文结束,释放TCP连接,执行四次挥手。

TCP三次握手 建立连接

在TCP/IP协议中,TCP协议提供可靠的连接服务,采用三次握手建立一个连接.

第一次握手:Client将标志位SYN置为1,随机产生一个值seq=J,并将该数据包发送给Server,Client进入SYN_SENT状态,等待Server确认。

SYN:同步序列编号(Synchronize Sequence Numbers)

第二次握手:Server收到数据包后由标志位SYN=1知道Client请求建立连接,Server将标志位SYN和ACK都置为1,ack=J+1,随机产生一个值seq=K,并将该数据包发送给Client以确认连接请求,Server进入SYN_RCVD状态。

第三次握手:Client收到确认后,检查ack是否为J+1,ACK是否为1,如果正确则将标志位ACK置为1,ack=K+1,并将该数据包发送给Server,Server检查ack是否为K+1,ACK是否为1,如果正确则连接建立成功,Client和Server进入ESTABLISHED状态,完成三次握手,随后Client与Server之间可以开始传输数据了。

TCP四次挥手

1.第一次挥手:Client发送一个FIN,用来关闭Client到Server的数据传送,Client进入FIN_WAIT_1状态。

2.第二次挥手:Server收到FIN后,发送一个ACK给Client,确认序号为收到序号+1(与SYN相同,一个FIN占用一个序号),Server进入CLOSE_WAIT状态。

3.第三次挥手:Server发送一个FIN,用来关闭Server到Client的数据传送,Server进入LAST_ACK状态。

4.第四次挥手:Client收到FIN后,Client进入TIME_WAIT状态,接着发送一个ACK给Server,确认序号为收到序号+1, Server进入CLOSED状态,完成四次挥手。

SOCKET

TCP滑动窗口

滑动窗口
tcp通过滑动窗口进行流量控制,所谓的窗口可以理解为接收端所能提供的缓冲区大小。

TCP是一个滑动窗口协议,即一个TCP连接的发送端在某个时刻能发多少数据是由滑动窗口控制的

滑动窗口示意图

RTT(Round trip time)
表示从发送端到接收端的一去一回需要的时间。

TCP在数据传输过程中会对RTT进行采样(即对发送的数据包及其ACK的时间差进行测量,并根据测量值更新RTT值)

RTO (Retransmission TimeOut)
发送数据包,启动重传定时器,重传定时器到期所花费的时间

TCP根据得到的RTT值更新RTO值,即Retransmission TimeOut,就是重传间隔,发送端对每个发出的数据包进行计时,如果在RTO时间内没有收到所发出的数据包的对应ACK,则任务数据包丢失,将重传数据。一般RTO值都比采样得到的RTT值要大。

在面对未知的流量暴增,可以预先怎么处理

大致为以下两种情况
  1. 不可预测流量(网站被恶意刷量;CDN回源抓取数据;合作业务平台调取平台数据等)
  2. 可预测流量(突然爆发的社会热点,营销活动的宣传;)

防止流量暴涨的预备方案

  1. 通过压测,进行流量预估,流量基本上要在压测结果,压测得到的结果要达到设计流量 * 3(*4, * 5都可以)
  2. 降级方案,需要考虑业务场景,采用不同的降级方案,不能随意在业务主流程进行
  3. 限流方案:计数器、滑动窗口、漏桶

计数器


计数器是一种比较简单的限流算法,用途比较广泛,在接口层面,很多地方使用这种方式限流。在一段时间内,进行计数,与阀值进行比较,到了时间临界点,将计数器清0。
局限性:
这里需要注意的是,存在一个时间临界点的问题。举个栗子,在12:01:00到12:01:58这段时间内没有用户请求,然后在12:01:59这一瞬时发出100个请求,OK,然后在12:02:00这一瞬时又发出了100个请求。这里你应该能感受到,在这个临界点可能会承受恶意用户的大量请求,甚至超出系统预期的承受。

滑动窗口

由于计数器存在临界点缺陷,后来出现了滑动窗口算法来解决

局限性:
滑动窗口的意思是说把固定时间片,进行划分,并且随着时间的流逝,进行移动,这样就巧妙的避开了计数器的临界点问题。也就是说这些固定数量的可以移动的格子,将会进行计数判断阀值,因此格子的数量影响着滑动窗口算法的精度

漏桶

虽然滑动窗口有效避免了时间临界点的问题,但是依然有时间片的概念,而漏桶算法在这方面比滑动窗口而言,更加先进。
有一个固定的桶,进水的速率是不确定的,但是出水的速率是恒定的,当水满的时候是会溢出的。

局限性:

生成令牌的速度是恒定的,而请求去拿令牌是没有速度限制的。这意味,面对瞬时大流量,该算法可以在短时间内请求拿到大量令牌,而且拿令牌的过程并不是消耗很大的事情。(有一点生产令牌,消费令牌的意味)
不论是对于令牌桶拿不到令牌被拒绝,还是漏桶的水满了溢出,都是为了保证大部分流量的正常使用,而牺牲掉了少部分流量,这是合理的,如果因为极少部分流量需要保证的话,那么就可能导致系统达到极限而挂掉,得不偿失。

IO 多路复用

同步阻塞

select,poll,epoll都是IO多路复用的机制。I/O多路复用就是通过一种机制,一个进程可以监视多个描述符,一旦某个描述符就绪(一般是读就绪或者写就绪),能够通知程序进行相应的读写操作。但select,poll,epoll本质上都是同步I/O,因为他们都需要在读写事件就绪后自己负责进行读写,也就是说这个读写过程是阻塞的,而异步I/O则无需自己负责进行读写,异步I/O的实现会负责把数据从内核拷贝到用户空间。

Select、Poll与Epoll区别

\ select poll epoll
支持最大连接数 1024(x86) or 2048(x64) 无上限 无上限
IO效率 每次调用进行线性遍历,时间复杂度为O(N) 每次调用进行线性遍历,时间复杂度为O(N)
使用“事件”通知方式,每当fd就绪,系统注册的回调函数就会被调用,将就绪fd放到rdllist里面,这样epoll_wait返回的时候我们就拿到了就绪的fd。时间发复杂度O(1)
fd拷贝 每次select都拷贝 每次poll都拷贝
调用epoll_ctl时拷贝进内核并由内核保存,之后每次epoll_wait不拷贝

Linux IO模型

网络IO的本质是socket的读取,socket在linux系统被抽象为流,IO可以理解为对流的操作。
常见的IO模型有阻塞、非阻塞、IO多路复用,异步

同步阻塞IO

同步阻塞 IO 模型是最常用的一个模型,也是最简单的模型。在linux中,默认情况下所有的socket都是blocking。它符合人们最常见的思考逻辑。阻塞就是进程 “被” 休息, CPU处理其它进程去了。

同步非堵塞IO

同步非阻塞就是 “每隔一会儿瞄一眼进度条” 的轮询(polling)方式。

对比同步阻塞IO
优点:能够在等待任务完成的时间里干其他活了(包括提交其他任务,也就是 “后台” 可以有多个任务在同时执行)。
缺点:任务完成的响应延迟增大了,因为每过一段时间才去轮询一次read操作,而任务可能在两次轮询之间的任意时间完成。这会导致整体数据吞吐量的降低。

进程

一个在内存中运行的应用程序。每个进程都有自己独立的一块内存空间,一个进程可以有多个线程,比如在Windows系统中,一个运行的xx.exe就是一个进程。

线程

进程中的一个执行任务(控制单元),负责当前进程中程序的执行。一个进程至少有一个线程,一个进程可以运行多个线程,多个线程可共享数据。

与进程不同的是同类的多个线程共享进程的堆和方法区资源,但每个线程有自己的程序计数器、虚拟机栈和本地方法栈,所以系统在产生一个线程,或是在各个线程之间作切换工作时,负担要比进程小得多,也正因为如此,线程也被称为轻量级进程。

进程与线程的区别总结

线程具有许多传统进程所具有的特征,故又称为轻型进程(Light—Weight Process)或进程元;而把传统的进程称为重型进程(Heavy—Weight Process),它相当于只有一个线程的任务。在引入了线程的操作系统中,通常一个进程都有若干个线程,至少包含一个线程。

根本区别:进程是操作系统资源分配的基本单位,而线程是处理器任务调度和执行的基本单位

资源开销:每个进程都有独立的代码和数据空间(程序上下文),程序之间的切换会有较大的开销;线程可以看做轻量级的进程,同一类线程共享代码和数据空间,每个线程都有自己独立的运行栈和程序计数器(PC),线程之间切换的开销小。

包含关系:如果一个进程内有多个线程,则执行过程不是一条线的,而是多条线(线程)共同完成的;线程是进程的一部分,所以线程也被称为轻权进程或者轻量级进程。

内存分配:同一进程的线程共享本进程的地址空间和资源,而进程之间的地址空间和资源是相互独立的

影响关系:一个进程崩溃后,在保护模式下不会对其他进程产生影响,但是一个线程崩溃整个进程都死掉。所以多进程要比多线程健壮。

执行过程:每个独立的进程有程序运行的入口、顺序执行序列和程序出口。但是线程不能独立执行,必须依存在应用程序中,由应用程序提供多个线程执行控制,两者均可并发执行

CPU密集型和IO密集型

CPU密集型

CPU密集型也叫计算密集型,指的是系统的硬盘、内存性能相对CPU要好很多,此时,系统运作CPU读写IO(硬盘/内存)时,IO可以在很短的时间内完成,而CPU还有许多运算要处理,因此,CPU负载很高。

CPU密集表示该任务需要大量的运算,而没有阻塞,CPU一直全速运行。CPU密集任务只有在真正的多核CPU上才可能得到加速(通过多线程),而在单核CPU上,无论你开几个模拟的多线程该任务都不可能得到加速,因为CPU总的运算能力就只有这么多。

CPU使用率较高(例如:计算圆周率、对视频进行高清解码、矩阵运算等情况)的情况下,通常,线程数只需要设置为CPU核心数的线程个数就可以了。 这一情况多出现在一些业务复杂的计算和逻辑处理过程中。比如说,现在的一些机器学习和深度学习的模型训练和推理任务,包含了大量的矩阵运算。

IO密集型

IO密集型指的是系统的CPU性能相对硬盘、内存要好很多,此时,系统运作,大部分的状况是CPU在等IO (硬盘/内存) 的读写操作,因此,CPU负载并不高。

密集型的程序一般在达到性能极限时,CPU占用率仍然较低。这可能是因为任务本身需要大量I/O操作,而程序的逻辑做得不是很好,没有充分利用处理器能力。

CPU 使用率较低,程序中会存在大量的 I/O 操作占用时间,导致线程空余时间很多,通常就需要开CPU核心数数倍的线程。

其计算公式为:IO密集型核心线程数 = CPU核数 / (1-阻塞系数)。

当线程进行 I/O 操作 CPU 空闲时,启用其他线程继续使用 CPU,以提高 CPU 的使用率。例如:数据库交互,文件上传下载,网络传输等。

CPU密集型与IO密集型任务的使用说明

当线程等待时间所占比例越高,需要越多线程,启用其他线程继续使用CPU,以此提高CPU的利用率;
当线程CPU时间所占比例越高,需要越少的线程,通常线程数和CPU核数一致即可,这一类型在开发中主要出现在一些计算业务频繁的逻辑中。

CPU密集型任务与IO密集型任务的区别

计算密集型任务的特点是要进行大量的计算,消耗CPU资源,全靠CPU的运算能力。这种计算密集型任务虽然也可以用多任务完成,但是任务越多,花在任务切换的时间就越多,CPU执行任务的效率就越低,所以,要最高效地利用CPU,计算密集型任务同时进行的数量应当等于CPU的核心数,避免线程或进程的切换。

计算密集型任务由于主要消耗CPU资源,因此,代码运行效率至关重要。Python这样的脚本语言运行效率很低,完全不适合计算密集型任务。对于计算密集型任务,最好用C语言编写。
IO密集型任务的特点是CPU消耗很少,任务的大部分时间都在等待IO操作完成(因为IO的速度远远低于CPU和内存的速度)。涉及到网络、磁盘IO的任务都是IO密集型任务,

对于IO密集型任务,线程数越多,CPU效率越高,但也有一个限度。

总结

一个计算为主的应用程序(CPU密集型程序),多线程或多进程跑的时候,可以充分利用起所有的 CPU 核心数,比如说16核的CPU ,开16个线程的时候,可以同时跑16个线程的运算任务,此时是最大效率。但是如果线程数/进程数远远超出 CPU 核心数量,反而会使得任务效率下降,因为频繁的切换线程或进程也是要消耗时间的。因此对于 CPU 密集型的任务来说,线程数/进程数等于 CPU 数是最好的了。
如果是一个磁盘或网络为主的应用程序(IO密集型程序),一个线程处在 IO 等待的时候,另一个线程还可以在 CPU 里面跑,有时候 CPU 闲着没事干,所有的线程都在等着 IO,这时候他们就是同时的了,而单线程的话,此时还是在一个一个等待的。我们都知道IO的速度比

https原理

HTTP与HTTPS有什么区别?

HTTPS和HTTP的区别主要如下:

  1. https协议需要到ca申请证书,一般免费证书较少,因而需要一定费用。
  2. http是超文本传输协议,信息是明文传输,https则是具有安全性的ssl加密传输协议。
  3. http和https使用的是完全不同的连接方式,用的端口也不一样,前者是80,后者是443。
  4. http的连接很简单,是无状态的;HTTPS协议是由SSL+HTTP协议构建的可进行加密传输、身份认证的网络协议,比http协议安全。

非对称加密方案

  1. 某网站服务器拥有公钥A与对应的私钥A’;浏览器拥有公钥B与对应的私钥B’。
  2. 浏览器把公钥B明文传输给服务器。
  3. 服务器把公钥A明文给传输浏览器。
  4. 之后浏览器向服务器传输的内容都用公钥A加密,服务器收到后用私钥A’解密。由于只有服务器拥有私钥A’,所以能保证这条数据的安全。
  5. 同理,服务器向浏览器传输的内容都用公钥B加密,浏览器收到后用私钥B’解密。同上也可以保证这条数据的安全。

数字签名

数字签名的制作过程:

  1. CA机构拥有非对称加密的私钥和公钥。
  2. CA机构对证书明文数据T进行hash。
  3. 对hash后的值用私钥加密,得到数字签名S。

浏览器验证过程:

  1. 拿到证书,得到明文T,签名S。
  2. 用CA机构的公钥对S解密(由于是浏览器信任的机构,所以浏览器保有它的公钥。详情见下文),得到S’。
  3. 用证书里指明的hash算法对明文T进行hash得到T’。
  4. 显然通过以上步骤,T’应当等于S‘,除非明文或签名被篡改。所以此时比较S’是否等于T’,等于则表明证书可信。

制作数字签名时需要hash一次?

最显然的是性能问题,前面我们已经说了非对称加密效率较差,证书信息一般较长,比较耗时。而hash后得到的是固定长度的信息(比如用md5算法hash后可以得到固定的128位的值),这样加解密就快很多。

每次进行HTTPS请求时都必须在SSL/TLS层(http的安全层)进行握手传输密钥吗?

服务器会为每个浏览器(或客户端软件)维护一个session ID,在TLS握手阶段传给浏览器,浏览器生成好密钥传给服务器后,服务器会把该密钥存到相应的session ID下,之后浏览器每次请求都会携带session ID,服务器会根据session ID找到相应的密钥并进行解密加密操作,这样就不必要每次重新制作、传输密钥了!

Linux根据文件路径查找索引节点

查找时,会遍历路径的过程中,会逐层地将各个路径组成部分解析成目录项对象。如果此目录项对象在目录项缓存中,则直接从缓存中获取;如果该目录项在缓存中不存在,则进行一次实际的读盘操作,从磁盘中读取该目录项所对应的索引节点。得到索引节点之后,则建立索引节点与该目录项的联系。如此循环,直到找到目标文件对于的目录项,也就找到了索引节点,而由索引节点找到对应的超级块对象,就可知道该文件所在的文件系统的类型。

进程创建时文件的复制与共享

当一个进程系统调用fork()创建一个子进程时,fork()将调用内核函数do_fork()对父进程的进程控制块进行复制,并将这个副本作为子进程的控制块。如果父进程有已经打开的文件,那么子进程理所当然的按某种方式来继承这些文件

算法

算法

二叉树

L、D、R分别表示遍历左子树、访问根结点和遍历右子树

  • 先序遍历:DLR
  • 中序遍历:LDR
  • 后序遍历:LRD

仅有前序和后序遍历,不能确定一个二叉树,必须有中序遍历的结果

二叉树的性质

  • 性质1:在二叉树中第 i 层的结点数最多为 2^i-1 (i ≥ 1)
  • 性质2:高度为k的二叉树其结点总数最多为 2^k -1 (k ≥ 1)
  • 性质3:对任意的非空二叉树 T ,如果叶结点的个数为n0,而其度为 2 的结点数为 n2,则: n0 = n2 + 1

满二叉树

深度为k,且有 2^k -1 个节点称之为 满二叉树;

  • 性质4:第i层上的节点数为 2^i -1 ;

完全二叉树

深度为k,有n个节点的二叉树,当且仅当其每一个节点都与深度为k的满二叉树中,序号为1至n的节点对应时,称之为完全二叉树。

  • 性质5:对于具有n个结点的完全二叉树的高度为 log2^n +1:

平衡二叉树

B-Tree

红黑树每个节点上只存一个数据,导致大数据量时高度太高,B-Tree为了优化数的高度,如图所示:每一层树高上存储多个节点,节点中的数据索引从左到右递增排列,这样每个节点区间(数据页)内又可以向下延伸新的节点区间(数据页)。这样每一层都可以放更多的索引元素,有效的降低了树的高度,B-Tree具有以下特点:

  • 节点中的数据索引从左到右递增排列
  • 所有索引节点上都存储数据,所有索引节点不重复

B+Tree


B+Tree 作为 B-Tree的变种,有以下特点

  • 非叶子节点不存储data,只存储索引(冗余),可以放更多的索引
  • 叶子节点包含所有索引字段
  • 节点中的数据索引从左到右递增排列,叶子节点用指针连接,提高区间访问的性能
  • B+Tree在查询数据时,也是从上往下查询的,首先第一次磁盘IO把B+Tree的第一层数据加载到内存中,然后通过算法找到找个要查的这个数据位于第一层的哪个区间(数据页),然后再进行一次磁盘IO把这个区间加载到内存,到这个区间中去找,以此类推。。。最终可找到想要的数据!mysql正是使用了B+Tree的数据结构,才可以支撑千万级的数据。

二叉树(先序、中序、后序遍历)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
package main

import "fmt"

type Tree struct {
No int
Left *Tree
Right *Tree
}

func PreTree(node *Tree) {
if node != nil {
fmt.Printf("no:%d \n", node.No)
PreTree(node.Left)
PreTree(node.Right)
}
}

func InfixTree(node *Tree) {
if node != nil {
InfixTree(node.Left)
fmt.Printf("no:%d \n", node.No)
InfixTree(node.Right)
}
}

func AfterTree(node *Tree) {
if node != nil {
AfterTree(node.Left)
AfterTree(node.Right)
fmt.Printf("no:%d \n", node.No)
}
}

func buildTree(nums []int, l int, r int) *Tree {
if l > r {
return nil
}
mid := (l + r) >> 1
node := &Tree{nums[mid], nil, nil}

node.Left = buildTree(nums, l, mid-1)
node.Right = buildTree(nums, mid+1, r)
return node
}

func main() {
nums := []int{1, 2, 3}
root := buildTree(nums, 0, len(nums)-1)
fmt.Print(root.Left.No)
fmt.Print(root.Right.No)
//PreTree(root)
AfterTree(root)
}

堆通常是一个可以被看做一棵树的数组对象。堆的实现通过构造二叉堆(binary heap),实为二叉树的一种;

  • 任意节点小于(或大于)它的所有后裔,最小元(或最大元)在堆的根上(堆序性)。
  • 堆总是一棵完全树。即除了最底层,其他层的节点都被元素填满,且最底层尽可能地从左到右填入。
    将根节点最大的堆叫做最大堆或大根堆,根节点最小的堆叫做最小堆或小根堆。常见的堆有二叉堆、斐波那契堆等。

通常堆是通过一维数组来实现的。在数组起始位置为1的情形中:

  • 父节点i的左子节点在位置 2 ×i ;
  • 父节点i的右子节点在位置 2×i+1 ;
  • 子节点i的父节点在位置 i÷2 ;

LRU缓存机制

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
package main

import "fmt"

type Node struct {
pre *Node
next *Node
key int
val int
}

type lruCache struct {
cap int
headNode *Node
tailNode *Node
nodeMap map[int]*Node
}

func (l *lruCache) get(k int) int {
node := l.nodeMap[k]
if node == nil {
return -1
}
headNode := l.headNode

//将节点node的前驱结点和后继节点连接起来
node.pre.next = node.next
node.next.pre = node.pre

headNode.next.pre = node
node.next = headNode.next
headNode.next = node
node.pre = headNode

v := node.val
return v
}

func (l *lruCache) set(k, v int) {
node := l.nodeMap[k]
if node == nil {
if len(l.nodeMap) == l.cap {
lastNode := l.tailNode.pre
lastNode.pre.next = l.tailNode
l.tailNode.pre = lastNode.pre
lastNode.pre = nil
lastNode.next = nil
deleteKey := lastNode.key
delete(l.nodeMap, deleteKey)
}

newNode := &Node{
pre: l.headNode,
next: l.headNode.next,
key: k,
val: v,
}
l.headNode.next = newNode
l.headNode.next.pre = newNode
l.nodeMap[k] = newNode
} else {
node.val = v
//摘除node
node.pre.next = node.next
node.next.pre = node.pre

l.headNode.next.pre = node
l.headNode.next = node

node.pre = l.headNode
node.next = l.headNode.next
}
}

func main() {
head := &Node{
pre: nil,
next: nil,
key: 0,
val: 0,
}
tail := &Node{
pre: nil,
next: nil,
key: 0,
val: 0,
}
head.next = tail
tail.pre = head
lru := lruCache{
cap: 2,
headNode: head,
tailNode: tail,
nodeMap: make(map[int]*Node),
}
lru.set(1, 1)
lru.set(2, 2)
re := lru.get(1)
fmt.Println(re) // 1
lru.set(3, 3)
re = lru.get(2)
fmt.Println(re) // -1
re = lru.get(3)
fmt.Println(re) // 3
lru.set(4, 4)
re = lru.get(1)
fmt.Println(re) // -1
re = lru.get(3)
fmt.Println(re) // 3
re = lru.get(4)
fmt.Println(re) // 4
}

经典链表
https://www.cnblogs.com/huangliang-hb/p/10855558.html

如何发现代码的坏味道之SLAP原则

Long Method (代码的坏味道)

  • 可读性很差
  • 复用性差
  • 难以调试
  • 难以维护
  • 冗余代码多

SLAP(单一抽象原则)

定义

SLAP 是 Single Level of Abstraction 的缩写。解释:指定代码块上的代码应该在单一的抽象层上。

SLAP优势

  • 短方法的提取产生,会使得方法更加具有原子性,职责更加单一,更加的符合Unix的哲学 Do one thing, and do it well。
  • 短方法的复用性更强,使得编码更加便捷
  • 短方法可读性更强,更加便于理解
  • 实践表明,SLAP应用后,维护成本应该是降低的。

举例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
<?php 
class User {
public static function getUserInfo() {
return [
'email' => '786188095@qq.com',
'password' => '232321312312'
];
}
}

function validateUser($user_info) {
$preg_email='/^[a-zA-Z0-9]+([-_.][a-zA-Z0-9]+)*@([a-zA-Z0-9]+[-.])+([a-z]{2,5})$/ims';
if (!preg_match($preg_email,$user_info['email'])) {
return false;
}

if (strlen($user_info['password']) <= 6) {
return false;
}

//验证用户名 不包含特殊符号

//验证银行卡号

//验证银行卡开户行信息

//以此类推 方法越来越长

}

validateUser(User::getUserInfo());

代码存在的问题

  • validateUser 方法中暴露了校验email和密码的具体实现
  • validateUser 方法应该只关心校验结果(第一层抽象),而不是具体实现(第二层抽象)
  • validateUser 违反了SLAP原则

修改后

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
<?php 
class User {
public static function getUserInfo() {
return [
'email' => '786188095@qq.com',
'password' => '232321312312'
];
}
}

class UserValidator {
protected static $preg_email = '/^[a-zA-Z0-9]+([-_.][a-zA-Z0-9]+)*@([a-zA-Z0-9]+[-.])+([a-z]{2,5})$/ims';

public static function validateEmail($email) {
if (!preg_match(self::$preg_email,$email)) {
return false;
}
return true;
}

public static function validatePassWord($password) {
if (strlen($password) <= 6) {
return false;
}
return true;
}
}

function vailUserInfo($user_info) {
return UserValidator::validateEmail($user_info['email']) && UserValidator::validatePassWord($user_info['password']);
}


var_dump(vailUserInfo(User::getUserInfo()));

Linux IO模型

#概念说明

  • 用户空间和内核空间
  • 进程切换
  • 进程堵塞
  • 文件描述符
  • 缓存IO

用户空间和内核空间

操作系统的核心是内核,独立于普通应用程序,可以访问受保护的内存空间,也有访问底层硬件设备的所有权限。为了保证用户进程不能直接操作内核(kernel),保存内核的安全,操作系统将虚拟空间划分为两部分,一部分是内核空间,一部分为用户空间。

进程切换

为了控制进程的执行,内核必须有能力挂起正在CPU上运行的进程,并恢复以前挂起的某个进程的执行。这种行为被称为进程切换。

进程堵塞

正在执行的进程,由于期待的某些事件未发生,如请求系统资源失败、等待某种操作的完成、新数据尚未到达或无新工作做等,则由系统自动执行阻塞原语(Block),使自己由运行状态变为阻塞状态。当进程进入阻塞状态,是不占用CPU资源的。

文件描述符fd

文件描述符(File descriptor)是计算机科学中的一个术语,是一个用于表述指向文件的引用的抽象化概念

缓存IO

缓存IO又被称作标准IO,大部分文件系统默认的IO操作都是缓存IO。数据会被先拷贝到操作系统内核的缓冲区中,然后才会从操作系统内核的缓冲区拷贝到应用程序的地址空间。

Linux IO模型

网络IO的本质是socket的读取,socket在linux系统被抽象为流,IO可以理解为对流的操作。
常见的IO模型有阻塞、非阻塞、IO多路复用,异步

同步阻塞IO

同步阻塞 IO 模型是最常用的一个模型,也是最简单的模型。在linux中,默认情况下所有的socket都是blocking。它符合人们最常见的思考逻辑。阻塞就是进程 “被” 休息, CPU处理其它进程去了。

同步非堵塞IO

同步非阻塞就是 “每隔一会儿瞄一眼进度条” 的轮询(polling)方式。

对比同步阻塞IO
优点:能够在等待任务完成的时间里干其他活了(包括提交其他任务,也就是 “后台” 可以有多个任务在同时执行)。
缺点:任务完成的响应延迟增大了,因为每过一段时间才去轮询一次read操作,而任务可能在两次轮询之间的任意时间完成。这会导致整体数据吞吐量的降低。

IO多路复用

IO多路复用是指内核一旦发现进程指定的一个或者多个IO条件准备读取,它就通知该进程。
当需要同时处理多个客户端接入请求时,可以利用多线程或者IO多路复用技术进行处理。IO多路复用的最大优势就是系统开销小,系统不需要额外创建进程或者线程,也不需要维护这些进程和线程的运行,降底了系统的维护工作量,节省了系统资源。

异步非阻塞 IO

相对于同步IO,异步IO不是顺序执行。用户进程进行aio_read系统调用之后,无论内核数据是否准备好,都会直接返回给用户进程,然后用户态进程可以去做别的事情。等到socket数据准备好了,内核直接复制数据给进程,然后从内核向进程发送通知。IO两个阶段,进程都是非阻塞的