1、Kafka 是什么?主要应用场景有哪些?
Kafka 是一个分布式流式处理平台。这到底是什么意思呢?
流平台具有三个关键功能:
- 消息队列:发布和订阅消息流,这个功能类似于消息队列,这也是 Kafka 也被归类为消
息队列的原因。
- 容错的持久方式存储记录消息流: Kafka 会把消息持久化到磁盘,有效避免了消息丢失
的风险。
- 流式处理平台: 在消息发布的时候进行处理,Kafka 提供了一个完整的流式处理类库。
Kafka 主要有两大应用场景:
-
消息队列 :建立实时流数据管道,以可靠地在系统或应用程序之间获取数据。
-
数据处理: 构建实时的流数据处理程序来转换或处理数据流
- Kafka 可以将主题划分为多个分区(Partition),会根据分区规则选择把消息存储到哪个
分区中,只要分区规则设置的合理,那么所有的消息将会被均匀的分布到不同的分区中,
这样就实现了负载均衡和水平扩展。另外,多个订阅者可以从一个或者多个分区中同时消
费数据,以支撑海量数据处理能力。
- producer 只需要关心消息发往哪个 topic,而 consumer 只关心自己订阅哪个 topic,
并不关心每条消息存于整个集群的哪个 broker。 为了性能考虑,如果 topic 内的消息
只存于一个 broker,那这个 broker 会成为瓶颈,无法做到水平扩展。所以把 topic 内
的数据分布到整个集群就是一个自然而然的设计方式。
65535 。
服务器的 ip ,端口号 ,客户端的 ip 都是确定的。 能变的只有客户端的端口号。
加网卡 ,保证四元组唯一,理论上能是客户端和服务器之间建立 10 万以上的连接 。
-
多个线程同时操作一个 hashmap 就可能出现不安全的情况。
-
如果两个线程同时遇到 HashMap 的大小达到 12 的倍数时,就很有可能会出现在将
oldTable 转移到 newTable 的过程中遇到问题,从而导致最终的 HashMap 的值存储
异常。
- 构造 entry<K,V>单链表时,也会出现不安全的情况。
单列索引
- 普通索引:MySQL 中基本索引类型,没有什么限制,允许在定义索引的列中插入重复值
和空值,纯粹为了查询数据更快一点。
-
唯一索引:索引列中的值必须是唯一的,但是允许为空值,
-
主键索引:是一种特殊的唯一索引,不允许有空值。
组合索引:
多个字段组合上创建的索引,只有在查询条件中使用了这些字段的左边字段时,索引才会被使
用,使用组合索引时遵循最左前缀集合。
全文索引:
只有在 MyISAM 引擎上才能使用,只能在 CHAR,VARCHAR,TEXT 类型字段上使用全文
索引,介绍了要求,说说什么是全文索引,就是在一堆文字中,通过其中的某个关键字等,就
能找到该字段所属的记录行,比如有"你是个靓仔,靓女 …" 通过靓仔,可能就可以找到该条
记录
空间索引:
空间索引是对空间数据类型的字段建立的索引,MySQL 中的空间数据类型有四种,
GEOMETRY、POINT、LINESTRING、POLYGON。在创建空间索引时,使用 SPATIAL 关
键字。要求,引擎为 MyISAM,创建空间索引的列,必须将其声明为 NOT NULL。
操作系统中可以拥有多个进程,一个进程里可以拥有多个线程,线程在进程内执行
进程和线程的区别
-
容易创建新线程。创建新进程需要重复父进程
-
线程可以控制同一进程的其他线程。进程无法控制兄弟进程,只能控制其子进程
-
进程拥有自己的内存空间。线程使用进程的内存空间,且要和该进程的其他线程共享这个
空间;而不是在进程中给每个线程单独划分一点空间。
-
(同一进程中的)线程在共享内存空间中运行,而进程在不同的内存空间中运行
-
线程可以使用 wait(),notify(),notifyAll()等方法直接与其他线程(同一进程)
通信;而,进程需要使用“进程间通信”(IPC)来与操作系统中的其他进程通信。
- 管道( pipe ):管道是一种半双工的通信方式,数据只能单向流动,而且只能在具有亲缘
关系的进程间使用。进程的亲缘关系通常是指父子进程关系。
- 有名管道 (named pipe) : 有名管道也是半双工的通信方式,但是它允许无亲缘关系进
程间的通信。
- 信号量( semophore ) : 信号量是一个计数器,可以用来控制多个进程对共享资源的访
问。它常作为一种锁机制,防止某进程正在访问共享资源时,其他进程也访问该资源。因
此,主要作为进程间以及同一进程内不同线程之间的同步手段。
- 消息队列( message queue ) : 消息队列是由消息的链表,存放在内核中并由消息队列
标识符标识。消息队列克服了信号传递信息少、管道只能承载无格式字节流以及缓冲区大
小受限等缺点。
-
信号 ( sinal ) : 信号是一种比较复杂的通信方式,用于通知接收进程某个事件已经发生。
-
共享内存( shared memory ) :共享内存就是映射一段能被其他进程所访问的内存,这段
共享内存由一个进程创建,但多个进程都可以访问。共享内存是最快的 IPC 方式,它是针
对其他进程间通信方式运行效率低而专门设计的。它往往与其他通信机制,如信号两,配
合使用,来实现进程间的同步和通信。
- 套接字( socket ) : 套解口也是一种进程间通信机制,与其他通信机制不同的是,它可用
于不同及其间的进程通信。
对于一个定时任务,如果当前任务已经被某一个服务器处理后,另外一个服务器就不需要执行 这个任务了
-
在定时任务里加锁机制,等某台服务器获取权限,其他服务器将不再执行此次定时任务。
-
在数据库的创建定时任务控制表 job_controller,创建 updated_by 字段,用来存放执
行代码的服务器生成的序列号。创建 updateTime 字段,用于记录标记更新 update_by
的时间戳,也可以理解为上一次任务执行的时间戳。
- 在代码层面,在执行任务的时候,首先生成一个序列号,然后将序列号存储在当前任务的
记录上。然后再从数据库里查询当前记录的序列号,在做标记前,首先检查当前任务的上
一次执行时间离当前时间超过阈值(自己定义),如果超过则表明还没有其他节点执行该
任务,然后为 task 保存标签和当前运行时间。当然如果上一次运行时间为空的情况下,
也是允许标记的,如果数据库中的序列号与当前节点生成序列号相匹配,则执行任务的具
体逻辑,反之,则什么都不做处理。
-
基于数据库实现分布式锁
-
基于缓存实现分布式锁
-
基于 Zookeeper 实现分布式锁
-
set px nx
-
守护线程,进行 renew
-
Redis 分布式锁实现: 先拿 setnx 来争抢锁,抢到之后,再用 expire(过期)给锁加一个
过期时间防止锁忘记了释放。
- 如果在 setnx 之后执行 expire 之前进程意外 crash 或者要重启维护了,那会怎么样:
set 指令有非常复杂的参数,这个应该是可以同时把 setnx 和 expire 合成一条指令来用
的。
11、Redis 的数据类型及它们的使用场景?
string
- key/value; 二进制安全的。意思是 redis 的 string 可以包含任何数据。比如 jpg 图片
或者序列化的对象 。一个键最大能存储 512MB。
hash
- 存储对象数据
list : 简单的字符串列表
关注列表
- 队列
set: string 类型的无序集合
共同关注列表
- 统计独立 IP
zset : (sorted set:有序集合),每个元素都会关联一个 double 类型的分数。redis 正是通
过分数来为集合中的成员进行从小到大的排序。
排行
- 带权重的消息队列
- 信号:(signal)是一种处理异步事件的方式。信号是比较复杂的通信方式,用于通知接
受进程有某种事件发生,除了用于进程外,还可以发送信号给进程本身。
- 信号量:(Semaphore)进程间通信处理同步互斥的机制。是在多线程环境下使用的一
种设施, 它负责协调各个线程, 以保证它们能够正确、合理的使用公共资源。
简单地说,信号就是一种异步通信,通知进程某种事件的发生;信号量是进程/线程同步与互
斥的一种机制,保证进程/线程间之间的有序执行或对公共资源的有序访问。
select:支持阻塞操作的设备驱动通常会实现一组自身的等待队列如读/写等待队列用于支持
上层(用户层)所需的 BLOCK 或 NonBLOCK 操作。当应用程序通过设备驱动访问该设备时
(默认为 BLOCK 操作),若该设备当前没有数据可读或写,则将该用户进程插入到该设备驱
动对应的读/写等待队列让其睡眠一段时间,等到有数据可读/写时再将该进程唤醒。
select 就是巧妙的利用等待队列机制让用户进程适当在没有资源可读/写时睡眠,有资源可读/
写时唤醒。
epoll:epoll 由三个系统调用组成,分别是 epoll_create,epoll_ctl 和 epoll_wait。
epoll_create 用于创建和初始化一些内部使用的数据结构;epoll_ctl 用于添加,删除或者修
改指定的 fd 及其期待的事件,epoll_wait 就是用于等待任何先前指定的 fd 事件。
- 最容易想到的方法是将数据全部排序,然后在排序后的集合中进行查找,最快的排序算法
的时间复杂度一般为 O(nlogn),如快速排序。
- 局部淘汰法,该方法与排序方法类似,用一个容器保存前 10000 个数,然后将剩余的所
有数字——与容器内的最小数字相比,如果所有后续的元素都比容器内的 10000 个数还
小,那么容器内这个 10000 个数就是最大 10000 个数。如果某一后续元素比容器内最
小数字大,则删掉容器内最小元素,并将该元素插入容器,最后遍历完这 1 亿个数,得
到的结果容器中保存的数即为最终结果了。此时的时间复杂度为 O(n+m^2),其中 m
为容器的大小,即 10000。
- 分治法,将 1 亿个数据分成 100 份,每份 100 万个数据,找到每份数据中最大的
10000 个,最后在剩下的 10010000 个数据里面找出最大的 10000 个。如果 100
万数据选择足够理想,那么可以过滤掉 1 亿数据里面 99%的数据。100 万个数据里面
查找最大的 10000 个数据的方法如下:用快速排序的方法,将数据分为 2 堆,如果大
的那堆个数 N 大于 10000 个,继续对大堆快速排序一次分成 2 堆,如果大的那堆个
数 N 大于 10000 个,继续对大堆快速排序一次分成 2 堆,如果大堆个数 N 小于
10000 个,就在小的那堆里面快速排序一次,找第 10000-n 大的数字;递归以上过程,
就可以找到第 1w 大的数。参考上面的找出第 1w 大数字,就可以类似的方法找到前
10000 大数字了。此种方法需要每次的内存空间为 10^64=4MB,一共需要 101 次这
样的比较。
- Hash 法,如果这 1 亿个书里面有很多重复的数,先通过 Hash 法,把这 1 亿个数字
去重复,这样如果重复率很高的话,会减少很大的内存用量,从而缩小运算空间,然后通
过分治法或最小堆法查找最大的 10000 个数。
- 采用最小堆法,首先读入前 10000 个数来创建大小为 10000 的最小堆,建堆的时间复
杂度为 O(mlogm)(m 为数组的大小即为 10000),然后遍历后续的数字,并于堆
顶(最小)数字进行比较。如果比最小的数小,则继续读取后续数字;如果比堆顶数字大,
则替换堆顶元素并重新调整堆为最小堆。整个过程直至 1 亿个数全部遍历完为止。然后
按照中序遍历的方式输出当前堆中的所有 10000 个数字。该算法的时间复杂度为 O
(nmlogm),空间复杂度是 10000(常数)。
生产者丢失消息的情况
生产者(Producer) 调用 send 方法发送消息之后,消息可能因为网络问题并没有发送过去。
为了确定消息是发送成功,我们要判断消息发送的结果,Kafka 生产者(Producer) 使用
send 方法发送消息实际上是异步的操作,我们可以通过 get()方法获取调用结果,但是这样
也让它变为了同步操作,可以采用为其添加回调函数的形式,示例代码如下:
ListenableFuture<SendResult<String, Object>> future = kafkaTemplate.send(topic, o);
future.addCallback(result -> logger.info(“生产者成功发送消息到 topic:{} partition:{}的消息”,
result.getRecordmetadata().topic(), result.getRecordmetadata().partition()),
ex -> logger.error(“生产者发送消失败,原因:{}”, ex.getMessage()));
Producer 的 retries**(重试次数)设置一个比较合理的值,一般是 3 ,但是为了保证消息不**
丢失的话一般会设置比较大一点。设置完成之后,当出现网络问题之后能够自动重试消息发送,
避免消息丢失。另外,建议还要设置重试间隔,因为间隔太小的话重试的效果就不明显了,网
络波动一次你 3 次一下子就重试完了
消费者丢失消息的情况
当消费者拉取到了分区的某个消息之后,消费者会自动提交了 offset。自动提交的话会有一个
问题,试想一下,当消费者刚拿到这个消息准备进行真正消费的时候,突然挂掉了,消息实际
上并没有被消费,但是 offset 却被自动提交了。
解决办法也比较粗暴,我们手动关闭自动提交 offset,每次在真正消费完消息之后再自己手
动提交 offset 。 但是,细心的朋友一定会发现,这样会带来消息被重新消费的问题。比如你
刚刚消费完消息之后,还没提交 offset,结果自己挂掉了,那么这个消息理论上就会被消费两
次。
Kafka 弄丢了消息
试想一种情况:假如 leader 副本所在的 broker 突然挂掉,那么就要从 follower 副本重新选
出一个 leader ,但是 leader 的数据还有一些没有被 follower 副本的同步的话,就会造成消
息丢失。
当我们配置了 unclean.leader.election.enable = false 的话,当 leader 副本发生故障时就
不会从 follower 副本中和 leader 同步程度达不到要求的副本中选择出 leader ,这样降低了
消息丢失的可能性。
消息队列在实际应用中包括如下四个场景:
- 应用耦合:多应用间通过消息队列对同一消息进行处理,避免调用接口失败导致整个过程
失败;
- 异步处理:多应用对消息队列中同一消息进行处理,应用间并发处理消息,相比串行处理,
减少处理时间;
-
限流削峰:广泛应用于秒杀或抢购活动中,避免流量过大导致应用系统挂掉的情况;
-
消息驱动的系统:系统分为消息队列、消息生产者、消息消费者,生产者负责产生消息,
消费者(可能有多个)负责对消息进行处理;
悲观锁:
总是假设最坏的情况,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会
上锁,这样别人想拿这个数据就会阻塞直到它拿到锁。传统的关系型数据库里边就用到了很多
这种锁机制,比如行锁,表锁等,读锁,写锁等,都是在做操作之前先上锁。再比如 Java 里
面的同步原语 synchronized 关键字的实现也是悲观锁。
乐观锁:
顾名思义,就是很乐观,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更
新的时候会判断一下在此期间别人有没有去更新这个数据,可以使用版本号等机制。乐观锁适
用于多读的应用类型,这样可以提高吞吐量,像数据库提供的类似于 write_condition 机制,
其实都是提供的乐观锁。在 Java 中 java.util.concurrent.atomic 包下面的原子变量类就是
使用了乐观锁的一种实现方式 CAS 实现的。
- 数据结构实现:ArrayList :基于数组,便于按 index 访问,超过数组需要扩容,扩容成
本较高。linkedList:使用链表实现,无需扩容。
- 随机访问效率:ArrayList 比 linkedList 在随机访问的时候效率要高,因为 linkedList
是线性的数据存储方式,所以需要移动指针从前往后依次查找。
- 增加和删除效率:在非首尾的增删操作,linkedList 要比 ArrayList 效率要高,因为
ArrayList 增删操作要影响数组内的其他数据的下标。
- 内存空间占用:linkedList 比 ArrayList 更占内存,因为 linkedList 的节点除了存储数
据,还存储了两个引用,一个指向前一个元素,一个指向后一个元素。
-
线程安全:ArrayList 和 linkList 都是不同步的,不保证线程安全。
-
综合来说,需要频繁读取集合中的元素时,更推荐使用 Arrayist,而在增删操作较多时,
更推荐使用 linkedList。
- linkedList 的双向链表是链表的一种,它的每个数据结点中都有 2 个指针,分别指向直
接后继和直接前驱。所以,从双向链表中的任意一个结点开始,都可以很方便的访问它的
前驱结点和后继结点。
所谓 SQL 注入式攻击,就是攻击者把 SQL 命令插入到 Web 表单的输入域或页面请求的
查询字符串,欺骗服务器执行恶意的 SQL 命令。
如何防范 SQL 注入式攻击?
在利用表单输入的内容构造 SQL 命令之前,把所有输入内容过滤一番就可以了。过滤输入内
容可以按多种方式进行。
- 对于动态构造 SQL 查询的场合
a. 替换单引号,即把所有单独出现的单引号改成两个单引号,防止攻击者修改 SQL 命令的含
义。
b. 删除用户输入内容中的所有连字符
c. 对于用来执行查询的数据库帐户,限制其权限**。**用不同的用户帐户执行查询、插入、更新、
删除操作。
-
用存储过程来执行所有的查询。
-
限制表单或查询字符串输入的长度。
-
检查用户输入的合法性。
-
将用户登录名称、密码等数据加密保存。
-
检查提取数据的查询所返回的记录数量。
-
原子性:即不可分割性,事务要么全部被执行,要么就全部不被执行。
-
一致性或可串性。事务的执行使得数据库从一种正确状态转换成另一种正确状态
-
隔离性。在事务正确提交之前,不允许把该事务对数据的任何改变提供给任何其他事务,
-
持久性。事务正确提交后,其结果将永久保存在数据库中,即使在事务提交后有了其他故
障,事务的处理结果也会得到保存
尽可能使用散列表(hashes),散列表(是说散列表里面存储的数少)使用的内存非常小,
所以你应该尽可能的将你的数据模型抽象到一个散列表里面。比如你的 web 系统中有一个用
户对象,不要为这个用户的名称,姓氏,邮箱,密码设置单独的 key,而是应该把这个用户的
所有信息存储到一张散列表里面
缓存穿透
- 问题:大量并发查询不存在的 KEY,在缓存和数据库中都不存在,同时给缓存和数据库
带来压力。
- 原因:一般而言,缓存穿透有 2 种可能性:业务数据被误删,导致缓存和数据库中都没
有数据。恶意进行 ddos 攻击。
- 分析:为什么会多次透传呢?不存在 一直为空,需要注意让缓存能够区分 KEY 不存在和
查询到一个空值。
- 解决办法:缓存空值的 KEY,这样第一次不存在也会被加载会记录,下次拿到有这个
KEY。Bloom 过滤或 RoaingBitmap 判断 KEY 是否存在,如果布隆过滤器中没有查到
这个数据,就不去数据库中查。在处理请求前增加恶意请求检查,如果检测到是恶意攻击,
则拒绝进行服务。完全以缓存为准,使用延迟异步加载的策略(异步线程负责维护缓存的
数据,定期或根据条件触发更新),这样就不会触发更新。
缓存击穿
-
问题:某个 KEY 失效的时候,正好有大量并发请求访问这个 KEY。
-
分析:跟穿透其实很像,属于比较偶然的。
-
解决办法:KEY 的更新操作添加全局互斥锁。完全以缓存为准,使用延迟异步加载的策
略(异步线程负责维护缓存的数据,定期或根据条件触发更新),这样就不会触发更新。
缓存雪崩
- 问题:当某一时刻发生大规模的缓存失效的情况,导致大量的请求无法获取数据,从而将
流量压力传导到数据库上,导致数据库压力过大甚至宕机。
- 原因:一般而言,缓存雪崩有 2 种可能性:大量的数据同一个时间失效:比如业务关系
强相关的数据要求同时失效 Redis 宕机
- 分析:一般来说,由于更新策略、或者数据热点、缓存服务宕机等原因,可能会导致缓存
数据同一个时间点大规模不可用,或者都更新。所以,需要我们的更新策略要在时间上合
适,数据要均匀分享,缓存服务器要多台高可用。
- 解决办法:更新策略在时间上做到比较平均。如果数据需要同一时间失效,可以给这批数
据加上一些随机值,使得这批数据不要在同一个时间过期,降低数据库的压力。使用的热
数据尽量分散到不同的机器上。多台机器做主从复制或者多副本,实现高可用。做好主从
的部署,当主节点挂掉后,能快速的使用从结点顶上。实现熔断限流机制,对系统进行负
载能力控制。对于非核心功能的业务,拒绝其请求,只允许核心功能业务访问数据库获取
数据。服务降价:提供默认返回值,或简单的提示信息
23、数组和链表的区别?当数组内存过大时会出现什么问题?链表增删过多会 出现的什么问题?
-
数组静态分配内存,链表动态分配内存;
-
数组事先定义固定的长度,不能适应数据动态的增减的情况。当数据增加时,可能超出原
先定义的元素个数;当数据减少时,造成内存浪费;
-
链表动态地进行存储分配,可以适应数据动态地增减的情况
-
数组在内存中连续,链表不连续;
-
数组元素在栈区,链表元素在堆区;
-
(静态)数组从栈中分配空间,对于程序员方便快速,但是自由度小;
-
链表从堆中分配空间,自由度大但是申请管理比较麻烦。
-
数组利用下标定位,时间复杂度为 O(1),链表定位元素时间复杂度 O(n);
-
数组插入或删除元素的时间复杂度 O(n),链表的时间复杂度 O(1)。
-
当数组内存过大时会出现什么问题(堆内存溢出),链表增删过多会出现的什么问题(大
量内存碎片)
23、数组和链表的区别?当数组内存过大时会出现什么问题?链表增删过多会
出现的什么问题?
-
数组静态分配内存,链表动态分配内存;
-
数组事先定义固定的长度,不能适应数据动态的增减的情况。当数据增加时,可能超出原
先定义的元素个数;当数据减少时,造成内存浪费;
-
链表动态地进行存储分配,可以适应数据动态地增减的情况
-
数组在内存中连续,链表不连续;
-
数组元素在栈区,链表元素在堆区;
-
(静态)数组从栈中分配空间,对于程序员方便快速,但是自由度小;
-
链表从堆中分配空间,自由度大但是申请管理比较麻烦。
-
数组利用下标定位,时间复杂度为 O(1),链表定位元素时间复杂度 O(n);
-
数组插入或删除元素的时间复杂度 O(n),链表的时间复杂度 O(1)。
-
当数组内存过大时会出现什么问题(堆内存溢出),链表增删过多会出现的什么问题(大
量内存碎片)
- 冒泡排序,O(n2),通过重复走完数组的所有元素,通过两两比较,直到没有数可以交换
的时候结束这个数,再到下个数,直到整个数组排好顺序。
- 插入排序,O(n2),每次从未排好序的数据堆中拿出一个数,插入到已排好序的数据队列
的正确位置。
- 选择排序,O(n2),每次从未排好序的数据堆中找到最小的数,插入到已排好序的数据队
列的头部。
- 快速排序,O(N*logN),以数据堆中的一个数为标准,将数据堆分为小于等于和大于该数
的两堆,对于分割后的两堆数再分别利用上述方法进行分割,以此类推,直到堆中只有一
个数为止。
- 堆排序,O(N*logN),将数据堆中的数两两组队排序,对于排序好的这些子堆再两两组队
排序,以此类推,直到只剩下一个堆。
- 归并排序,O(N*logN),基于堆的排序算法,分为最大堆和最小堆。排序分为两个过程堆
的构造和堆的排序。
- 方法区(method):被所有的线程共享。方法区包含所有的类信息和静态变量。(运行时
常量池)
-
堆(heap):被所有的线程共享,存放对象实例以及数组,Java 堆是 GC 的主要区域。
-
栈(stack):每个线程包含一个栈区,栈中保存一些局部变量等。(本地局部变量、操作数
栈、动态链接、返回地址)
-
程序计数器:是当前线程执行的字节码的行指示器。
-
本地方法栈
- 索引是一种特殊的文件(InnoDB 数据表上的索引是表空间的一个组成部分),它们包含着
对数据表里所有记录的引用指针。
-
普通索引(由关键字 KEY 或 INDEX 定义的索引)的唯一任务是加快对数据的访问速度。
-
普通索引允许被索引的数据列包含重复的值。如果能确定某个数据列将只包含彼此各不相
同的值,在为这个数据列创建索引的时候就应该用关键字 UNIQUE 把它定义为一个唯一
索引。也就是说,唯一索引可以保证数据记录的唯一性。
- 主键,是一种特殊的唯一索引,在一张表中只能定义一个主键索引,主键用于唯一标识一
条记录,使用关键字 PRIMARY KEY 来创建。
-
索引可以覆盖多个数据列,如像 INDEX(columnA, columnB)索引,这就是联合索引。
-
索引可以极大的提高数据的查询速度,但是会降低插入、删除、更新表的速度,因为在执
行这些写操作时,还要操作索引文件。
阻塞 I/O, 非阻塞 I/O 模型,I/O 复用模型,信号驱动 I/O 模型 ,异步 I/O 模型。
域名解析–> 发起 TCP 的 3 次握手 –> 建立 TCP 连接后发起 http 请求 –> 服务器响应
http 请求–>浏览器得到 html 代码 –> 浏览器解析 html 代码,并请求 html 代码中的资
源(如 js、css、图片等) –> 浏览器对页面进行渲染呈现给用户 。
优点:
- 保证性能下限: 虚拟 DOM 可以经过 diff 找出最小差异,然后批量进行 patch,这种操作虽
然比不上手动优化,但是比起粗暴的 DOM 操作性能要好很多,因此虚拟 DOM 可以保证
性能下限
- 无需手动操作 DOM: 虚拟 DOM 的 diff 和 patch 都是在一次更新中自动进行的,我们
无需手动操作 DOM,极大提高开发效率
- 跨平台: 虚拟 DOM 本质上是 Javascript 对象,而 DOM 与平台强相关,相比之下虚拟
DOM 可以进行更方便地跨平台操作,例如服务器渲染、移动端开发等等
缺点:
无法进行极致优化: 在一些性能要求极高的应用中虚拟 DOM 无法进行针对性的极致优化,比
如 VScode 采用直接手动操作 DOM 的方式进行极端的性能优化。
幻读是一个事务在前后两次查询同一个范围的时候、后一次查询看到了前一次查询未看到的行。
在可重复读隔离级别下,普通的查询是快照读,是不会看到别的事务插入的数据的。因此,幻
读在“当前读”下才会出现。
SERIALIZABLE(可串行化)可以防止幻读:最高的隔离级别,完全服从 ACID 的隔离级别。