计算机网络 操作系统
linux常用命令
-
用户命令:who、su、sudo、pwd、cd、ls、cat、man
-
cp、mv、rm、mkdir
-
创建文件 echo、touch、cat、vim
-
chmod
-
find命令
find . -name “file1”
-
grep命令
grep -nE “k” file1
-
head、tail、history
-
tar打包和压缩、gzip压缩
-
scp命令
进程和线程
1.进程和线程的区别
- 资源:进程是资源分配的基本单位,线程不独立拥有资源,但可以访问其隶属进程的资源。
- 调度:线程是独立调度的基本单位,同一进程中线程的切换不会引起进程的切换;不同进程的线程的切换会引起进程的切换;
- 系统开销:
- 创建或撤销进程,系统要为其分配或回收资源(如内存空间、IO设备),所付出的开销远比创建或撤销线程大;
- 进程切换,需要保存当前进程cpu环境,设置新调度进程的cpu环境
根本区别在于:每个进程都有自己的地址空间,线程则共享地址空间。所有的区别都是根据这个区别产生的。
2.进程通信方式IPC inter-process communication 进程间通信
-
共享内存——一块可供多个进程访问的内存区域
-
管道——半双工
-
信号和信号量——通知接收进程某个事件已经发生;信号量是一个计数器,用户互斥和同步
-
消息队列
-
套接字——可用于不同设备间的进程通信
3.线程通信方式
-
线程间通信可以通过 直接读写同一进程的数据 进行通信
-
锁机制
互斥锁:以排他方式防止数据结构被并发修改
读写锁:允许多个进程同时读,对写是互斥的
条件变量:可以以园子的方式阻塞进程,知道某个特定条件为真。条件测试是在互斥锁的保护下进行的,条件变量与互斥锁一起使用。
-
信号量机制(Semophore)和信号机制(Signal)
-
-
线程间通信目的主要用于线程同步,所以线程没有像进程通信中的用于数据交换的通信机制。
OSI七层、四层、五层网络协议
物理层:主要规定了比特流如何在传输媒体上传输
链路层:实现了具体每段链路之间的通信,帧的传输
网络层:实现了主机之间的通信,ip分组的传输
运输层:实现了进程之间的通信,TCP和UDP报文的传输
会话层:建立和管理会话
表示层:数据内部格式
应用层:为特定应用程序提供数据传输,HTTP,DNS
TCP和UDP
TCP
- 面向连接、可靠的、传输TCP报文段、提供完整性服务
UDP
- 无连接、不可靠、传输用户数据包、提供及时性服务
三次握手和四次挥手
1.三次握手
- 客户端向服务器发送连接请求报文:SYN=1,ACK=0,报文序号seq=x
- 服务器向客户端发送连接确认报文:SYN=1,ACK=1,报文确认号ack=x+1,seq=y
- 客户端再向服务器确认:ACK=1,ack=y+1,sep=x+1
2.四次挥手
-
A发送连接释放报文:FIN=1,seq=u
-
B收到后确认:ACK=1,ack=u+1,seq=v
此时连接处于半关闭状态
-
当B不需要连接是,发送连接释放报文:FIN=1,ACK=1,ack=u+1,seq=w
-
A收到后确认:ACK=1,ack=w+1,seq=u+1
3.为什么分两次释放:
- A向B发送连接释放请求,B收到请求后就进入了close-wait状态;这个状态是为了让B发送还未传送完毕的数据。传送完毕后,B会想A发送连接释放请求
4.A在向B进行确认以后进入time_wait状态,再进入closed状态
- 等待2MSL,确保最后一个确认报文能够到达;如果不能到达,B会再次发送连接释放请求。
URL输入浏览器经历的过程
- DNS域名解析,浏览器查找DNS域名服务器获取域名对应的ip地址
- 浏览器和服务器建立TCP链接
- 浏览器向服务器发送一个HTTP请求;服务器处理请求,返回一个HTTP响应报文;浏览器解析和渲染页面;
- 释放TCP链接;
应用层使用了http协议,传输层使用了ospf协议和TCP协议,网络层使用了IP协议和ARP协议
操作系统
IO模型
IO概念和五种IO模型 - shengguorui - 博客园 (cnblogs.com)
什么是IO?
-
linux中一切皆文件,文件就是一串二进制流而已;不管socket,还是FIFO、管道、终端、对我们来说,一切都是文件,一切都是流;
-
在信息交换的过程中、我们都是对这些流进行数据的收发工作、简称I/O操作(input and output)
- 从流中读出数据,系统调用read
- 往流中写入数据,系统调用write
-
计算机中有很多流,如何标识?
采用文件描述符,fd;一个fd就是一个整数,所以对这个整数的操作,就是对这个文件(流)的操作;
我们创建一个socket,通过系统调用会返回一个文件描述符;那么剩下对socket的操作就会转化为对在这个描述符的操作;这是分层和抽象的思想。
这里的IO主要侧重网络通信 数据应用程序等待网络数据,数据需要经过内核缓冲区。
IO交互?
用户空间< == >内核空间< == >设备空间(磁盘)
-
linux使用的是虚拟内存机制,用户应用程序必须通过系统调用请求内核协助完成IO操作,内核会为每个IO设备维护一个缓冲区。
-
对于一个从磁盘到用户应用程序输入操作,系统调用后、内核会先看缓冲区有没有相应的缓存数据,没有的话再到设备中读取
-
对于一个网络输入操作通常包括两个阶段:
- 等待网络数据到达网卡,读取到内核缓冲区
- 从内核缓冲区复制数据到用户空间
IO有内存IO、磁盘IO、网络IO三种,通常我们说的IO是后两种
IO模型有哪些?
1.阻塞IO(阻塞指阻塞程序)——一次等一个
- 在内核将数据准备好之前,应用进程 系统调用会一直等待
- 默认是阻塞IO
2.非阻塞IO
-
在内核将数据准备好之前,应用进程 可以进行其他操作;
-
但是会向内核轮询数据是否准备好
每次客户询问内核是否有数据准备好,即文件描述符缓冲区是否就绪。当数据准备好,就进行拷贝。当数据没有准备好,不阻塞程序,内核直接返回为准备就绪的信号,等待用户程序的下一次轮询。
3.信号驱动IO
- 用户程序告诉内核,当数据准备好时,给它发送一个信号;
- 应用进程执行sigactiion系统调用,内核立即返回,应用程序可以继续执行;等待数据阶段是非阻塞状态
- 数据准备好时,内核向应用进程发送SIGIO信号,应用进程收到信号后在信号处理程序中调用recvfrom将数据从内核缓冲区复制到应用进程中。
4.IO多路复用(事件驱动IO)——可以一次等多个
-
D同样也在河边钓鱼,但是D生活水平较好,D拿了很多鱼竿,一次性有很多鱼竿在等,D不断的查看每个鱼竿是否有鱼上钩。增加了效率,减少了等待的时间。
-
IO多路转接多了一个select函数,select函数有一个参数是文件描述符集合,对这些文件描述符进行循环间厅,当某个文件描述符就绪时,就对这个文件描述符进行处理。
其中,select只负责等,recvfrom只负责拷贝。
-
IO多路转接是属于阻塞IO,但可以对多个文件描述符进行阻塞监听,所以效率较阻塞IO高
5.异步IO
- E也想钓鱼,但是有事情,于是他雇来了F,让F帮他等待鱼上钩,一旦有鱼上钩,F就打电话给E,E就会将鱼钓上去
- 应用程序执行aio_read系统调用,内核会立即返回,应用程序可以继续执行,等待数据阶段是非阻塞状态
- 当数据准备好后,由内核将数据拷贝到应用程序中,调用aio_read中定义好的函数处理程序
信号驱动IO和异步IO的区别:
信号驱动IO的信号是通知应用程序可以开始IO,异步IO的信号是通知应用进程已经IO完成。
select/poll/epoll都是IO多路复用的具体实现
select、poll、epoll之间的区别总结整理- Rabbit_Dale - 博客园 (cnblogs.com)
-
select出现最早,之后是poll,再是epoll
-
IO多路复用就是通过一种机制,可以监视多个描述符,一旦某个描述符就绪(一般是读就绪或者写就绪),能够通知应用程序进行相应的读写操作。
-
但select、poll、epoll本质上都是同步IO,需要在读写事件就绪后自己负责进行读写,即读写过程是阻塞的。
异步IO则无需自己负责进行读写,异步IO的实现会负责把数据从内核拷贝到用户空间。
1.select
-
过程:应用程序进行系统调用,select函数遍历所有的fd,调用其对应的poll方法,poll方法会返回一个描述读写操作是否就绪的mask掩码。如果有fd已经准备就绪就进行复制操作,没有就继续睡眠一定时间再轮流检查。
-
select的三大缺点:
- 每次调用select,都需要把fd集合从用户态拷贝到内核态,这个开销在fd很多时会很大
- 每次调用select,都需要在内核遍历传递进来的所有fd,这个开销在fd很多时也很大
- select支持的文件描述符数量太小了,默认是1024
2.poll
- poll的实现和select非常相似,只是描述fd集合的方式不同,poll使用pollfd结构,而select使用fd_set结构,其他都差不多。
- poll文件描述符使用链表实现,没有数量限制。
3.epoll
-
epoll是对select和poll的改进,解决了select的三大缺点
-
epoll与“select和poll“的调用接口不同:
-
select和poll都只提供了一个函数,select函数或者poll函数
-
epoll提供了三个函数:epoll_create创建一个epoll句柄、epoll_ctl注册要监听的事件类型、epoll_create等待事件的产生
-
对于第一个缺点:epoll_ctl 每次注册新的事件到epoll句柄时,会将所有的fd拷贝进内核,而不是在epoll_wait时重复拷贝。保证了每个fd在整个过程中只会拷贝一次。
-
对于第二个缺点:epoll不像select和poll每次都在内核遍历一遍所有传递进来的fd,epoll_ctl只遍历一次所有fd,并为每个fd在指定一个回调函数,当设备就绪,调用回调函数,把就绪的fd加入一个就绪链表。
epoll_wait就是在就绪链表中查看有没有就绪的fd,实现睡一会,判断一会。
-
对于第三大缺点,epoll为没有限制,支持的fd上限是最大可以打开文件的数目,这个数字一般非常大。
-
-
总结:
-
select和poll实现需要自己不断轮询所有fd集合,直到有设备就绪,期间可能要睡眠和唤醒多次交替。
epoll也需要调用epoll_wait不断轮询就绪链表,期间也可能多次睡眠和唤醒交替;但是它是设备就绪时,调用回调函数,把就绪fd放入就绪链表中,并唤醒在epoll_wait中进入睡眠的进程。
虽然都要睡眠和交替,但是select和poll在“醒着”时需要遍历整个fd集合,而epoll在醒着时只需要判断一下就绪链表是否为空就行了。这节省了大量cpu时间,这就是回调机制带来的性能提升。
-
select和poll每次调用都要把fd从用户态往内核态拷贝一次,并且要把current往设备等待队列中挂一次;
而epoll整个过程只要拷贝一次,而且也只把current往等待队列挂一次,并为每个fd指定一个回调函数。
这也能节省不少的开销。
应用场景:
- select的timeout参数精度更高,更适合实时性要求高的场景。几乎支持所有主流平台
- epoll只能运行在linux上;当fd集合数量较少时不能体现epoll优势,当有大量fd时考虑使用。
知道哪些锁?优劣?使用场景?
操作系统的各种锁_ZIMAJIM的博客-优快云博客_操作系统锁的类型
1.互斥锁
- 使用场景:多线程访问时,用于保证临界区互斥资源在任意时刻只有一个线程访问
2.读写锁
- 使用场景:读共享,写独占。读写不能同时,写的优先级高
- 当没有写锁时,允许多个进程同时读
- 当没有读锁和写锁时,才能分配写锁用于写
3.条件变量
-
条件变量严格意义上不是锁,但是条件变量能够阻塞线程
-
互斥锁用于上锁,条件变量用于等待,每个条件变量总有一个互斥锁与之关联使用。
-
互斥量:保护一块共享数据,条件变量引起阻塞
-
条件不满足:线程阻塞;条件满足,唤醒阻塞线程
4.信号与信号量
信号量和信号机制用于线程之间的同步
5.自旋锁
-
概念:用于进程或线程之间的同步。
普通锁获取失败,该线程被阻塞。
而cpu“处理阻塞引起的上下文切换”比“忙等”的代价高时(锁的已保持者保持锁的时间比较短),可以不放弃cpu时间片,而是在“原地”忙等,知道锁的持有者释放了该锁。
这就是自旋锁的原理,自旋锁是一种非阻塞锁。
-
存在问题:
- 过多占用cpu时间
- 死锁问题:一个线程自身递归调用自旋锁(自身两次调用),原则:递归程序不能在持有自旋锁是调用它自己,也不能在递归调用时试图获取相同的自旋锁。
-
自旋锁与互斥锁区别:
-
类似互斥锁,都是为了实现保护资源共享
-
在任意时刻,都最多只能有一个保持者
-
获取互斥锁的线程,如果锁已被占用,则该线程将进入睡眠状态;
获取自旋锁的线程不会睡眠,会一直死等,直到锁释放。
-
6.乐观锁和悲观锁(分类思想)
悲观锁:
-
总是假设最坏的情况,每次拿取数据时都害怕别人会修改;所以每次去拿数据时都会上锁,
这样别人想再拿这个数据就会阻塞(共享资源每次只给一个线程使用,其他线程阻塞,用完后再把资源转让给其他线程
-
传统关系型数据库使用了很多这种锁机制,如表锁,行锁等。读锁,写锁,都是在操作之前先上锁。
Java中synchronized和ReentrantLock等独占锁。
乐观锁:
-
总是假设最好的情况,每次去拿数据的时候都认为别人不会修改,所以不会上锁,
但是在数据更新的时候,会判断在此期间别人有没有去更新这个数据;
可以使用版本号机制和CAS算法实现。
-
乐观锁适用于多读的应用类型,这样可以提高吞吐量。
像数据库提供的类似于write_condition机制
Java中java.util.concurrent.atomic包下的原子变量类就是使用了乐观锁的一种实现方式CAS实现的
使用场景:
- 各有优势,没有好坏之分
- 乐观锁适用于多读场景,冲突很少发生时,可以省去锁的开销,加大系统的整个吞吐量
- 悲观锁适用于多写场景,冲突比较多
乐观锁的两种实现方式:
-
版本号机制
一般在数据表中加上一个数据版本号version字段,数据更新前后验证数据版本号是否一致,一致才更新。
-
CAS算法
-
compare and swap,是著名的无锁算法。
无锁编程,在不使用锁的情况下实现多线程之间的变量同步,非阻塞同步
-
原理:CAS操作包含三个操作数——内存位置V、预期原值A,新值B。
如果内存位置的值与预期原值相匹配,那么处理器会自动将该位置值更新为新值,否则,处理器不做任何操作。
第一次修改失败,后面重新尝试的过程称为自旋。
当且仅当V的值等于A时,CAS通过原子方式用新值B来更新V的值,否则不会执行任何操作(比较和替换是一个原子操作),一般情况下是一个自旋操作,即不断的尝试。
-
CAS有效的说明了“我认为位置V应该包含值A;如果包含该值,则将B放到这个位置;否则,不要更改该位置,只告诉我这个位置现在的值即可”。
-
只能保证一个共享变量的原子操作。
-
7.递归锁
-
即在同一线程上该锁是可重入的,对于不同线程则相当于普通的互斥锁。
-
同一线程上函数A递归获取已经获得的锁,不会形成死锁。但此时如果其他线程想要加锁,只有等待拥有锁的线程释放所有的锁(加锁几次就要释放几次)。
口述生产者消费者
-
属于进程同步问题,利用信号与信号量
-
进程同步常见问题:
- 生产者消费者问题
- 读者写者问题
- 哲学家进餐问题
1.概念
-
生产者消费者模式通过一个容器来解决生产者和消费者的强耦合关系;两者不直接通信,而是通过阻塞队列来进行通信。
-
生产者生产完数据不用直接等待消费者处理,直接扔给阻塞队列;
消费者不找生产者要数据,而是直接从阻塞队列中取;
阻塞队列相当于一个缓冲区,平衡了生产者和消费者的处理能力。
2.原则
- 生产者和生产者之间是互斥关系,消费者和消费者之间是互斥关系,生产者和消费者之间是同步与互斥关系。
- 阻塞队列:当队列为空时,从队列中获取元素的操作将会被阻塞,直到队列中被放入了元素;当队列满时,往队列中存放元素的操作将会被阻塞,直到有元素从队列中取出。
负载均衡?轮询权重怎么确定?
1.概念
-
集群中的应用服务器(节点),通常被设计成无状态,用户可以请求任意一个节点。
负载均衡服务器,根据集群中每个节点的情况,将请求转发到合适的节点上。
-
过程:
- 根据负载均衡算法选择合适的节点
- 进行转发
2.负载均衡算法
-
轮询、加权轮询
-
最少连接、加权最少连接(当前连接数/权重)
-
随机算法
-
源地址哈希法:对客户端IP计算哈希值后,在对服务器数量取模,得到目标转发节点序号;
可以保证同一IP的客户端请求被转发到同一台服务器上,用来实现会话粘滞。
轮询权重可以通过服务器性能确定
最小连接实现:可以根据每个服务器的当前最小连接数维护一个最小堆,注意堆节点的设计(服务器序号,服务器最小连接数)
3.转发实现
-
http重定向
-
在DNS域名解析时,根据负载均衡算法计算源节点IP(DNS域名服务器同时是负载均衡服务器)
-
反向代理思想
-
内网中,增加反向代理服务器
-
外网中,增加反向代理服务器;在网络层根据负载均衡算法计算源服务器的IP地址,进行转发;
一般将集群的网关服务器设置成为负载均衡服务器
-
内网中,在链路层根据负载均衡算法计算源服务器的MAC地址,并进行转发。
由于源服务器此时拥有客户端的IP,因此返回时直接返回响应给客户端,响应不经过负载均衡服务器。
大型网站普遍使用。
-
4.集群下的Session管理
-
sticky session:配置负载均衡服务器,使得同一个用户的请求,每次都能转发某一个特定服务器;
问题:服务器宕机
-
session replication:在服务器之间进行session同步
问题:占据过多内存
-
session server:使用单独的服务器存储session数据(session服务器可以使用MySQL或者Redis进行存储)
集群中的应用服务器应该保持无状态,使用单独的session服务器单独存储用户的会话信息。
经常使用;
数据库
MySQL有哪些引擎、InnoDB和MyISAM区别
- InnoDB引擎:支持事务、支持表级锁和行级锁、支持外键约束、不支持全文索引、表空间较大
- MyISAM引擎:不支持事务、只支持表级锁、不支持外键约束、支持全文索引、表空间较小
事务
1.事务是什么?
- 一个事务里的操作要么全部成功,要么全部失败。
2.事务有哪些操作?
-
开始事务、提交、回滚;
-
创建一个保存点、回到保存点
3.ACID
- 原子性
- 一致性:事务执行前后,数据库处于一致性状态,例如银行转账。
- 隔离性:多个线程同时操作数据库
- 持久性
4.隔离性造成的问题:
- 脏读
- 不可重复读
- 幻读
索引
索引分类:主键索引、唯一索引、常规索引、全文索引
redis
Java基础复习
JVM
一、JDK运行时内存
程序计数器、JVM栈、本地方法栈都是一个线程所私有的。
程序计数器:记录正在执行的虚拟机字节码指令的地址
JVM栈:
-
每次java方法从方法调用到执行完成的过程,对应着一个栈帧在JVM中入栈和出栈的过程
-
存储“局部变量、操作数栈、动态链接、返回地址”
局部变量表:基本数据类型、对象引用、returnAddress类型
操作数栈:可以理解为JVM栈中一个用于计算的临时数据存储区
动态链接:运行时常量池的方法引用
方法的返回地址:
本地方法栈:
-
java方法是由java编写的,编译成字节码,存储在class文件中
本地方法是由其他语言编写的,多为c和c++,编译成和处理器相关的机器代码
java方法与平台无关,本地方法与平台相关
-
一个本地方法就是java调用非java代码的接口
堆:
-
堆中存储的是new的对象
栈中存储的时基本数据类型和堆栈中对象的引用
-
是GC的主要区域
方法区:
-
存储“已被加载的类信息,常量,静态变量,即时编译器编译后的代码”
-
运行时常量池 是方法区的一部分
class文件中除了有类的版本,字段,方法,接口等描述信息;还有一项信息是常量池,用于存放编译器生成的各种字面量和符号引用,这部分内容将在类加载后存放到方法取得运行时常量池中。
直接内存:
- NIO类可以使用Native函数直接分配堆外内存
- 直接内存并不是虚拟机运行时数据区的一部分,也不是JVM规范中定义的内存区域
二、GC
- 程序计数器、JVM栈、本地方法栈 都是线程私有的,只存在于线程的生命周期内,随线程结束而消失,无需回收。
- GC主要针对堆和方法区
1.判断一个对象是否被回收
-
引用计数:为每个对象添加一个引用计数器,java中不使用
-
可达性分析算法:JVM使用其来判断对象是否可被回收
以GC Roots为起点进行搜索,可达的对象都是有效的,不可达的对象可被回收
- JVM栈中局部变量表中引用的对象
- 本地方法中引用的对象
- 方法区中类静态属性引用的对象,常量引用的对象
方法区的回收主要是对常量池的回收和类的卸载
2.垃圾回收算法
-
标记-清除算法
- 标记阶段会为活动对象打上标记
- 清除节点会进行对象回收并取消标记位,如果回收的分块与前一个空闲分块连续则合并分块,连接到“空闲链表”中
- 再分配时查询空闲链表找到合适大小的分块
会产生大量内部碎片,导致无法给对象分配内存
-
标记-整理算法
- 回收时让所有存活的对象移动到内存的一端,再对其他所有区域进行清理
需要大量移动对象
-
复制
以堆中新生代对象为例
- 将内存分为eden、fromSurvivor、toSurvivor三部分
- eden用于存放新生的对象,fromSurvivor用于存放上一次清理后存活下来的对象,在进行新的一次GC时,将eden和fromSurvivor中存活的对象复制到toSurvivor中,修改fromSurvivor和toSurvivor指针。
堆中分代回收:
根据对象的存活周期将内存划分为几块,不同块采用不用的收集算法
新生代:复制
老年代:标记-清除算法、或者标记-整理算法
三、内存分配与回收策略
主要指堆和方法区
Java新生代和老年代、永久代详解
概念:
1 程序计数器、java虚拟机栈、本地方法栈 这3个区域是线程私有的,随线程而生,随线程而灭,这几个区域就不需要过多考虑回收的问题,因为方法结束或者线程结束,内存就自然跟着回收了。
2 Java堆分为新生代、老年代
永久代——实际就是方法区
【方法区】是JVM的一种规范,存放类信息,常量,静态变量,即时编译后的代码
【永久代】是HotSpot虚拟机的一种具体体现,实际上指的就是方法区,或者说“用永久代”来实现方法区。
对于其他虚拟机是不存在“永久代”的概念的。
新生代:
1 主要用来存放新生的对象,一般占据堆空间的1/3;
新生代与老年代的比例值为1:2;
由于频繁创建对象,所以新生代会频繁触发minorGC进行垃圾回收;
GC采用复制收集算法
2 新生代分为Eden区、ServivorFrom、ServivorTo三个区(8:1:1)
Eden区:Java新对象的出生地(如果新创建的对象很大,则直接分配给老年代)。
当Eden区内存不够的时候,就会触发一次MinorGC,对新生代 区进行一次垃圾回收
SurvivorFrom区:保留上一次MinorGC过程中的幸存者
SurvivorTo区:保留MinorGC过程中的幸存者
3 MinorGC的过程
Eden区和 From指向的Survivor区 中的存活对象会被复制到 to指向的Survivor区 中,(然后清理Eden和From区)
然后交换from和to指针,以保证下一次MinorGC时,to指向的Survivor区还是空的。
老年代:
老年代的对象比较稳定,所以MajorGC不会频繁执行
MajorGC的触发机制
1 进行MinorGC时,如果Eden和FromSurvivor区中存活对象需要的内存空间>ToSurvivor的空间,溢出的对象就会进入老年代,当溢出对象所需空间>老年代空间,MajorGc。
2 当需要足够大的连续空间 分配给新创建的较大对象时,如果新生代不够就放入老年代,老年代如果也不够,就会触发MajorGC进行垃圾回收腾出空间。
MajorGC采用标记-清除算法(或标记-整理)
MajorGC的耗时比较长,因为要先整体扫描再回收,MajorGC会产生内存碎片。
当老年代也满了,装不下的时候,就会抛出OOM(out of memory)
方法区(永久代):
指内存的永久保存区域,主要存放类信息,常量,静态变量,即时编译后的代码。
Class在被加载的时候 元数据信息会被放入永久区域,
但是GC不会在主程序运行的时候清除永久代的信息。
所以导致了永久代的信息会随着类加载的增多而膨胀,最终导致OOM。
在Java8中,方法区(永久代)概念被删除,
原来方法区中的数据——被分到堆+元空间中。
堆中分到——常量(常量池)、静态变量
元空间——存储类的信息
元空间
本质和“永久代”类似,都是对JVM规范中“方法区”的实现。
区别:元空间不存储在虚拟机(Java虚拟机内存分配)中,而是使用本地内存,因此默认情况下元空间的大小仅仅受本地内存的大小限制。
这样加载多少类的元数据就不再由MaxPermSize控制,而是由系统的实际可用空间来控制。
MinorGC、MajorGC、FullGC:
MinorGC清理新生代——当Eden区满的时候
MajorGC清理老年代——新生代MinorGC存活对象溢出到老年代,导致老年代满+新生代装不下大对象,来老年代也装不下
FullGC清理整个堆空间——包括新生代和老年代
FullGC的触发机制
1 调用System.gc时,系统建议执行FullGC,但是不必然执行
2 老年代空间不足
3 方法区空间不足
4 通过MinorGC后进入老年代的溢出内存 大于老年代的可用内存
虚拟机给每个对象定义了一个“对象年龄(Age)”计数器。
如果对象在Eden出生,并经过第一次MinorGC后仍然存活,并且能够被ToSurvivor接纳,将被移动到ToSurvivor中,并将对象年龄设为1.
对象在Survivor区中每熬过一次MinorGC,年龄就增加一岁,
当他的年龄增加到一定程度(默认为15岁)时,就会被晋升到老年代中。
对象晋升老年代的的年龄阈值,可以通过MaxTenuringThreshold来设置。
四、类加载机制
1.类的加载过程:
-
类加载是一个将.class字节码文件读入内存,并实例化为Class对象且进行相关初始化的过程
-
java的类使用时才被加载,且类的加载过程只发生一次
类是在运行期间,第一次使用时动态加载的;而不是一次性加载所有类,因为如果一次性加载,那么会占用很多的内存
java类的加载过程可分为三个步骤:加载、链接、初始化
区分“类的加载过程”和其步骤“加载”
“类的初始化”只是“类的加载过程”的一个步骤
-
加载:
-
将.class字节码文件读入内存,将静态数据转换成方法区的运行时数据结构
-
生成这个类对应的Class对象
Class对象是加载到内存中才会产生的,由JVM创建,我们只能获取,不能生成
通过反射动态进行的
-
-
链接:将Java类的二进制代码 合并到 JVM的运行状态之中
- 验证:确保类信息符合JVM规范
- 准备:为static变量分配内存并设置初始值(在类初始化之前做),在内存的方法区进行
- 解析:JVM常量池内的符号引用(常量名)替换为直接引用(地址)
-
初始化:。
-
JVM执行类构造器
<clinit>()方法
的过程。类构造器
<clinit>()方法
是由编译期 自动收集类中所有类变量的赋值动作和static代码块中的语句合并产生的。类构造器是构造类信息的,不是构造该类对象的构造器。
-
当初始化一个类的时候,如果发现其父类还没有进行初始化,则需要先触发其父类的初始化。
-
JVM会保证一个类的
<clinit>()方法
在多线程环境中被正确加锁和同步。
-
代码测试:
public class Test05 {
public static void main(String[] args) {
A a=new A();
System.out.println(A.m);
}
}
class A{
static {
System.out.println("A类静态代码快初始化");
m=300;
}
static int m=100;
public A(){
System.out.println("A类的无参构造初始化");
}
}
过程:
-
加载阶段:加载Test05类和A类,在方法区分别生成其对应的Class对象
-
链接阶段:为static变量m,在方法区分配内存空间,并进行默认初始化
static int m=0;
-
初始化阶段:类的
<clinit>()方法
将static变量赋值和static代码合并,再执行赋值System.out.println("A类静态代码快初始化"); m=300; m=100; //定义阶段已经在链接阶段完成
2.类加载时机和初始化时机:
-
**类加载时机:**从执行包含main函数的类开始加载,涉及到下面情况的类就加载
- 使用类的静态变量或静态方法
- 使用反射方式来强制创建某个类或接口 对应的java.lang.Class对象
- 创建类的实例
- 初始化某个类的子类
- (直接使用java。exe命令来运行某个主类)
-
**类的初始化理解:**类的初始化 主要就是对类的static变量进行初始化
区分实例变量和静态变量,实例变量在new对象时初始化
-
JVM执行类构造器
<clinit>()方法
的过程。类构造器
<clinit>()方法
是由编译期 自动收集类中所有类变量的赋值动作和static代码块中的语句合并产生的。类构造器是构造类信息的,不是构造该类对象的构造器。
-
当初始化一个类的时候,如果发现其父类还没有进行初始化,则需要先触发其父类的初始化。
-
JVM会保证一个类的
<clinit>()方法
在多线程环境中被正确加锁和同步。
-
-
会发生类初始化的情况(类的主动引用):
- 当JVM启动,先初始化main方法所在的类
- 使用new关键字创建一个类的对象
- 调用该类的static变量(final的常量除外)和static方法
- 使用java.lang.reflect包对该类进行反射调用
- 当初始化一个类时,如果其父类没有被初始化,则会先初始化其父类
-
不会发生类初始化的情况(类的被动引用):
-
引用static常量不会触发此类的初始化(常量在链接阶段就存入调用类的常量池中了)
-
当访问一个静态成员变量时,只有真正声明这个静态成员的类才会被初始化,当通过子类引用父类的静态成员变量时,不会导致子类的初始化
-
通过数组定义类引用,不会触发此类的初始化。
数组只是一个名字和一片空间
-
测试:
//测试类什么时候会初始化
public class Test06 {
static {
System.out.println("Main类被加载");
}
public static void main(String[] args) throws ClassNotFoundException {
//1.主动引用,会发生类的初始化
Son son = new Son();
/*
Main类被加载
父类被加载
子类被加载
*/
//反射也会产生主动引用
Class.forName("com.kuang.reflection.Son");
/*
Main类被加载
父类被加载
子类被加载
*/
//2.被动引用,不会触发类的初始化
//通过子类调用父类static变量或者方法
System.out.println(Son.b);
/*
Main类被加载
父类被加载
2
*/
//通过数组定义类引用
Son[] array=new Son[5];
/*
Main类被加载
*/
//类的常量
System.out.println(Son.M);
/*
Main类被加载
1
*/
}
}
class Father{
static final int M=1;
static int b=2;
static{
System.out.println("父类被加载");
}
}
class Son extends Father{
static{
System.out.println("子类被加载");
m=300;
}
static int m=100;
}
3.类加载器:
什么是类加载器?
类加载器 就是 加载字节码文件(.class)的类
Java程序是一种具有动态性的解释性语言,类(class)只有被加载到 JVM中后才能运行。
当运行指定程序时,JVM会将编译生成的.class文件按照需求和一定的规则加载到内存中,组织成为一个完整的Java应用程序。
这个加载的过程是由类加载器来完成的。具体来说,就是由ClassLoader和它的子类来实现的。
类加载器本身也是一个类,其实质是把类文件从硬盘读取到内存中
类加载的方式:
- 隐式加载:使用new等方式创建对象,会隐式调用类加载器把对应的类加载到JVM中
- 显式加载:通过反射直接调用Class.forName()方法把所需的类加载到JVM中。
类加载器分类:
在Java语言中,类的加载是动态的,它并不会一次性将所有类全部加载后再运行。而是保证程序运行的基础类(例如基类)完全加载到JVM中。
其他类,在需要时才加载。
- 引导类加载器:BootStapLoader,加载最基础的文件
- 用C++编写,是JVM自带的类加载器,负责Java平台核心类库。
- 该加载器程序无法直接获取。
- 在
jre/lib/rt.jar
包下
- 扩展类加载器:ExtClassLoader,加载基础的文件
- 在
jre/lib/ext/*.jar
包下
- 在
- 应用类加载器:AppClassLoader,加载三方jar包和自己编写的java文件
- 在
CLASSPATH指定的所有jar和目录
- 在
类加载器的协调工作:双亲委派机制
3种类加载器是通过委托方式实现:
当加载一个类,JVM会自底向上去找是否已经加载;
且不能存在同名,如我们不能定义java.lang.Spring类;
双亲委派机制会检测安全性,保证创建出的类的唯一性
- 为了防止同名包、类与jdk中相冲突
- 实际加载类的时候,先通知appClassLoader看appClassLoader是否已经缓存;没有的话,appClassLoader又委派给他的父类加载器(extClassLoader)询问,看他是不是已经缓存;没有的话,exClassLoader又委派给他的父类BootStapLoader询问,看他是不是已经缓存或者能加载,有就加载;没有再返回extClassLoader,exClassLoader能加载就加载,不能的话再返回给自己的appClassLoader加载。在返回的路上,谁能加载,加载的同时也放到缓存当中去。
- 正是由于刚开始时自底向上不停地找自己的父级,所以才有Parents加载机制,翻译过来叫双亲委派机制。
加载时:
-
自底向上检查类是否已经装载
-
自顶向下尝试加载类
获取测试:
public class Test07 {
public static void main(String[] args) {
//获取AppClassLoader
ClassLoader appClassLoader=ClassLoader.getSystemClassLoader();
System.out.println(appClassLoader);
//获取ExtClassLoader
ClassLoader extClassLoader=appClassLoader.getParent();
System.out.println(extClassLoader);
//获取BootStapClassLoader
ClassLoader bootStapClassLoader=extClassLoader.getParent();
System.out.println(bootStapClassLoader);
}
//测试当前变量是由那个加载器加载的
ClassLoader curClassLoader=Class.forName("com.kuang.reflection.Test07").getClassLoader();
}
可以看出:
Test07类是由AppClassLoader加载的。
因为BootStapLoader先搜索指定目录找不到,其次ExtClassLoader也找不到,最后AppClassLoader在ClassPath中找到了Test07类。
【注意】BootStapLoader是用C++语言实现的,所以在Java语言中看不到它,输出null
类加载器 加载过程 分为以下3步:
- 加载(将.class文件放入内存,生成Class对象)
- 链接(1.验证:验证待加载.class文件的正确性;2.准备:给类中静态变量分配存储空间并默认初始化;3.解释:将符号引用转换成直接引用)
- 初始化(对静态变量和静态代码块执行初始化工作)
JAVA并发
一、基础:线程状态、使用线程、基础线程机制
-
线程状态
New、Runnable、Blocked、(waited、timed waited)、(Terminated)
-
使用线程
- 实现Runnable接口,实现run方法;当一个线程被调度就会执行其run()方法
- 实现Callable接口
- 继承Thread类
采用实现接口更好,java不支持多继承,但可以实现多个接口。
-
基础线程机制
- Executor:管理多个异步任务的执行,任务间无需同步
- CachedThreadPool,一个任务创建一个线程
- FixedThreadPool,所有任务只能使用固定大小的线程
- SingleThreadExecutor,相当于大小为1的FixedThreadPool
- Daemon:设置为守护线程,守护线程是为其他线程服务的线程
- thread.sleep():休眠当前正在执行的线程
- thread.yield():声明当前线程已经完成其最重要的一部分操作,建议可以切换到其他线程
- Executor:管理多个异步任务的执行,任务间无需同步
二、同步:中断、互斥同步、线程之间的协作、JUC-AQS和其他组件
-
中断
-
调用一个线程thread.interrupt()来做中断该线程
如果该线程处于blocked、waited、timed waited,那么就会抛出InterruptedException异常,从而提前结束该进程
但是不能中断I/O阻塞和synchronized阻塞
-
如果一个线程的run方法执行一个无限循环
可以在循环体中使用interrupted()方法来判断线程是否处于中断状态,从而提前结束进程
-
Executor的中断
调用executor.shutdown()方法,会等待全部线程都执行完毕,再关闭线程
调用executor.shutdownNow()方法,相当于调用每个线程的interrupt()方法
-
-
互斥同步两种锁:synchronized和ReentrantLock
java提供了两种锁机制,来控制多个线程对共享资源的互斥访问
-
synchronized
- 同步代码块:两个线程调用同一个对象的同步代码块,才会同步
- 同步方法:作用于同一个对象
- 同步类:作用于整个类,两个线程调用同一个类的不同对象,同步语句也会进行同步
- 同步一个静态方法:作用于整个类
-
ReentrantLock
是java.util.concurrent(JUC)包中的锁
- 显式定义锁,在同步语句前后显式加锁和解锁
- 仍是作用于同一个对象
- 一般用于同步一个方法中的代码块
比较:
-
synchronized是由JVM实现的,ReentrantLock是JDK实现的
-
synchronized不可中断,ReentrantLock可中断
等待可中断概念:当持有锁的线程长期不释放锁的时候,等待的线程可以选择放弃等待,改做其他事情。
-
synchronized是非公平的,ReentrantLock默认非公平,但是可以设置为公平
公平锁概念:多个线程在等待同一个锁时,按照申请锁的时间顺序来依次获取锁
-
一个ReentrantLock可以同时绑定多个Condition对象
-
优先使用synchronized,由JVM实现;ReentrantLock不是所有的JDK版本都支持
-
-
线程之间的协作
-
join():在线程中调用另一个线程的join()方法,会将当前线程挂起,不是忙等,直到目标线程结束
-
wait()、notify()、notifyAll()
-
线程a中调用wait(),a会被挂起(等待某个条件满足,由其他线程决定,再显式唤醒),挂起的线程回释放锁
-
其他线程中调用notify()或者notifyAll()显式唤醒用wait()挂起的线程;
只能在同步方法或者同步代码块中使用
wait()与sleep()的区别
-
wait()是Object的方法,sleep()是Thread的静态方法
-
wait()会挂起释放锁,sleep()不会释放锁在忙等
-
-
condition.await()、condition.signal()、condition.signalAll()
java.util.concurrent类提供了Condition类来实现线程之间的协作,使用ReentrantLock的lock来获取condition对象
- 使用于wait()类似,一个线程使用condition.await()主动挂起,其他线程使用condition.signal()、condition.signalAll()唤醒
-
-
(JUC的AQS)
java.util.concurrent大大提高了并发性能,Abstract Queued Synchronizer被认为是其核心
- CountDownLatch:用于控制让一个线程等待其他线程;设置线程A初始等待数目,其他线程完成一个,线程A的等待数目减一
- CyclicBarrier:控制多个线程互相等待;6个进程,只有其他6个都结束某一功能,才能继续一起进行
- Semaphore:类似于信号量,可以控制对互斥资源的访问线程数,如每次只能有3个线程访问
-
(JUC的其他组件)
- FutureTask 实现了Runnable接口,可异步获取执行结果
- BlockingQueue 生产者消费者
- ForkJoin用于并行计算类似MapReduce
三、线程安全:Java内存模型、线程安全
-
Java内存模型
-
并发下线程的工作内存、主内存
每个线程都有自己的工作内存,工作内存存储在高速缓存或者寄存器中;保存了该线程使用的变量的主内存副本拷贝;
线程只能直接操作工作内存中的变量,不同的线程之间变量值的传递,通过主内存来完成
-
8个操作完成工作内存和主内存的交互
read把一个变量的值从主内存中传到工作内存;
load把read的值装载到工作内存变量中
use把工作内存中的一个变量传递给执行引擎
assign把一个从执行引擎中接受到的值赋给工作内存中的变量
store把工作内存中变量的值传送回主内存
write把store传来的值装载到主内存变量中
lock作用于住内存变量
unlock
-
内存模型的三大特性
-
原子性
JAVA内存模型保证了上述八大操作的原子性
1)但JVM允许将“没有用volatile修饰的64位变量long、double”的读写操作划分为两次32位来操作,即read、load、store、write不具备原子性
2)int类型也存在线程不安全问题;内存间的交互操作简化为:load,assign、store,对int类型的操作,单个操作满足原子性,但在整体不满足原子性;
使用原子类AtomicInteger能保证多个线程修改的原子性
也可以使用synchronized互斥锁来保证操作的原子性
-
可见性
当多个线程访问同一变量,一个线程修改了这个值,会立马写入内存,其他线程能立即看到修改的值
三种实现方式:
- 使用volatile修饰,保证了可见性;不保证原子性,不能解决线程不安全的问题
- synchronized对一个变量执行unlock操作之前,必须把变量值同步到主内存
- final修饰
-
有序性
在本线程中观察,所有的操作都是有序的;在一个线程中观察另一个线程,所有操作都是无序的,无序是因为发生了指令重排;
Java内存模型中,允许编译器和处理器对指令进行重排;指令重排不会影响单线程的运行,但是会影响多线程并发执行的正确性
解决办法:
- volatile可以禁止指令重排
- synchronized也可以保证有序性,保证每个时刻只有一个线程执行同步代码,相当于让线程顺序执行同步代码
volatile的小结:
-
volatile变量的特性
1)保证可见性,不保证原子性
当写一个用volatile修饰的变量时,JVM会立即把该线程的该变量强制刷新到主内存中去
这个写操作会导致其他线程中的该共享变量缓存无效
2)禁止指令重排
重排序需要遵守的规则:
重排序操作不能对存在数据依赖关系的操作进行重排序;
重排序是为了优化性能,但单线程下程序的执行结果不能改变;
重排序在单行程下能保证结果的正确性,但是多线程环境下的重排序可能影响结果;
使用volatile关键字修饰的共享变量,在编译时会通过插入内存屏障来禁止指令重排序;
volatile禁止指令重排序的也有一些规则:
a.当程序执行到volatile变量的读操作或者写操作时,在其前面的操作的更改肯定全部1已经进行,且结果已经对后面的操作可见;在后面的操作肯定还没有进行;
b.在进行指令优化时,不能将对volatile变量访问的语句放在其后面执行,也不能把volatile变量后面的语句放到其前面执行。
即:执行到volatile变量时,其前面的所有语句都执行完,后面所有语句都未执行。且前面语句的结果对volatile变量及其后面语句可见。
-
-
(先行发生原则)
-
单一线程原则:在单一线程内,前面的操作先于后面的操作
-
管程锁定原则:一个unlock操作,先于后面对同一个锁的lock操作
-
volatile变量原则:对一个volatile变量的写操作,先于后面对其读操作
-
线程启动原则:thread的start()方法,先于此线程每一个动作
-
线程加入原则:thread对象的结束,先于join()方法返回
-
线程中断原则:对线程interrupt()方法的调用,先于被中断线程的代码检测到中断事件的发生
-
对象终结原则:一个对象的初始化完成,先于它的finalize()方法的开始
-
传递性:a先于b,b先于c,a先于c
-
-
-
线程安全
-
不可变对象
不可变对象一定是线程安全的
不可变类型:
-
final修饰的基本数据类型
-
String
-
枚举
-
Number部分子类,如Long和Double等数值包装类型,BigInteger和BigDecimal等大数据类型
-
-
互斥同步(阻塞同步)——悲观锁
采用synchronized或者ReentrantLock进行同步从而达到线程安全
最重要的问题就是线程阻塞和唤醒所带来的性能问题。
-
非阻塞同步——乐观锁
- 版本号控制、CAS指令(CAS需要3个操作数,内存地址v,旧的预期值A,新值B。当执行操作时,只有当V的值等于A时,才将V的值更新为B)
- 乐观锁需要检测和操作这两个步骤具有原子性,靠硬件完成
- JUC里面的AtomicInteger调用了CAS操作
- ABA问题:初次读取为A,被改成B,再改回A,对于CAS操作就会误认为它从来没有被改变过。采用传统的互斥同步比原子类更有效。
-
无同步方案
要保证线程安全,并不是一定就要进行同步。如果一个方法本来就不涉及共享数据,那它自然就无须任何同步措施去保证正确性。
-
栈封闭
多个线程访问同一个方法的局部变量时,不会出现线程安全问题,因为局部变量存储在虚拟机栈中,属于线程私有的。
-
线程本地存储
如果一段代码所需要的数据,必须与其他代码共享,看看这些共享的数据,能否保证在同一个线程中执行。(根本不存在多线程竞争)
-
可重入代码(Reentrant Code 纯代码 Pure Code)
特征:不依赖存储在堆上的数据和公共的系统资源,用到的状态量都由参数中传入,不调用非可重入的方法
可重入代码,可以在代码执行的任何时刻中断它,转而去执行另外一段代码(包括递归调用它自己),在控制权返回后,原来的程序不会出现任何错误。
-
-
四、Java并发过程中锁:锁优化
-
自旋锁:和互斥锁类似,也是用户互斥资源的访问保护;只不过当线程请求的资源被加锁以后,当前进程不进入阻塞,而是死等自旋。
-
锁消除:对于“被检测出不存在竞争的共享数据的锁”进行消除
-
锁粗化(扩展锁范围):如果一连串的操作都是同一个对象反复加锁和解锁,会导致性能降低,JVM就会把加锁的的范围扩展(粗化)到整个操作系列的外部。
-
JDK1.6引入了轻量级锁和偏向锁,从而让锁拥有了四种状态:无锁状态、偏向锁状态、轻量级锁状态、重量级锁状态。
-
轻量级锁状态:是相对于传统重量级锁而言,使用了CAS操作避免了重量级锁使用互斥量的开销
-
偏向锁状态:偏向于第一个获取锁对象的线程,这个线程在之后再次获取该锁,就不在需要进行同步操作,甚至连CAS操作都不要。
但是当另一个线程去获取这个锁对象时,偏向状态结束。
-
JVM容器
一、ArrayList、Vector、CopyOnWriteArrayList、LinkedList
- ArrayList(基于动态数组,默认大小为10,扩容为原来的1.5倍复制,只序列化数组中有元素填充的那部分内容)
- Vector(与ArrayList类似,但是使用了synchronized进行同步)
- CopyOnWriteArrayList(读写分离,写操作在复制的数组上进行,并发需要加锁)
- LinkedList(基于双向链表)
二、HashMap、ConcurrentHashMap、LinkedHashMap、WeakHashMap
-
Hashmap
-
存储结构
保存了一个哈希table表,每个桶位置存储一个链表,链表中存放hash值相同的Entry节点;
使用拉链法解决冲突,链表采用头插法插入;允许插入key为null的节点
HashMap的默认大小是16,table长度;
-
扩容基本原理
capacity size threshold loadFactor
为了减少查找,需要保证每个桶的链表不要太长,同时适量增大table长度;
table的长度默认为16,一般为2的n次方,扩容每次变为原来的2倍;
传入容量可以不是2的n次方,自动转换为2的n次方
-
链表转换为红黑树
当链表长度大于8时,链表转换为红黑树,减少查找次数
-
-
ConcurrentHashMap
采用了分段锁,每个分段锁维护者几个桶;多个线程可以同时访问不同分段锁上的桶,从而提高并发度。
JDK1.8使用CAS指令,进一步提高并发度
-
LinkedHashMap
继承了HashMap,并且在内部还维护了一个双向链表,用来维护插入顺序或指责LRU顺序
-
WeakHashMap
继承了WeakReference,主要用于实现缓存,WeakHashMap存储的对象,会定时被JVM进行GC
数据库复习
数据库系统原理
一、事务、并发一致性问题、隔离级别
-
概念、ACID
-
并发一致性问题
-
脏读
-
不可重复读
-
幻读
-
丢失修改,两个事务都对一个数据进行修改,一个事务的修改覆盖了另一个事务的修改
-
-
隔离级别
为了避免上述并发不一致问题,防止事务之间的干扰;因此多个并发事务之间需要相互隔离。隔离等级分为4个级别,由低到高分别是:
- 读未提交(Read uncommitted):允许读取未提交的数据,最差的隔离级别
- 读已提交(Read committed):只允许读取已提交的数据
- 可重复读(repeatable read):只允许读取已提交的数据,并且在一个事务两次读取一个数据项期间,其他事务不得更新数据
- 可串行化(Serializable):保证所有的事务串行化调度
二、封锁(数据库锁)
-
封锁(数据库锁)
-
封锁粒度
表级锁和行级锁,封锁粒度和并发程度负相关
-
封锁类型
-
读写锁(互斥锁的一种)
写锁(排他锁,Exclusive,X锁)
读锁(共享锁,Shared,S锁)
-
意向锁
问题:在存在行级锁和表级锁的情况下,加表锁时,需要判断每一个行的行锁是否已加
意向锁在原来的X/S锁之上引入了IX/IS锁,都是表锁,用来表示一个事务想要加行锁时必须先获得IS/IX锁;这样当其他事务想要获取表锁时,只需判断表是否加了IX/IS锁。
-
-
(封锁协议)
-
三级封锁协议
一级封锁协议:写加X锁,解决丢失修改(X期间,不能再加X)
二级封锁协议:一的基础上,读加S锁,解决脏数据(读取必须加S锁,读完马上释放;X期间,不能加S)
三级封锁协议:二的基础上,读加S锁,解决不可重复读(读取时必须加S锁,直到事务结束才能释放;S期间,不能加X)
-
两段锁协议
问题:事务调度分为串行调度和并行调度;串行调度肯定能够保证调度结果的正确性,但是效率低;并行调度能同时处理多个事务,但是可能出现数据不一致问题;
解决办法:有一个具有串行调度结果的并行调度方法:两段锁协议就是保证事务可串行化的方法;
事务可串行化:多个事务的并发执行是正确的,当且仅当其结果与某一“次序”串行执行他们时的结果相同,称这种调度策略是可串行化调度。
两段式协议:所有事务必须分为两个阶段对数据项进行加锁和解锁。(在两个时间段分别同一加锁和解锁)
即事务分为两个阶段:
-
第一个阶段是获得封锁,事务可以获得任何数据项的任何锁,但是不能释放(扩展阶段,加锁不成功,则事务进入等待状态)
-
第二个阶段是释放封锁,事务可以释放人和数据项的任何锁,但是不能申请(收缩阶段,只能解锁,不能加锁)
MySQL的InnoDB采用两段锁协议,会根据隔离级别自动加锁隐式锁定,也可以手动显式锁定
-
-
-
三、多版本并发控制、Next-key Locks
-
多版本并发控制(Multi-Version Concurrency Comtrol,MVCC)
概念:MVCC是MySQL的InnoDB实现隔离级别的一种具体方式,用于实现提交读和可重复读这个两个级别。
未提交读总是读取最新的数据行,无需使用MVCC;可串行化隔离级别需要对所有读取的行都加锁,单纯使用MVCC无法实现。
问题:大多数MySQL的InnoDB引擎都不是使用简单的表锁和行锁机制,而是和MVCC一起使用。锁机制可以控制并发操作,但是系统开销较大,大部分情况下MVCC可以代替行级锁,使用MVCC可以降低系统开销。
实现:MVCC通过保存数据在某个时间点的快照来实现
不同存储引擎的MVCC实现是不同的,分为悲观并发控制和乐观并发控制
-
具体实现:InnoDB的MVCC,是通过在每行记录后面保存的两个隐藏的列来实现的,分别是这个行的“创建版本号和删除版本号”。
-
系统版本号:每开始一个事务,系统版本号就会递增
-
具体事务版本号:事务开始时的系统版本号
-
创建版本号:创建一个数据行快照时的系统版本号
-
删除版本号:删除一个数据行快照时的系统版本号,若数据行删除版本号<当前版本号,表示数据行被删除(标记删除)
-
Undo日志:MVCC使用到的数据快照存储在Undo日志中,可用于回滚。
-
-
区分快照读和当前读
- 快照读:使用MVCC读取的是快照中的数据,这样可以减少加锁带来的开销
- 当前读:读取的是最新的数据,需要加锁。
-
-
Next-Key Locks
是MySQL的InnoDB存储引擎、事务级别在可重复读的情况下使用的数据库锁的一种实现。
MVCC不能解决幻读的问题,在可重复读隔离级别下Next-Key Locks可以解决幻读问题。
-
Record Locks:锁定一个记录的索引,而不是记录本身(所有索引)
-
Gap Locks:锁定索引之间的间隙,但是不包含索引本身(所有间隙)
为了在可重复读下防止幻读
-
Next-Key Locks是二者结合(读的过程中,就不能发生修改)
-
四、三大范式、ER图
-
三大范式:
-
第一范式:每一列的字段属性不可分割
-
第二范式:每一行记录都需要有一个唯一主键
-
第三范式:每一行记录的其他属性都只依赖于主键,不依赖于其他属性
规范与性能的问题:关联查询的表,不得超过三张表
-
考虑商业化的需求和目标(成本、用户体验),数据库性能更加重要
-
在考虑性能的时候,适当考虑规范性
-
故意给一些表增加一些冗余字段(从多表查询变为单表查询)
-
故意增加一些计算列(从大数据量降低为小数据量的查询,索引)
-
SQL
一、数据库、数据表基本操作
二、CURD(指定字段去重别名、连接查询、where条件、分组过滤、排序分页、MySQL函数)
三、事务、索引、权限管理与备份、数据库字段设计与三大范式
四、JDBC和数据库连接池
+++++++++++++++++++++++++++++++++++++++++++++++++
MySQL
一、MySQL索引
-
B+树
B+树与红黑树的比较
-
B+树h更小,查找次数更少;
平衡树树高h=logmN,其中m为每个节点的出度;
红黑树的出度为2,B+树的出度一般都非常大。
-
磁盘IO预读特性可以一次读取B+树的一个叶子节点所有数据
磁盘IO操作比较费时,磁盘读取数据时并不是按需读取,而是会预读;
数据库系统将B+树节点大小设为一个页的大小,这样一次读取可以读取一个节点数据。
-
-
MySQL索引可能的数据结构
-
B+树索引
是大多数MySQL存储引擎的索引数据结构。
可以指定多个列作为索引列,多个列共同组成键;适用于全键值,键值范围,键最左前缀查找。
主索引为聚集索引,辅助索引。
-
哈希索引
-
索引的数据结构采用哈希表实现,查找时采用哈希查找;每个节点存储key值和数据记录的物理地址。(此时为非聚集索引)
-
哈希索引能以o(1)的时间进行查找,但是失去了有序性:
只支持精确查找,无法用于部分查找和范围查找;
无法用于排序与分组;
-
InnoDB有一个特殊功能:“自适应哈希索引”——当某个索引值被使用非常频繁时,会在B+树索引之上再为其创建一个哈希索引,加快查找。
-
-
全文索引
用于查找字段文本中的关键词,而不是直接比较是否相等。
查找条件采用Match against,而不是where;
全文索引采用倒排索引实现。
-
-
索引使用(优化)
-
多列索引比单列索引性能更好,多列索引最左边的列必须存在,采用最左前缀匹配
-
设计索引的时候,可以采用前缀索引,只使用列前缀的一部分作为索引的key
-
进行索引时,索引列不能是函数或者表达式,否则不使用索引
-
覆盖索引:一般来说除了聚集索引,索引文件的节点中包含key值和其他记录信息;查找时直接从索引文件中就可以查找到想要的数据,不必访问实际数据文件。
-
二、MySQL查询性能优化
使用Explain可以分析select语句的性能
-
优化数据访问
-
减少请求的数据量
只返回必要的列(尽量少使用select *)
只返回必要的行(使用limit限定返回结果)
缓存重复查询的数据
-
减少扫描的行数
使用索引
-
-
重构查询方式
-
切分大查询:一个大查询如果一次性执行的话,可能一次锁住很多数据,阻塞很多小的查询;
-
分解大连接查询:将大连接查询分解成对每个表进行一次单表查询,然后在应用程序中关联。
-
三、MySQL存储引擎、(MySQL数据类型、MySQL切分、MySQL主从复制读写分离)
-
MySQL的存储引擎
-
InnoDB引擎:支持事务、支持表级锁和行级锁、支持外键约束、不支持全文索引、表空间较大
在可重复读隔离级别下,通过“多版本并发控制MVCC”和“间隙锁next-key Locks”防止幻读
-
MyISAM引擎:不支持事务、只支持表级锁、不支持外键约束、支持全文索引、表空间较小
-
-
(MySQL数据类型)
tinyint,smallint,mediumint,int,bigint
float,double,decimal
varchar,char,text
datetime,timestramp
-
(MySQL切分)
-
水平切分:sharding,将一个表中的记录拆分到多个表中
-
垂直切分:将一张表按列切分成多个表
-
-
(MySQL主从复制读写分离)
主服务器处理写操作以及实时性要求比较高的读操作,从服务器处理读操作
代理服务器接收数据读写请求,再决定转发给那个服务器
Redis
一、Redis数据类型、Redis数据结构、Redis使用场景
-
Redis数据类型
键的类型只能是字符串
值支持5种数据类型:
-
字符串String,可以存储字符串、整数、浮点数
-
列表list
-
集合set
-
散列表hash,包含键值对的无序散列表
-
有序集合zset
-
-
Redis具体数据结构
-
字典:是集合的一种,集合中每个元素都是key-value键值对
-
跳跃表:是有序集合的底层实现之一,是基于多指针有序链表实现的,可以看成多个有序链表。
对于一个单链表来说,即使链表中存储的数据有序,查找数据也只能从头到尾遍历
想要提高效率,考虑在链表上建立索引,每两个节点提取一个节点到上一级。
查找时,从上层指针开始查找,找到对应区间之后再到下一层查找。
-
-
Redis的使用场景
-
缓存
-
会话缓存:存储多个应用服务器的会话信息,应用服务器无状态
-
消息队列:写入和读出消息
-
计数器
-
查找表:和缓存类似,如存储DNS记录;查找表内容不允许失效,缓存允许失效
-
分布式锁:分布式场景下,无法使用单机环境下的锁来对多个节点上的进程进行同步
-
二、Redis和Memcashed
都是非关系型内存键值数据库
- Redis支持5种不同的数据类型;Mencashed只支持字符串类型
- Redis支持两种持久化策略:RDB快照和AOF日志;而Memcashed不能持久化
- Redis支持分布式,Memcashed不支持分布式
- Redis中并不是所有数据都一直在内存中,可以将很久没用的value交换到磁盘;而Memcashed的所有数据都一直在内存中
三、Redis键的过期时间、数据淘汰策略、持久化、(事务、事件)
-
键的过期时间
-
数据淘汰策略(算法)
可以设置内存最大使用量,当内存使用量超出时就会淘汰;
-
从以设置过期时间的数据中选择:volatile-lru、volatile-ttl(将要过期)、volatile-random
-
从全部数据中选择:allkeys-lru、allkeys-random
禁止驱逐数据:noeviction
一般使用allkeys-lru算法
-
-
数据持久化:将数据从内存中持久化到磁盘上
- ROB快照:将某个时间点的所有数据,以快照形式存放到硬盘上
- AOF日志:以独立日志的方式记录“每一次的写命令”,在Redis重启时,重新执行AOF中的命令,以达到恢复数据的目的。
-
(事务)
一个事务包含多个命令,服务器执行事务期间,不会执行其他命令。
-
(事件)
Redis服务器是一个事件驱动程序
- 文本事件:服务器通过套接字与客户端或其他服务器通信,文本事件就是对套接字操作的抽象
- 时间事件:服务器有一些操作,需要在给定的事件点执行,时间事件是对这类定时操作的抽象
四、(Redis复制、sentinel、分片)
-
Redis主从复制
Redis主服务器,从服务器
可以设置主从链
-
Redis的Sentinel
哨兵,可以监听集群中的服务器,可以在主服务器下线时,自从从 从服务器中选举出新的主服务器
-
Redis的分片
将数据划分为多个部分,可以将数据存储到多台机器里面
计网复习
一、概述、体系结构
-
概述
网络的网络、ISP、主机之间的通信方式、电路交换和分组交换、时延
-
体系机构
OSI的7层网络模型、TCP/IP四层网络模型、五层协议
二、物理层、链路层
-
物理层
-
链路层
-
基本问题:封装成帧、透明传输、差错检测
-
信道分类
-
信道复用
-
CSMA/CD协议:广播信道信号传输
-
PPP协议:用户计算机与ISP进行通信
-
MAC地址,48位
-
局域网
-
以太网,是局域网的一种,是星型拓扑结构局域网;以太网帧格式
-
交换机
-
VLAN:以太网帧上加入VLAN标签,标识该帧属于哪个虚拟局域网
-
三、网络层、运输层
-
网络层
-
IP分组
-
IP地址
-
ARP协议:在通信过程之中,IP数据包的源IP地址和目的IP地址始终不变,而MAC地址随着链路的改变而改变
-
ICMP协议:差错报告报文和询问报文
-
VPN:内部网络之间通信,非内部网络之间使用隧道技术实现通信
隧道技术:对原IP数据报加密,作为新IP数据报数据部分并封装,发送
-
NAT:本地IP和全局IP的映射,一般使用端口号
-
路由器结构及其分组转发:路由选择处理机,路由选择算法,分组转发
-
路由选择协议
RIP、OSPF、BGP
-
-
运输层
-
UDP和TCP特点、报文
-
TCP三次握手和四次挥手
-
TCP可靠传输——确认+超时重传
-
TCP滑动窗口——发送窗口,接收敞口,累计确认
-
TCP流量控制——为了让接收方来得及接收
-
TCP拥塞控制——为了降低整个网络的拥塞程度、慢开始与拥塞避免、快恢复、快重传
-
四、应用层
-
应用层
-
DNS
-
FTP
-
DHCP
-
电子邮件(SMTP、POP3、IMCP)
-
远程登录TELNET
-
常用端口
-
Http:
一、http请求报文、http响应报文、HTTP方法(Get和Post的比较)、HTTP状态码、HTTP首部
-
http请求报文(请求行——请求方法+URL+协议版本,请求头部,请求数据)
-
http响应报文(状态行——协议版本+状态码+状态描述,响应头部,响应数据)
-
http方法
-
get,post
head(只返回响应头,不返回报文实体主体部分),确认url的有效性
put,patch,delete
options——查询指定URL支持的http方法,connect——要求与代理服务器建立隧道
-
get和post的区别
-
get主要用于请求资源,post主要用于传输数据
-
get请求的请求参数在url中,url本身对数据大小没有限制值,但是浏览器和服务器对url长度有限制,所以数据传输大小受限;
post请求的请求参数放在请求体中,不是通过URL传值,数据大小不受限制
-
get由于请求参数在url中,相对不安全;post的请求参数在请求体中,相对安全
-
-
-
http状态码
- 1XX 表示接收的请求正在处理,信息状态码
- 2XX 成功
- 3XX 重定向
- 4XX 客户端访问错误 403请求被拒绝,404not found
- 5XX 服务器错误 500服务器内部错误
-
http首部
-
通用首部字段
-
请求首部字段
-
响应首部字段
-
实体首部字段——包含在请求/响应报文中,body部分所使用的首部,补充相关信息
-
二、具体应用1:连接管理、cookie和session、缓存、(内容协商、内容编码)
-
连接管理
-
长链接和短连接——一次TCP链接能否进行多次HTTP通信,
http/1.0默认短连接,http/1.1默认长链接,connection
流水线和非流水线——长链接时,客户端能否连续发送请求
-
-
cookie
- http是无状态的,cookie是一个存储在浏览器的文本文件,发送请求时携带上在这个站点对应的cookie
session
- 服务器保存客户端的会话信息,为每个客户端保存一个session,由sessionID标识;服务器将sessionID在响应报文中放到cookie中,返回给浏览器。
-
缓存
缓解服务器的压力:让代理服务器缓存
降低客户端获取资源的延迟:让客户端浏览器进行缓存
-
(内容协商):服务器根据请求首部返回合适内容
-
(内容编码):返回内容可以压缩,告诉浏览器压缩算法就行
三、(具体应用2:范围请求、分块传输编码、多部分对象集合、虚拟主机、通信数据转发)
- 范围请求(遇请求中断,后续只请求部分内容)
- 分块传输编码(将数据分块,让浏览器逐步显示页面)
- 多部分对象集合
- 虚拟主机(一台服务器可以拥有多个域名,逻辑上看成多个服务器)
- 通信数据转发(代理:正向代理,反向代理、网关服务器、隧道通信)
四、HTTPS、HTTP/2.0 1.0 1.1
-
HTTPS
-
加密
- 对称加密
- 非对称加密
- https采用的加密方式:利用非对称加密传输对称加密的公钥,数据传输采用对称加密
-
认证:SSL证书(CA证书)
-
SSL提供了对报文摘要功能进行完整性保护
https的报文摘要结合了加密和认证,加密后的报文被篡改,很难重新计算报文摘要
-
-
HTTP/2.0
- 将报文分为headers帧和data帧,只有一个TCP连接存在,可以承载任意数量的双向数据流
-
HTTP/1.1
- 默认长链接,支持流水线
- 支持同时打开多个TCP链接
操作系统复习
一、概述:特征、基本功能、系统调用、大内核和小内核、中断
- 特征(并发、共享、虚拟、异步)
- 基本功能(进程管理、内存管理、文件管理、设备管理)
- 系统调用(进程在用户态态通过系统调用请求内核协助完成某些任务)
- 大内核和微内核(大内核将整个操作系统放入内核;微内核将操作系统分层成模块,只将部分核心模块放在内核,需要频繁在用户态和核心态切换)
- 中断(内部中断(异常)、外中断、陷入(使用系统调用))
二、进程管理(进程与线程、进程状态的切换、进程调度算法、进程同步、进程通信、死锁)
-
进程与线程
-
进程状态的切换(就绪态,运行态,阻塞态)
-
进程调度算法
- 批处理系统:FIFS、短作业优先、最高响应比优先;最短剩余时间优先(抢占式)
- 交互式系统:时间片轮转,优先级调度,多级反馈队列优先
-
进程同步:多个进程按一定顺序执行
-
条件变量与互斥锁
-
信号与信号量(信号量是一个整形变量)
进程同步问题:生产着消费者、读者写者问题、哲学家进餐问题
-
-
进程通信:进程间传输信息
共享存储、管道、消息队列、信号量、套接字(可用于不同机器间的进程通信)
-
死锁
- 必要条件:资源互斥、占有且等待、不可抢占、存在循环等待链
- 处理方法:死锁避免(破坏四个条件)、死锁预防(银行家算法,安全状态),死锁检测与恢复
三、内存管理(虚拟内存、分页系统地址映射 页面置换算法、分段、段页式)
-
虚拟内存:程序不需要全部调入内存就可以运行;剩余的部分放入在外存中,在需要的时候进行页面置换;让物理内存扩充为更大的逻辑内存。
-
分页系统地址映射:内存和磁盘按存储块分页
虚拟内存采用的是分页技术,将程序地址空间划分成固定大小的页,每一页再与物理内存进行映射。
-
页面置换算法:最佳置换,先进先出,最近最久未使用,clock算法
-
分段:将程序地址空间划分为段
-
段页式:先将程序地址空间划分成多个拥有独立地址空间的段,每个段上的地址空间再划分成大小相同的页
+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
四、设备管理(磁盘结构、磁盘调度算法)
-
磁盘结构
-
磁盘调度算法
先来先服务,最短寻道时间优先
电梯算法(未改进、改进)
Linux:
一、常用操作和基本概念
二、磁盘、文件系统
三、文件和目录基本操作、搜索操作、获取文件内容操作、压缩和打包、scp
四、管道、正则表达式、Bash、进程管理
系统设计 面向对象
语言:
- 了解Java语言的基本特性、Java容器如List和HashMap;
- 简单了解Java并发、简单了解JVM如运行时的数据区域、垃圾回收算法、内存分配、类的加载等等;
数据结构:
- 了解数组、链表、栈、二叉树和哈希表等基本数据结构;
- 了解排序、回溯和动态规划等算法。
计算机网络:
- 了解TCP/IP,http等协议,了解socket通信IO模型,能搭建简单的客户端和服务端通信。
操作系统:
- 了解进程和线程,死锁、内存管理和设备管理;
- 了解基本Linux开发环境和操作命令;
- 会一些简单的git操作
数据库:
- 了解基本SQL操作、事务以及并发一致性问题;
- 了解MySQL的索引和简单性能优化;
- 了解Redis使用场景;
MySQL索引详解
摘要
- MySQL支持多种数据引擎,各种数据引擎对索引的支持各不相同;因此MySQL数据库支持多种索引类型,如B+树索引、哈希索引、全文索引等。本文只讨论B+树索引
- 文章分为三个部分:
- 索引的本质(B树和B+树)
- 结合MySQL数据库中的MyISAM和InnoDB存储引擎的索引实现,讨论聚簇索引、非聚簇索引
- 讨论MySQL中索引的使用策略、是否为字段建立索引及建立索引的优化策略、主键选择及记录插入优化
一、索引的本质、B树与B+树
(一)索引的本质
-
概念
-
索引是“帮助MySQL高效获取数据”的“数据结构
-
数据库查询:顺序查找、二分查找、二叉树查找,每种查找都只能应用在特定的数据结构上。
因此,数据库系统,除了保存数据之外,还维护着满足特定查找算法的数据结构;
这些数据结构的每个节点并不存储数据记录本身,而是以某种方式引用(指向数据)。
这样就可以在这些数据结构上实现高级查找算法,这种数据结构就是索引。
-
-
看一个例子
左边是真实的存储数据的数据表,数据存储时不一定相邻;
右边维护了一个二叉查找树的索引数据结构,每个节点包含一个索引键值,和一个指向对应“数据记录”物理地址的指针;这样就可以运用二叉查找树来加快数据查找。
这是一个货真价实的索引,但是实际的数据库几乎没有使用二叉查找树或红黑树实现的。
(二)B树和B+树
二叉查找树,(红黑树),平衡二叉查找树,B树,B+树,B*树
目前大部分数据库系统及文件系统都采用B树或者B+树作为索引结构
1.B树
-
概念:又称多路平衡查找树,B树中所有节点的孩子节点数的最大值称为B树的阶,通常用m表示。
-
一棵m阶B树可以为空树,或满足如下特性的m叉树:
-
树中每个节点最多有m棵子树,即最多含有m-1个关键字
-
若根节点不是终端节点,则至少含有两棵子树
-
除根节点外的所有非叶节点至少含有[m/2] 棵子树,至少含有[m/2]-1个关键字(向上取整)
-
非叶节点的结构如下:每个节点包含k个关键字,k+1个指针;关键字Ki左边指针指向的子树中所有节点的关键字均小于Ki,右边指针指向的子树中所有节点的关键字均大于Ki
-
所有的叶子结点都出现在同一层,并且不带信息(可以视为外部节点活在这类似于折半查找判定树的查找失败节点,实际上这些节点不存在,只想这些节点的指针为空)
B树是所有节点的平衡因子均等于0的多路查找树。
-
-
B树的检索:在B树中查找节点,再在节点内二分查找关键字
算法:首先在根节点内二分查找,如果找到则返回对应的data;否则对相应区间的指针指向的节点递归查找;直到找到节点或只找到null指针,前者查找成功,后者查找失败。
伪代码:
BTree_Search(node.key){ //递归出口 if(node==null) return null; //查找本节点 for(int i=1;i<=m;i++){ if(node.key[i]==key) return node.data[i];//返回数据记录(实际为物理指针) else if(node.key[i]>key) return BTree_Search(point[i]->node);//左子树查找 else return BTree_Search(point[i+1]->node);//右子树查找 } } data=BTree_Search(root,my_key);
B树的插入、删除此处不讨论;
-
B+树
-
B+树是B树的变种,MySQL普遍采用B+树实现其索引结构
-
一棵m阶的B+树应该满足下列条件:
-
每个分支节点最多有m棵子树(子节点)
-
非叶根节点至少有两棵子树,其他每个分支节点至少有[m/2]棵子树(向上取整)
-
节点的子树个数和关键字个数相等
-
所有叶节点包含全部关键字及指向相应记录的指针,叶节点中将关键字按大小排序排列,并且相邻叶节点按大小顺序相互链接起来。
-
所有分支节点(可视为索引的索引)中仅包含它的各个子节点(即下一级的索引块)中关键字的最大值及指向其子节点的指针。
-
-
m阶B+树和m阶B树的主要差异如下:
-
在B+树中,具有n个关键字的节点含有n棵子树,即每个关键字对应一棵子树;而在B树中,具有n个关键字的节点含有n+1棵子树
-
在B+树中,每个节点(非根内部节点)的关键字个数n的范围是[m/2]<=n<=m(根节点:1<=n<=m)。在B树内,每个节点(非根内部节点)的关键字个数n的范围是[m/2]-1<=n<=m-1(根节点:1<=n<=m-1)
-
在B+树中,叶节点包含信息,所有非叶节点仅起到索引所用,非叶节点的每个索引项只含有对应子树的最大关键字和指向该子树的指针,不含有该关键字对应记录的存储地址。
-
在B+树中,叶节点包含了全部关键字,即在非叶节点中出现的关键字也会出现在叶节点中;而在B+树中,叶节点的关键字和其他重复节点的关键字是不重复的。
-
-
B+树的查找
-
在B+树中有两个头指针,一个指向根节点,一个指向关键字最小的叶子结点。因此可以对B+树进行两种查找,一种是从根节点开始的多路查找,一种是从最小关键字开始的顺序查找。
-
多路查找过程中,当非叶节点上的关键字等于给定值时并不终止,而是继续向下查找,直到找到叶节点上的该关键字为止。
因此在B+树中查找,无论查找成功与否,每次查找都是从根节点到叶节点的路径
-
-
B+树的每个叶子结点有一个指向相邻叶子节点的指针,提高了区间访问的性能。
-
(三)索引的数据结构为什么选择B树或者B+树
红黑树等数据结构也可以用来实现索引,结合计算机组成原理讨论选择B树或B+树的原因。
-
索引本身一般也很大,索引往往以文件的形式存储在磁盘上;
这样索引查找过程中就要产生磁盘IO,相对于内存存取,磁盘IO的时间消耗要高几个数量级;
所以评价一个数据结构作为索引的优劣:索引的数据结构要尽量减少磁盘IO的次数
-
内存的存取原理
主存目前主要是RAM
-
主存是由一系列的存储单元组成的矩阵,每个存储单元存储固定大小的数据,每个存储单元有唯一的地址;
此处简化了一个二维地址,通过行地址和列地址可以唯一定位一个存储单元,图示为一个4*4的主存模型。
-
主存的存取过程:
地址信号定位存储单元,将存储单元的数据放到数据总线上,供外部读写。
存取过程较快。
-
-
磁盘的读取原理
-
磁盘读取存在机械运动费时,磁头先定位到相应盘片,再定位到磁道(寻道时间),最后定位到扇区(旋转时间);每个扇区是磁盘的最小存储单元。
-
当需要从磁盘读取数据时,系统会将物理地址传给磁盘,磁盘中的控制电路按照寻址逻辑将逻辑地址翻译成物理地址,即确定盘片,磁道,扇区。
-
局部性原理与磁盘预读:
预读:为了尽量减少磁盘IO,磁盘往往不是严格按需读取,而是每次都会预读;即使只需要一个字节,磁盘也会从这个位置开始,顺序向后读取一定长度的数据放入内存。
局部性原理:程序运行期间所需要的数据同城比较集中。当一个数据用到时,其附近的数据也通常会马上被使用。
由于磁盘的顺序读取效率很高(不再需要寻道时间,只需要很少的旋转时间),因此对于局部性较好的的程序来说,预读可以提高IO效率
-
页面:预读的长度一般为页(page)的整数倍。
操作系统往往将主存和磁盘存储区分割为连续的大小相等的块,每一个存储块成为一页(通常为4K),主存和磁盘以页为单位交换数据。
当程序要读取的数据不在主存中是,缺页中断。
-
-
B树的索引性能分析
-
索引数据结构的优劣取决于磁盘IO次数;
-
在B树中,每次检索都访问不超过h个节点(h为B树高度);并且数据库系统巧妙地利用磁盘预读原理,将每个节点的大小设为一个页,这样每个节点只需要一次磁盘IO就可以完全载入。
-
为了达到这个目的,在实际实现B树时还需要使用如下技巧:
- 每次新建节点时,直接申请一个页的空间
- B树中一次检索最多只需要h-1次磁盘IO(根节点常驻内存);一般而言,h=logmN,h会比较小
综上所述,用B树作为索引结构效率是非常高的。
而用红黑树,h明显要深的多。由于逻辑上很近的节点(父子)物理上可能很远,无法利用局部性,所以红黑树的IO复杂度也比B树差很多
-
相对于B树,B+树更适合索引,原因和内节点出度m相关。
从上面的分析来看,m越大,h越小,性能越好;B树和B+树差不多。
B+树的内节点只存储子树的最大值关键字,只有叶节点存储记录数据,因此每次都会进行h次磁盘IO,但是B+树所有叶子结点都链接起来了,便于读取局部性连续数据。
-
二、MySQL索引的实现
在MySQL中,不同的存储引擎索引的实现方式不同。
(一)MyISAM索引的实现
- MyISAM引擎采用了B+树作为索引结构
MyISAM的索引文件仅仅保存数据记录的主键和物理地址;
-
在MyISAM中,主索引和辅助索引在结构上没有区别;只是主索引要求key是唯一的,而辅助索引key可以重复。
如果我们在Col2上建立一个辅助索引,则此索引的结构如下图所示:
-
MyISAM中索引检索:
首先按照B+树搜索算法搜索索引,如果指定的key存在,则取出data域的值(相应记录的物理地址);
然后再去数据存储区域,读取相应的数据记录。
-
MyISAM的索引方式也叫做“非聚集”的,之所以这么称呼是为了与InnoDB的“聚集索引”区分
(二)InnoDB索引的实现
虽然InnoDB也使用B+树作为索引结构,但是具体的实现方式和MyISAM截然不同。
-
区别一:InnoDB的数据文件本身就是索引文件。
MyISAM中索引文件和数据文件是分离的,索引文件仅仅保存了数据记录的地址。
而InnoDB中,数据文件本身就是按照B+树组织的一个索引结构;这棵树的叶节点data域保存了完整的数据记录。
索引的key就是数据表的主键,因此InnoDB表数据文件本身就是主索引文件。
可以看到叶节点包含了完整的数据记录,这种索引叫做“聚集索引”
因为InnoDB的数据文件本身就是按主键聚集,所以InnoDB要求表必须有主键(MyISAM可以没有);如果没有显式指定,MySQL系统会自动选择一个可以唯一标识数据记录的列作为主键;如果不存在这种列,则MySQL自动为InnoDB表生成一个隐含字段作为主键。
-
区别二:InnoDB的辅助索引data域存储相应数据记录主键的值,而不是物理地址。
换句话说,InnoDB所有辅助索引都引用主键作为data域。
例如,下图为定义在Col3上的一个辅助索引
-
检索:
聚集索引,使得主键的检索十分有效;
而辅助索引检索,需要检索两边索引,首先检索辅助索引获得主键,然后用主键到主索引中检索获得记录。
了解不同存储引擎的“索引实现方式”对于“正确使用’和”优化索引“都非常有帮助;
- 不建议使用过长的字段作为InnoDB的主键,因为所有辅助索引都引用主索引,过长的主索引会令副主索引变得过大;
- 用非单调的字段作为主键在InnoDB中不好,因为InnoDB数据文件本省就是一颗B+树,非单调的主键会造成在插入新记录时数据文件为了维持B+树的特性而频繁分裂挑真正,十分低效,而使用自增字段作为主键则是一个很好的选择。
三、索引使用策略及优化
MySQL的优化主要分为结构优化和查询优化。此处讨论的“高性能索引策略”属于结构优化范畴。
示例数据库:
(一)索引匹配策略:最左前缀原理
联合索引的概念:MySQL的索引可以以一定顺序引用多个列,一般一个联合索引是一个有序元组<a1,a2,…,an>,其中各个元素均为数据表的一列;
另外单列索引可以看成联合索引元素数为1的特例。
-
以employees.titles表为例,下面先查看其上都有哪些索引:
-
全列匹配
当按照索引中所有列进行精确匹配(这里的精确匹配指“=”或“IN”匹配)时,索引可以被用到。
理论上索引对顺序是敏感的,但是由于MySQL的查询优化器会自动调整where子句的条件顺序以使用适合的索引。
-
最左前缀匹配
使用了索引,但是key_len为4,说明只使用到了索引的第一列前缀
-
查询条件用到了索引中列的精确匹配,但是中间某个条件未提供
后面的from_data虽然也在索引中,但是由于title不存在而无法与左前缀链接。
可以手工补充中间缺失列,如果中间列元素较少且已知:
-
查询条件没有指定索引第一列
-
匹配某列的前缀字符串
-
范围查询
范围列可以用到索引(必须是最左前缀),但是范围列后面的列无法用到索引。
同时索引最多用于一个范围列,因此如果查询条件中有两个范围列则无法全用到索引。
explain无法区分范围索引和多值匹配,用“between”并不意味之范围查询,相当于多值匹配“IN”
-
查询条件中含有函数或表达式
此时MySQL不会为这列使用索引
(二)是否为字段建立索引、建立索引的优化策略
-
问题:索引可以查询速度,但是也有代价:索引文件本身要消耗存储空间;同时索引会加重插入、删除、和修改记录时的负担、另外,MySQL在运行时也要消耗资源维护索引;
因此,索引并不是越多越好,一般两种情况下不建议使用索引。
-
表记录较少时,没必要建立索引;
让查询做全表扫描就好,阈值在2000左右
-
当索引的选择性较低时,不建议建立索引;
索引的选择性:指不重复的索引值与表记录数的比值。
当重复键值太多,索引价值不大。
-
为列建立索引时的一个优化策略:前缀索引——用列的前缀替代整个列作为索引的key。
当前缀长度合适时,可以做到既使得前缀索引的选择性接近全列索引;同时因为索引key变短而减少了索引文件的大小和维护开销。
(三)InnoDB的主键选择、与插入优化
-
主键选择
在使用InnoDB存储引擎时,如果没有特别的需要,请永远使用一个与业务无关的自增字段作为主键。
-
与插入优化
-
InnoDB使用聚集索引,数据记录本身被存在主索引(一个B+树)的叶子结点上。
这就要求同一个叶子结点内(大小为一个内存页或磁盘页)的各条数据记录按主键顺序存放;
因此每当有一条新的记录插入时,MySQL会根据其主键将其插入适当的节点及其位置中;如果页面达到装载因子(InnoDB默认为15/16),则开辟一个新的页(节点)
-
如果表使用自增主键,那么每次插入新的记录,记录就会在当前索引叶子节点的最后面添加,当一页写满,就会自动开辟一个新的页。
这样就会形成一个紧凑的索引结构,近似顺序填满。
由于每次插入也不需要移动已有数据,因此效率很高,也不会增加很多开销在维护索引上。
-
如果表使用非自增主键(如身份证号或学号),由于每次插入主键的值近似于随机,因此每次新纪录都要被插入到现有索引页的中间某个位置:
此时MySQL不得不为了将新记录插入到合适位置而移动数据。
甚至目标页面可能已经被写到磁盘上而从缓存中清理掉,此时又要从磁盘上读回来,这增加了很多开销;同时频繁的移动、分页操作造成了大量的碎片,得到了不够紧凑的索引结构,后续不得不通过OPTIMIZE TABLE来重建表并优化填充页面。
因此,只要可以,请尽量在InnoDB上采用自增字段做主键。
-
web安全考虑问题:
-
SQL注入问题,数据库本地访问
-
https,加密,ssl证书,报文摘要
-
token问题
-
使用session和内存数据库,少使用cookie
-
post和get
web性能优化
- cdn
- 负载均衡
- 消息队列
- 主从数据库