极客时间《架构师训练营》第八周学习笔记
数据结构与算法
空间复杂度和时间复杂度
算法的优劣主要从算法的执行时间和所需要占用的存储空间两个方面衡量。
时间复杂度是一个函数,它定性描述该算法的运行时间。时间复杂度常用大 O 符号表述,一般不包括这个函数的低阶项和首项系数。使用这种方式时,时间复杂度可被称为是渐近的,亦即考察输入值大小趋近无穷(n 表示)时的情况。
空间复杂度是对一个算法在运行过程中临时占用存储空间大小的量度,它也是问题规模 n 的函数。
数据结构
数组:线性表的一种,一块连续的内存区,因此元素的访问时间复杂度是 O(1),但是插入数据和删除数据操作复杂,还有越界的风险。
链表:也是线性表,但在内存中是分散的,访问时间复杂度是 O(n),插入数据和删除数据操作简便,而且链表长度几乎没有限制(不超过内存空间的情况下)
Hash 表:哈希表是根据键(Key)而直接访问在内存储存位置的数据结构。它通过计算一个关于键值的函数,将所需查询的数据映射到表中一个位置来访问记录,从而加速了查找速度。这个映射函数称做散列函数,存放记录的数组称做散列表。
栈:也是一种线性表结构,实现上可以是数组也可以是链表。它规定了数据存取的规则后进先出——被插入的数据会放到线性表头部,读取时也是从线性表的头部开始。
队列:队列的实现结构和栈一样,但是它的存取规则是先进先出——被插入的数据被放到线性表尾部,而读取的时候总是从线性表的头部开始。
-
树:是一种抽象数据类型,用来模拟具有树状结构性质的数据集合。它是由 n 个有限节点组成一个具有层次关系的集合。把它叫做“树”是因为它看起来像一棵倒挂的树,也就是说它是根朝上,而叶朝下的。
二叉查找树(BST):运用了二分查找的思想,查找所需的最大次数等于树的高度;但是有缺点:可能会成为单边的二叉树,搜索时间复杂度为 O(n)
平衡二叉树(AVL):是对 BST 的改进,任一节点对应的两棵子树的最大高度差为 1;但是为了自平衡,插入数据时需要不断自旋调整,效率较低
红黑树:上面两数的 trade-off,自平衡的二叉查找树,能保证根到叶子的最长路劲不会超过最短路径的 2 倍;又能在插入数据时,保证最多只需 3 次旋转即能平衡
B+树:通常用于数据库和操作系统的文件系统中。B+树中的节点通常被表示为一组有序的元素和子指针;因此它是能够保持数据稳定有序,其插入与修改拥有较稳定的对数时间复杂度。
跳表:是一个随机化的数据结构,实质就是一种可以进行二分查找的有序链表。跳表在原有的有序链表上面增加了多级索引,通过索引来实现快速查找。跳表不仅能提高搜索性能,同时也可以提高插入和删除操作的性能。
算法
-
递归:是指一种通过重复将问题分解为同类的子问题而解决问题的方法。常用来解决的问题有:
- 数据的定义是按递归定义的。如费氏数列。
- 问题解法按递归算法实现。如汉诺塔。
- 数据的结构形式是按递归定义的。如二叉树、广义表等。
贪心:是一种在每一步选择中都采取在当前状态下最好或最优(即最有利)的选择,从而希望导致结果是最好或最优的算法。对于大部分的问题,贪心法通常都不能找出最佳解,因为他们一般没有测试所有可能的解。贪心法容易过早做决定,因而没法达到最佳解。不过也有例外,在某些图论上可以得到最优解,如著名的 Prim 算法、Kruskal 算法均为贪心算法。
动态规划:通过把原问题分解为相对简单的子问题的方式求解复杂问题的方法。动态规划在查找有很多重叠子问题的情况的最优解时有效。它将问题重新组合成子问题。为了避免多次解决这些子问题,它们的结果都逐渐被计算并被保存,从简单的问题直到整个问题都被解决。著名的动态规划算法有:最长公共子序列、Floyd-Warshall 算法、Viterbi 算法等等。
遗传算法:最初是借鉴了进化生物学中的一些现象而发展起来的搜索算法,这些现象包括遗传、突变、自然选择以及杂交等等。遗传算法通常实现方式为一种计算机模拟。对于一个最优化问题,一定数量的候选解(称为个体)可抽象表示为染色体,使种群向更好的解进化。遗传算法擅长解决的问题是全局最优化问题,例如,解决时间表安排问题就是它的一个特长,很多安排时间表的软件都使用遗传算法,遗传算法还经常被用于解决实际工程问题。
网络通信
OSI 七层模型 v.s. TCP/IP 四层模型
网络传输控制协议有两种最基本的模型——OSI 和 TCP/IP 协议。如图所示:两者都是标准模型,相比之下 OSI 更为严谨详实;但是经过多年斗争,TCP/IP 胜出,因为简单粗暴才更能被广泛应用。
下面是 TPC/IP 各层协议,我们常听见的专业术语其实都是 TCP/IP 里某一层的协议:
模型 | 协议 |
---|---|
应用层 | HTTP, DNS, SSH, RPC, Telnet, FTP, SMTP |
传输层 | TCP, UDP |
网络层 | IP, ICMP, ARP |
数据链路层 | Ethernet, Fast Eth, Token Ring, FDDI |
HTTP 协议
应用层中最最常用是 HTTP 协议;它是一种在客户端和服务器之间编码和传输数据的方法,一个请求/响应协议:客户端和服务端针对相关内容和完成状态信息的请求和响应。HTTP 是独立的,允许请求和响应流经许多执行负载均衡,缓存,加密和压缩的中间路由器和服务器。
一个基本的 HTTP 请求由一个动词(方法)和一个资源(端点)组成。 以下是常见的 HTTP 动词:
动词 | 描述 | 幂等 | 安全性 | 可缓存 |
---|---|---|---|---|
GET | 读取资源 | Yes | Yes | Yes |
POST | 创建资源或触发处理数据的进程 | No | No | Yes,如果回应包含刷新信息 |
PUT | 创建或替换资源 | Yes | No | No |
PATCH | 部分更新资源 | No | No | Yes,如果回应包含刷新信息 |
DELETE | 删除资源 | Yes | No | No |
此外还有两个动词Trace
和Options
用于测试或诊断,小众需求。
HTTP(HyperText Transfer Protocol)从 1989 年万维网诞生之日起就经历了众多版本迭代,早期经典版本有 1991 年出的 HTTP0.9 和 1996 年的 HTTP1.0,但是现在很少用到了。我自己最经常用的事实上是 HTTP1.1——用 Postman 调试 RESTFUL API 时能用到。
HTTP1.1 相比于 1.0 版本默认启用长连接,客户端可以不用等待上一个请求响应结果就可以发送下一个请求了。但是天生缺陷:队头阻塞、无状态、明文传输、不支持推送等等。
HTTPS 在 HTTP 协议下多加了一层加密协议——ssl/tls 协议。现在主流的 Web 前后端通讯基本都用 HTTPS 了。
HTTP2 是基于 google 的 SPDY 协议推行的改进版——多路复用、头部压缩、支持服务端推送、加密传输。
HTTP3 相比于之前的版本就是另起炉灶的另一种协议了。它基于的底层协议不是 TCP 而是 UDP,改进了之前的拥塞控制,提升了多路复用和连接迁移;但是落地可能仍需一定时日。
TCP
上面提到 HTTP 是依赖于传输层协议 TCP 和 UDP 的应用层协议。我们再来看一下更低层的 TCP 协议。TCP 是通过 IP 网络的面向连接的协议。使用握手建立和断开连接。
-
TCP 建立连接就是经典的三次握手,过程如下:
- 客户端向服务器发起一个 SYN 包
- 服务器端返回对应的 SYN 的 ACK 响应以及新的 SYN 包
- 然后客户端返回对应的 ACK
-
断开连接是四次挥手:
- 客户端发送一个 FIN 段,并包含一个希望接收者看到的自己当前的序列号 K. 同时还包含一个 ACK 表示确认对方最近一次发过来的数据
- 服务端将 K 值加 1 作为 ACK 序号值,表明收到了上一个包。这时上层的应用程序会被告知另一端发起了关闭操作,通常这将引起应用程序发起自己的关闭操作
- 服务端发起自己的 FIN 段,ACK=K+1, Seq=L
- 客户端确认,ACK=L+1
IP
IP 是 Internet Protocol(网际互连协议)的缩写,是 TCP/IP 体系中的网络层协议,可将 IP 信息包从源设备传送到目的设备。IP 协议依赖于 IP 地址(网络中唯一的地址)和路由(传送方式)两种机制。我们最常用的负载均衡——IP 负载均衡,就是通过 IP 地址分发流量到不同的服务器中。
网络编程
Socket
回顾了 TCP/IP 模型,那计算机之间如何进行网络请求呢?用的是软件手段——Socket。
Socket 是应用层与传输层(TCP、UDP)之间的一个软件抽象层,它是一组接口。在设计模式中,Socket 其实就是一个门面模式,它把复杂的 TCP/IP 协议族隐藏在 Socket 接口后面,对用户来说,一组简单的接口就是全部,让 Socket 去组织数据,以符合指定的协议。
Socket 通信的过程如下:
- 服务器端先初始化 Socket,然后与端口绑定(
bind
),对端口进行监听(listen
),调用accept
阻塞,等待客户端连接 - 客户端初始化一个 Socket,然后连接(
connect
)服务器;如果成功,客户端与服务器端的连接就建立了 - 客户端发送数据请求,服务器端接收请求并处理请求,然后把回应数据发送给客户端,客户端读取数据
- 最后关闭连接,一次交互结束
Java IO
Socket 是操作系统提供给用户的网络编程接口,但是对开发来说还是过于底层。一般我们调用的是它的进一步封装,如 Java IO。
Java IO 是 JDK java.net
下面的一组 API,调用方法如下:
- ServerSocket 创建并监听某端口
- 客户端请求连接,Socket 就返回
accept()
, 然后它自己就 block 住了 - 之后客户端和服务器就可以互发数据了
由于 Socket 接受连接后就被 block 住了,假如你想支持并发,只能建线程池。不过线程数量有限,无法突破数量上限。
Java NIO
然后就有了改进版的 Java NIO:
Java NIO 知识点很多——Channel、BUffer、Selector、Selection Key。篇幅有限就不一一列举了,只说重点。Java NIO 可以提高并发量(或是不阻塞),关键点在 Selector 上:虽然和上述的 accept()
阻塞一样,调 select()
时也会阻塞;但是它可以注册多个 channel,使得单个线程可以 handle 多个连接,这样就可以应对更高的并发量了。
数据库原理与性能优化
数据库架构
连接器:为每个请求分配一块专用的内存空间用于会话上下文。应用启动时通常会创建一个连接池用于加速数据库连接。
语法分析器:检测 SQL 语法是否正确
语义分析与优化:将常见的复杂嵌套语句转化成更高效的索引语句,但是种类比较有限吧
执行引擎:生成执行计划,可以用
explain
查看执行计划包含的信息,一般包括 Id、type、key,rows 等等信息。
索引
在几百万行的数据库里查找记录,需要全表扫描,效率很低。我们可以选择建索引提速。索引事实上就是创建一个 B+ 树(上文数据结构篇里提到了),在 O(log n)
时间内找到节点。
事务
- 原子性(Atomicity):每个事务内部所有操作要么全部完成,要么全部不完成
- 一致性(Consistency ):任何事务都使数据库从一个有效的状态转换到另一个有效状态
- 隔离性(Isolation):并发执行事务的结果与顺序执行事务的结果相同
- 持久性(Durability ):事务提交后,对系统的影响是永久的
日志
MySql 日志类型 | 解析说明 |
---|---|
错误日志(error log) | 当数据库启动、运行、停止时产生该日志 |
普通查询日志(general query log) | 客户端连接数据库执行语句时产生该日志 |
二进制日志(binary log) | 当数据库内容发生改变时产生该日志,也被用来实现主从复制功能 |
中继日志(relay log) | 从库上收到主库的数据更新时产生该日志 |
慢查询日志(show query log) | SQL 语句在数据库查询超过指定时间时产生该日志 |
DDL 日志(metadata log) | 执行 DDL 语句操作元数据时产生该日志 |
小结
本周内容还是有点多的,光本文罗列的字数都超过 6000 了。个人觉得数据结构这部分有点乱入的感觉,还是放在第一二周的基础篇比较合适。网络通讯这块收获比较大:学生时代对各种协议还很陌生,上课如听天书(虽然考试依旧高分);工作了些年,那些术语总算或多或少有点听说了,这次能从 TCP/IP 分层结构中串联起来,还是挺满足的。DB 这块,内容有点少,希望听到更多的调优手段,或是防注入策略(这块面试题不少吧)。