在2022年春节将至(半个月),适合在这个冬天里,温故知新。通过学习一门覆盖面较广的课程,来夯实基础,完善自己的知识体系,是一个很棒的选择。
总结性的学习,不求快,而求稳。这门课程的学习,我将跟随专栏的章节所讲,结合工作内容进行思考,记录并分享。欢迎大家对我的思考发起质疑,共同探讨。
建立起技术思维体系,掌握技术体系背后的原理,那么当你接触一个新技术的时候,就可以快速把握住这个新技术的本质特征和思路方法。
“让我自由地从物理规则出发去思考问题,而不是迎合那些所谓的世俗智慧。”
如果你掌握了软件开发技术的第一性原理,那么当你为了解决某个新问题,去学习和研究一个新技术的时候,就算遇到了知识的盲点,也可以快速定位到自己技术体系的具体位置,进一步阅读相关的书籍资料
第一性原理是一种思维方式,一种学习方式,一种围绕事物核心推动事物正确前进的做事方式
- 程序是静态的,安静地呆在磁盘上,什么也干不了
- 程序运行起来以后,被称作进程。
- 程序运行时如果需要创建数组等数据结构,操作系统就会在进程的堆空间申请一块相应的内存空间,并把这块内存的首地址信息记录在进程的栈中。
- 进程在生命周期中,主要有三种状态,运行、就绪、阻塞。
- CPU 以线程为单位进行分时共享执行
- 每个线程有自己的线程栈,所有的线程栈都是完全隔离的
- 多个线程访问共享资源的这段代码被称为临界区,解决线程安全问题的主要方法是使用锁
- 锁会引起线程阻塞,如果有很多线程同时在运行,那么就会出现线程排队等待锁的情况,线程无法并行执行,系统响应速度就会变慢。此外 I/O 操作也会引起阻塞,对数据库连接的获取也可能会引起阻塞。
- 如果阻塞的线程数超过了某个系统资源的极限,就会导致系统宕机,应用崩溃
数据结构
- 数组是最常用的数据结构,创建数组必须要内存中一块连续的空间,并且数组中必须存放相同的数据类型。
- 不同于数组必须要连续的内存空间,链表可以使用零散的内存空间存储数据。
- Hash 表的物理存储其实是一个数组,如果我们能够根据 Key 计算出数组下标,那么就可以快速在数组中查找到需要的 Key 和 Value。一旦发生 Hash 冲突,只需要将相同下标,不同 Key 的数据元素添加到这个链表就可以了
- 数组和链表都被称为线性表,因为里面的数据是按照线性组织存放的.
- 栈就是在线性表的基础上加了这样的操作限制条件:后面添加的数据,在删除的时候必须先删除,即通常所说的“后进先出”。
- 队列也是一种操作受限的线性表,栈是后进先出,而队列是先进先出。
- 树则是非线性表
为什么我们会关心O(1)、O(N)、O(nlogn)等算法复杂度,究其原因,是访问一次内存大约需要200-300个时钟周期(即比CPU慢这么多倍)。我们组织不同的数据结构,不同算法,根本原因还是要通过减少遍历来减少内存的访问。
我认为数据结构从内存的连续性可以分为两类,一类就是申请连续内存空间的数组演变的数据结构;一类是以类似链表,申请不连续的空间,通过记录下一个或上一个等关联元素内存地址来进行寻址的数据结构,例如树。
对于不同的数据结构有不同的算法进行查找、插入、删除,其所需操作内存的复杂度(时间、空间)会有不同的体现。例如连续内存空间的数据结构,如果有逻辑可以计算内存偏移,那么访问就是O(1),但是插入或删除,都会因为连续性导致其后的元素需要进行移动,就适合读多写少的场景。
我们要理解内存的访问,再去组织它的数据结构,来自己“定制”最合适的数据结构。
正如这节课老师提到的:“事实上,我很难相信,如果这些基本数据结构没有掌握好,如何能开发好一个稍微复杂一点的程序”。我想起大三的时候与同学参与比赛时共同开发“贪吃蛇大作战”这样的手机游戏,看起来应该是个easy的程序,也就链表记录蛇的位置,长度。而我们遇到了游戏运行时间久了之后,游戏卡顿的问题,最终发现是遍历蛇、每条蛇吃食物等等操作,在这个数据大了之后遍历就很慢。最后针对这样的情况分析修改,来做性能优化。
而在我的工作中,微服务体系的注册中心、配置中心、网关等中间件,其维护着整个公司以万为单位的微服务节点,其如何保持较低的延迟来响应都是基于精心设计的数据结构、网络模型的。
可能很多同学和我一样,也参与过很多业务系统的开发,觉得对于数组的应用也就是简单的ArrayList
,那不知道有没有想过这里会不会用LinkedList
更为合理呢?这些问题往往在开发环境会因为数据量不大而无差异,在生产上面对千万、上亿级的数据量,很有可能会形成一个耗时的慢接口,或者明显占用的内存过高导致OOM。
其实,在我参与开发一个无码BI平台时,就遇到类似数据结构稍作优化,就能大有改善的场景:
tableList
中遍历获取tableA
需要对columnList
进行变量,进行判等操作,过滤出所有与tableA
对应的列。然而,改为Map,则能以O(1)的时间复杂度进行读取。
Java 编译的字节码文件不是直接在底层的系统平台上运行的,而是在 Java 虚拟机 JVM 上运行,JVM 屏蔽了底层系统的不同,为 Java 字节码文件构造了一个统一的运行环境。
通过 Java 命令启动 JVM,JVM 的类加载器根据 Java 命令的参数到指定的路径加载.class 类文件,类文件被加载到内存后,存放在专门的方法区。然后 JVM 创建一个主线程执行这个类文件的 main 方法,main 方法的输入参数和方法内定义的变量被压入 Java 栈。
程序计数寄存器一开始存放的是 main 方法的第一行代码位置,JVM 的执行引擎根据这个位置去方法区的对应位置加载这行代码指令,将其解释为自身所在平台的 CPU 指令后交给 CPU 执行。
我们再回过头看 JVM,它封装了一组自定义的字节码指令集,有自己的程序计数器和执行引擎,像 CPU 一样,可以执行运算指令。它还像操作系统一样有自己的程序装载与运行机制,内存管理机制,线程及栈管理机制,看起来就像是一台完整的计算机,这就是 JVM 被称作 machine(机器)的原因。
JVM垃圾回收:(标记)清理、压缩(整理)、复制
不同垃圾回收器的执行流程:
JVM 有很多配置参数,Java 开发过程中也可能会遇到各种问题,了解了 JVM 的基本构造,就可以帮助我们从原理上去解决问题。
执行引擎在执行字节码指令的时候,是解释执行的,也就是每个字节码指令都会被解释成一个底层的 CPU 指令,但是这样的解释执行效率比较差,JVM 对此进行了优化,将频繁执行的代码编译为底层 CPU 指令存储起来,后面再执行的时候,直接执行编译好的指令,不再解释执行,这就是 JVM 的即时编译 JIT。
了解网络通信原理,了解互联网应用如何跨越庞大的网络构建起来,对我们开发一个互联网应用系统很有帮助
DNS、CDN、HTTP、TCP、LB(负载均衡)等等都是报文会经过的
OSI 7层协议合并为5层模型: 物理层、数据链路层、网络层、传输层、应用层(会话层、表示层、应用层)
报文:
TCP三次握手
App 和服务器之间发送三次报文才会建立一个 TCP 连接,报文中的 SYN 表示请求建立连接,ACK 表示确认。App 先发送 SYN=1,Seq=X 的报文,表示请求建立连接,X 是一个随机数;淘宝服务器收到这个报文后,应答 SYN=1,ACK=X+1,Seq=Y 的报文,表示同意建立连接;App 收到这个报文后,检查 ACK 的值为自己发送的 Seq 值 +1,确认建立连接,并发送 ACK=Y+1 的报文给服务器;服务器收到这个报文后检查 ACK 值为自己发送的 Seq 值 +1,确认建立连接。至此,App 和服务器建立起 TCP 连接,就可以进行数据传输了。
硬盘的形式主要两种,一种是机械式硬盘,一种是固态硬盘。
机械式硬盘的结构,主要包含盘片、主轴、磁头臂,主轴带动盘片高速旋转,当需要读写盘上的数据的时候,磁头臂会移动磁头到盘片所在的磁道上,磁头读取磁道上的数据。读写数据需要移动磁头,这样一个机械的动作,至少需要花费数毫秒的时间,这是机械式硬盘访问延迟的主要原因。
如果一个文件的数据在硬盘上不是连续存储的,比如数据库的 B+ 树文件,那么要读取这个文件,磁头臂就必须来回移动,花费的时间必然很长。如果文件数据是连续存储的,比如日志文件,那么磁头臂就可以较少移动,相比离散存储的同样大小的文件,连续存储的文件的读写速度要快得多。
固态硬盘则没有这种磁性特质的存储介质,也没有电机驱动的机械式结构。其中主控芯片处理端口输入的指令和数据,然后控制闪存颗粒进行数据读写。由于固态硬盘没有了机械式硬盘的电机驱动磁头臂进行机械式物理移动的环节,而是完全的电子操作,因此固态硬盘的访问速度远快于机械式硬盘。
inode 中记录着文件权限、所有者、修改时间和文件大小等文件属性信息,以及文件数据块硬盘地址索引。inode 是固定结构的,能够记录的硬盘地址索引数也是固定的,只有 15 个索引。其中前 12 个索引直接记录数据块地址,第 13 个索引记录索引地址,也就是说,索引块指向的硬盘数据块并不直接记录文件数据,而是记录文件数据块的索引表,每个索引表可以记录 256 个索引;第 14 个索引记录二级索引地址,第 15 个索引记录三级索引地址
RAID,即独立硬盘冗余阵列,将多块硬盘通过硬件 RAID 卡或者软件 RAID 的方案管理起来,使其共同对外提供服务。根据硬盘组织和使用方式不同,常用 RAID 有五种,分别是 RAID 0、RAID 1、RAID 10、RAID 5 和 RAID 6。
实践中,使用最多的是 RAID 5,数据被分成 N-1 片并发写入 N-1 块硬盘,这样既可以得到较好的硬盘利用率,也能得到很好的读写速度,同时还能保证较好的数据可用性。使用 RAID 5 的文件系统比简单的文件系统文件容量和读写速度都提高了 N-1 倍,但是一台服务器上能插入的硬盘数量是有限的,通常是 8 块,也就是文件读写速度和存储容量提高了 7 倍。(所有数据的bit位,逐位进行异或,得到的就是校验位。 如果丢失部分数据,用校验数据和其余数据逐位进行异或运算,可到丢失部分数据。)
HDFS:NameNode 负责整个分布式文件系统的元数据(MetaData)管理,也就是文件路径名、访问权限、数据块的 ID 以及存储位置等信息,相当于 Linux 系统中 inode 的角色。HDFS 为了保证数据的高可用,会将一个数据块复制为多份(缺省情况为 3 份),并将多份相同的数据块存储在不同的服务器上,甚至不同的机架上。这样当有硬盘损坏,或者某个 DataNode 服务器宕机,甚至某个交换机宕机,导致其存储的数据块不能访问的时候,客户端会查找其备份的数据块进行访问。
- 应用程序提交 SQL 到数据库执行,首先需要建立与数据库的连接,数据库连接器会为每个连接请求分配一块专用的内存空间用于会话上下文管理
- 一个 SQL 提交到数据库,经过连接器将 SQL 语句交给语法分析器,生成一个抽象语法树 AST;AST 经过语义分析与优化器,进行语义优化,使计算过程和需要获取的中间数据尽可能少,然后得到数据库执行计划;执行计划提交给具体的执行引擎进行计算,将结果通过连接器再返回给应用程序。
- 执行引擎是可替换的,只要能够执行这个执行计划就可以了。
- 一个是 PrepareStatement 会预先提交带占位符的 SQL 到数据库进行预处理,提前生成执行计划,当给定占位符参数,真正执行 SQL 的时候,执行引擎可以直接执行,效率更好一点。
- 另一个好处则更为重要,PrepareStatement 可以防止 SQL 注入攻击。
- 数据库索引使用 B+ 树,我们先看下 B+ 树这种数据结构。B+ 树是一种 N 叉排序树,树的每个节点包含 N 个数据,这些数据按顺序排好,两个数据之间是一个指向子节点的指针,而子节点的数据则在这两个数据大小之间。
- 数据库索引有两种,一种是聚簇索引,聚簇索引的数据库记录和索引存储在一起。MySQL 数据库的主键就是聚簇索引,主键 ID 和所在的记录行存储在一起。
- 另一种数据库索引是非聚簇索引,非聚簇索引在叶子节点记录的就不是数据行记录,而是聚簇索引,也就是主键。
- 这种通过非聚簇索引找到主键索引,再通过主键索引找到行记录的过程也被称作回表。
- 数据库实现事务主要就是依靠事务日志文件。
- 此外,像 MySQL 数据库还有 binlog 日志文件,记录全部的数据更新操作记录,这样只要有了 binlog 就可以完整复现数据库的历史变更
- 对于一般的应用开发者而言,全面掌握关系数据库的各种实现细节,代价高昂,也没有必要。我们只需要掌握数据库的架构原理与执行过程,数据库文件的存储原理与索引的实现方式,以及数据库事务与数据库复制的基本原理就可以了。
工作中,也有观察过apache kylin的SqlNode生成过程,了解calcite框架的使用。对自身的无码BI系统建设有较大益处。
不同的数据库因为底层储存不尽相同,索引方式也不相同,使得SQL的编写,也需要根据数据库特性来编写。例如:
-- 1
select count(a), sum(b), c from table_a where d = 1 group by c;
-- 2
select a, b, c from table_a where d = 1;
这样的两条SQL,在oracle或mysql等OLTP的数据库中,第1条会明显慢于第2条。毕竟,第一条还要做聚合计算。但是在apache kylin这样的OLAP数据库中,第1条会更加快。其预计算的特性,会将索引的度量都计算完成,存储在cube中,效率会更高。
sql的优化,并不是有固定公式的,查询都是要根据不同数据库的存储特征,查询引擎来写适合它的语句。
- 软件架构师必须站在一个很高的高度去审视自己软件的架构,去理解自己的工作在更宏大的背景中的位置和作用,才能构建出一个经得起时间考验的软件系统。
- 但是面向过程的复杂性随着软件规模的膨胀以更快的速度膨胀。于是很多大型软件的开发过程开始失控,最终以失败告终,人们遇到了软件危机。
- 事实上,现实中的面向对象编程几乎从未实现人们期望中的面向对象编程。上面举的 Java 的 User 对象示例就是典型,这是一个我们经常见到,却又非常不面向对象的对象。这个对象只有属性,没有行为,现实中的 User 对象显然不是这样。
- 面向数据的编程需求越来越多,能够更好迎合这一需求的编程模型开始得到青睐,比如函数式编程。
- 如何更好地利用 CPU 的多核以及分布式集群的多服务器特性,必须是软件编程以及架构设计时需要考虑的重要问题,软件编程越来越多需要考虑机器本身,相对应的,反应式编程得到越来越多的关注。
文中这个故事大概发生在 2009 年,整整十年前,那个时候互联网还不像今天这样炙手可热,提供的薪水也不像今天这样有竞争力,也没有 BAT 这样的专有名词指代所谓的互联网巨头。那个时候,计算机专业优秀的毕业生向往的是微软、Oracle、IBM 这样的外资 IT 巨头,退而求其次,国内好的 IT 公司是联想、用友这些企业。事实上,那个时候在技术研发能力上,互联网公司的技术能力也是落后传统企业的,阿里巴巴最核心的数据存储依赖的是 IBM、Oracle、EMC 的解决方案,即所谓的 IOE。
我个人感觉,互联网公司的崛起大概是在七八年前,移动互联网开始出现,互联网的渗透率得到加速,BAT 逐渐开始成为家喻户晓的名字,名气大涨。其次,经过前面时间的积累,互联网企业主导的各种分布式技术、大数据技术、移动互联网技术、云计算技术的风头超过传统 IT 巨头,阿里巴巴开始去 IOE,打造自己的云计算平台,成为先进技术的代表者;最主要的还是互联网企业盈利能力大幅增加,能够提供市场上更有竞争力的薪水和股票。
但是事情真正的吊诡之处还不在这里,当今这些互联网大厂的核心技术和业务模式在十几年前就已经奠定了,经过几年的摸索,大概在七八年前开始稳定成熟。也就是说,互联网企业的技术实力和商业能力是在这些企业还默默无闻的时候就发展起来的,而在这些企业成为明星之后,并没有什么突破性的进展。想想这些所谓的互联网大厂,最近几年,并没有什么值得称道的商业模式创新和技术创新。
也许你会发现,你可能不需要追逐当前所谓的热门技术,而应该好好想想需要为自己的未来准备些什么。