面试小抄
马上面了,光速打点小抄背背
数据结构与算法
排序
快速排序
- 确定基准值:随机一点x
- 调整区间:使得x左边的数<=x x右边的数>=x
- 递归
def quick_sort(nums):
# 定义递归的终止条件:如果列表为空或只包含一个元素,直接返回
if len(nums) <= 1:
return nums
# 选择基准值(可以选择任何元素,这里选择中间元素)
pivot = nums[len(nums) // 2]
# 初始化左右子数组
left = [x for x in nums if x < pivot] # 小于基准值的元素放入左子数组
middle = [x for x in nums if x == pivot] # 等于基准值的元素放入中间子数组
right = [x for x in nums if x > pivot] # 大于基准值的元素放入右子数组
# 递归调用快速排序,并拼接左子数组、中间子数组和右子数组的排序结果
return quick_sort(left) + middle + quick_sort(right)
归并排序
- 选取中间点
- 递归排序左右,分成若干个长度为1或0的子列(不能再分即为有序)
- 合并:两个指针遍历左右两个部分
def merge_sort(nums):
# 定义递归的终止条件:如果列表为空或只包含一个元素,直接返回列表
if len(nums) <= 1:
return nums
# 将列表分成两半
mid = len(nums) // 2
left_half = nums[:mid]
right_half = nums[mid:]
# 对左右两半分别调用归并排序函数
left_half = merge_sort(left_half)
right_half = merge_sort(right_half)
# 将排好序的左右两半合并起来并返回
return merge(left_half, right_half)
def merge(left, right):
merged = [] # 用于存储合并后的列表
left_index, right_index = 0, 0 # 分别用于追踪左右两个列表的当前元素索引
# 循环直到左右两个列表都被遍历完
while left_index < len(left) and right_index < len(right):
# 比较左右两个列表的当前元素,将较小的元素添加到合并列表中
if left[left_index] < right[right_index]:
merged.append(left[left_index])
left_index += 1
else:
merged.append(right[right_index])
right_index += 1
# 将剩余的元素添加到合并列表中
merged.extend(left[left_index:])
merged.extend(right[right_index:])
return merged
二分
def binary_search(nums,i):
head = 0
end = len(nums)-1
while head<=end: #终止条件小于等于
mid = (head+end)//2
if nums[mid] == i:
return mid
elif nums[mid] < i:
head = mid+1
else:
end = mid-1
前缀和
构建一个长度多1的,内容为前n项和的数组
def prefix_sum(nums):
lenth = len(nums)
prefixed_sum = [0]*(lenth+1)
for i in range(1,lenth+1):
prefixed_sum[i] = prefixed_sum[i-1] + nums[i-1]
return prefixed_sum
def range_sum(prefixed_sum,start,end):
return prefixed_sum[end+1] - prefixed_sum[start]
差分
构建一个长度一致的,内容为n+1 - n的差的数组
def difference_array(nums):
length = len(nums)
diff = [0] * length
diff[0] = nums[0]
for i in range(1,length):
diff[i] = nums[i] - nums[i-1]
return diff
差分常用来实现区间增量(在给定的数组的一段中加上某值)
def update_range(diff, start, end, val):
diff[start] += val
if end + 1 < len(diff):
diff[end + 1] -= val
def apply_updates(nums, diff):
for i in range(len(nums)):
if i == 0:
nums[i] = diff[i]
else:
nums[i] = diff[i] + nums[i - 1]
差分数组的start位加上val,如果end位不是末位,则再于end减去val,最后重新构造原数组
八股
主要是计网,os和数据库
没学过os课程,自己也没钻研过,后面有的看了
OS
CPU cache
三级缓存所有核心共享,一级二级缓存单独每个核心使用
访问cache的速度比内存快很多,所有要尽量写出缓存命中的代码
如何提高数据缓存的命中率?
eg : 二维数组遍历[i][j]
快于[j][i]
因为前者访问顺序是连续的
如何提高指令缓存的命中率
eg : 先排序再遍历快于先遍历再排序 因为分支预测器预测接下来的条件真假会提前缓存指令
如何提高多核cpu的缓存命中率
因为一二级缓存由每个核心独有,如果一个线程在不同核心来回切换,各个核心的缓存命中率就会受到影响,所以要把线程绑定在某一个 CPU 核心上
缓存一致性
写传播:当某个cpu核心写入时,把该事件广播给其他核心
事物的串行化 :保证各个核心收到的事件顺序一致 加入“锁”
伪共享
地址连续的两个变量如果被不同的核心上的不同线程调用的话,进行修改时会频繁同步浪费时间,使得cache失效
对于多个线程共享的,经常被修改的数据应该避免放在一个cache line中
eg Java Disruptor 中有一个 RingBuffer类使用final变量填充
内存管理
虚拟内存的作用
- 第一,虚拟内存可以使得进程对运行内存超过物理内存大小,因为程序运行符合局部性原理,CPU 访问内存会有很明显的重复访问的倾向性,对于那些没有被经常使用到的内存,我们可以把它换出到物理内存之外,比如硬盘上的 swap 区域。
- 第二,由于每个进程都有自己的页表,所以每个进程的虚拟内存空间就是相互独立的。进程也没有办法访问其他进程的页表,所以这些页表是私有的,这就解决了多进程之间地址冲突的问题。
- 第三,页表里的页表项中除了物理地址之外,还有一些标记属性的比特,比如控制一个页的读写权限,标记该页是否存在等。在内存访问方面,操作系统提供了更好的安全性。
进程
进程和线程的区别?
- 进程是资源分配的最小单位,线程是CPU调度的最小单位
- 每个进程有自己的PCB,有单独的代码和地址空间。而一个进程下的线程共享代码和地址空间。
- 线程相比于进程,线程间切换的消耗更小。
- 线程崩溃整个程序崩溃,进程崩溃不会影响其他进程
进程间通信的方式?
- 管道(匿名管道):半双工 仅在存在父子关系的进程中通信 shell中的
|
- 命名管道FIFO :
ll
可发现文件类型为p
- 消息队列: 保存在内核中的消息链表 不适合较大数据的传输
- 共享内存: 取出一段虚拟地址空间,映射到相同的物理地址(有冲突问题)
- 信号量: 解决共享内存的冲突问题
- 套接字
- 信号: 给某进程发送一个代表执行特定操作的信号值
互斥与同步
为了实现进程/线程间正确的协作,操作系统必须提供实现进程协作的措施和方法,主要的方法有两种:
- 锁:加锁、解锁操作;
- 信号量:P、V 操作;
锁用来解决互斥线/进程访问同一个资源时产生的冲突问题
任何想进入临界区的线程,必须先执行加锁操作。若加锁操作顺利通过,则线程可进入临界区;在完成对临界资源的访问后再执行解锁操作,以释放该临界资源。
信号量用于协调共享资源访问
信号量的值sem
表示资源的数量
- P 操作:将
sem
减1
,相减后,如果sem < 0
,则进程/线程进入阻塞等待,否则继续,表明 P 操作可能会阻塞; - V 操作:将
sem
加1
,相加后,如果sem <= 0
,唤醒一个等待中的进程/线程,表明 V 操作不会阻塞;
死锁?
在操作共享资源时加入锁可能会造成当两个线程为了保护两个不同的共享资源而使用了两个互斥锁后两个线程都在等待对方释放锁,此时程序无法继续运行下去
要避免死锁,可以使用资源有序分配法,来破环环路等待条件
- 线程 A 和 线程 B 获取资源的顺序要一样,当线程 A 是先尝试获取资源 A,然后尝试获取资源 B 的时候,线程 B 同样也是先尝试获取资源 A,然后尝试获取资源 B。也就是说,线程 A 和 线程 B 总是以相同的顺序申请自己想要的资源
网络
分层
TCP/IP模型
- 应用层
- 传输层
- 网络层
- 网络接口层
浏览器键入网址到网页显示,期间发生了什么
- 解析url:应用层协议(http)服务器名称(域名dns)路径
- 生成http请求:构造一个HTTP请求报文,其中包括请求方法、URL、HTTP版本、请求头部等信息
- 浏览器调用socket库,由os的协议栈传输,其组成:1、负责收发数据的TCP/UDP 2、由IP协议控制的网络包收发
- 经过传输层(tcp)网络层(ip)链路层(mac)层层打包
- 网卡在报文开头加上报头和起始帧分界符,在末尾加上用于检测错误的帧校验序列,并转换为电信号发送
- 经过交换机根据 MAC 地址表查找 MAC 地址,然后将信号发送到相应的端口
- 经过路由器,查询路由表,转发
- 到达服务器,逐层解包头部
路由器,交换机,网关?
交换机:
建立一张mac地址和端口的对应表,查看收到的包中接收方的mac地址,转发到对应的端口上
基于以太网设计,位于链路层,端口没有mac地址
路由器:
校验,查询收到的包中接收方mac地址是否是自己,否则丢弃包。之后去掉mac头部,根据ip包中的内容计算下一条的ip地址并转发
位于网络层,端口具有ip和mac地址
网关:
连接不同网络并进行协议转换的设备或软件
运行在网络层以上,可以处理更高级别的协议,如HTTP、FTP等
应用层
http状态码
- 2xx 表明服务器成功处理了客户端请求: 200 一切正常 204 没有body数据 206 返回的body仅是资源的一部分
- 3xx 重定向,表明客户端请求的资源发生了变动: 301 永久重定向,需要用新的url请求 302 暂时重定向
- 4xx 客户端报文有误: 404 请求的资源找不到 415 请求格式错误 401 无权限
- 5xx 服务器处理错误: 501 请求的功能暂不支持 502 访问后端服务器出错 503 服务器忙暂时无法响应
GET和POST
GET 的语义是从服务器获取指定的资源
POST 的语义是根据请求负荷(报文body)对指定的资源做出处理
HTTP和HTTPS
http明文传输信息,tcp三次握手建立连接后就可以传输,默认使用80端口
https在tcp和http之间加入SSL/TLS安全协议加密传输,需要向CA购买证书,默认使用443端口
对称加密和非对称加密
非对称加密:
生成一对公钥和私钥(eg:ssh连接时使用) 公钥加密的内容只能用私钥解密
对称加密:
解密和加密的密钥是一致的
HTTPS如何使用加密和非对称加密
服务器在收到客户端的请求后会返回自己的证书,其中包含了服务器的公钥,客户端验证证书的合法性后,生成一个随机的(对称)密钥,使用收到的公钥加密这个密钥并送回给服务器,服务器使用私钥解密后就得到了这个对称的密钥,之后的通信使用其进行对称加密
详细版HTTPS如何建立连接
TLS 协议建立的详细流程:
1. ClientHello
首先,由客户端向服务器发起加密通信请求,也就是 ClientHello
请求。
在这一步,客户端主要向服务器发送以下信息:
(1)客户端支持的 TLS 协议版本,如 TLS 1.2 版本。
(2)客户端生产的随机数(Client Random
),后面用于生成「会话秘钥」条件之一。
(3)客户端支持的密码套件列表,如 RSA 加密算法。
2. SeverHello
服务器收到客户端请求后,向客户端发出响应,也就是 SeverHello
。服务器回应的内容有如下内容:
(1)确认 TLS 协议版本,如果浏览器不支持,则关闭加密通信。
(2)服务器生产的随机数(Server Random
),也是后面用于生产「会话秘钥」条件之一。
(3)确认的密码套件列表,如 RSA 加密算法。
(4)服务器的数字证书。
3.客户端回应
客户端收到服务器的回应之后,首先通过浏览器或者操作系统中的 CA 公钥,确认服务器的数字证书的真实性。
如果证书没有问题,客户端会从数字证书中取出服务器的公钥,然后使用它加密报文,向服务器发送如下信息:
(1)一个随机数(pre-master key
)。该随机数会被服务器公钥加密。
(2)加密通信算法改变通知,表示随后的信息都将用「会话秘钥」加密通信。
(3)客户端握手结束通知,表示客户端的握手阶段已经结束。这一项同时把之前所有内容的发生的数据做个摘要,用来供服务端校验。
上面第一项的随机数是整个握手阶段的第三个随机数,会发给服务端,所以这个随机数客户端和服务端都是一样的。
服务器和客户端有了这三个随机数(Client Random、Server Random、pre-master key),接着就用双方协商的加密算法,各自生成本次通信的「会话秘钥」。
4. 服务器的最后回应
服务器收到客户端的第三个随机数(pre-master key
)之后,通过协商的加密算法,计算出本次通信的「会话秘钥」。
然后,向客户端发送最后的信息:
(1)加密通信算法改变通知,表示随后的信息都将用「会话秘钥」加密通信。
(2)服务器握手结束通知,表示服务器的握手阶段已经结束。这一项同时把之前所有内容的发生的数据做个摘要,用来供客户端校验。
可以发现是四次握手(TLS1.2)
HTTP 1.1 2.0 3.0?
- HTTP 1.1 引入了长连接(keep-alive)只要客户端不发出断开连接的请求就会一直保持连接降低开销
- HTTP 2.0 压缩头部:如果多个请求header相同会去重 二进制:header和body都使用二进制 并发传输 服务器推送
- HTTP 3.0 基于UDP,使用QUIC协议可以实现类似TCP的可靠传输
WebSocket
服务器推送实现:http轮询/长轮询(延长超时,减少请求次数)
websocket:实现全双工通信
高并发
- html静态化
- 图片服务器分离(图床,OSS)
- 负载均衡
- 数据库集群(master/slave)库表散列(将数据表分割成多个表)
- 缓存
- 镜像
服务器宕机
- 准备两个相同但ip不同的网站,一个挂了可以修改dns解析迅速启用另一个
- ups供电
- 经常进行安全检查,及时发现并修补漏洞
传输层
TCP
IP协议仅仅尽最大可能,但不可靠,但要实现可靠传输
面向连接的、可靠的、基于字节流
问题:
- 只能一对一通信,要先建立连接损耗大
- “粘包”(使用消息头+消息体格式解决)
TCP 四元组可以唯一的确定一个连接,四元组包括如下:
- 源地址
- 源端口
- 目的地址
- 目的端口
TCP三次握手和四次挥手
三次握手
- 一开始,客户端和服务端都处于
CLOSE
状态。先是服务端主动监听某个端口,处于LISTEN
状态 - 客户端会随机初始化序号(
client_isn
),将此序号置于 TCP 首部的「序号」字段中,同时把SYN
标志位置为1
,表示SYN
报文。接着把第一个 SYN 报文发送给服务端,表示向服务端发起连接,该报文不包含应用层数据,之后客户端处于SYN-SENT
状态。 - 服务端收到客户端的
SYN
报文后,首先服务端也随机初始化自己的序号(server_isn
),将此序号填入 TCP 首部的「序号」字段中,其次把 TCP 首部的「确认应答号」字段填入client_isn + 1
, 接着把SYN
和ACK
标志位置为1
。最后把该报文发给客户端,该报文也不包含应用层数据,之后服务端处于SYN-RCVD
状态。 - 客户端收到服务端报文后,还要向服务端回应最后一个应答报文,首先该应答报文 TCP 首部
ACK
标志位置为1
,其次「确认应答号」字段填入server_isn + 1
,最后把报文发送给服务端,这次报文可以携带客户到服务端的数据,之后客户端处于ESTABLISHED
状态。 - 服务端收到客户端的应答报文后,也进入
ESTABLISHED
状态。
第三次握手是可以携带数据的,前两次握手是不可以携带数据的
为什么是三次握手?
如果是两次握手,每当服务器接收到客户端请求就会建立连接,如果客户端无法使用这个连接(宕机、延迟等)就浪费了资源
TCP 使用三次握手建立连接的最主要原因是防止「历史连接」初始化了连接
四次挥手
- 客户端打算关闭连接,此时会发送一个 TCP 首部
FIN
标志位被置为1
的报文,也即FIN
报文,之后客户端进入FIN_WAIT_1
状态。 - 服务端收到该报文后,就向客户端发送
ACK
应答报文,接着服务端进入CLOSE_WAIT
状态。 - 客户端收到服务端的
ACK
应答报文后,之后进入FIN_WAIT_2
状态。 - 等待服务端处理完数据后,也向客户端发送
FIN
报文,之后服务端进入LAST_ACK
状态。 - 客户端收到服务端的
FIN
报文后,回一个ACK
应答报文,之后进入TIME_WAIT
状态 - 服务端收到了
ACK
应答报文后,就进入了CLOSE
状态,至此服务端已经完成连接的关闭。 - 客户端在经过
2MSL
一段时间后,自动进入CLOSE
状态,至此客户端也完成连接的关闭。
TCP如何保持可靠性?
序列号和确认应答
- TCP将每个发送的数据包都标记上序列号,接收方在收到数据包后会发送确认应答(ACK)给发送方,指示已成功接收到数据。如果发送方在一定时间内未收到确认应答,它会认为数据包丢失,并重新发送。
重传机制
- 超时重传:设定一个定时器,超时没收到
ACK
报文则重发 - 快速重传: 由于拥堵延迟等导致收发报文不同步,则发送方收到三个相同的
ACK
报文后会在定时器超时前重传 - SACK
- D-SACK
滑动窗口
由于TCP一问一答的通信方式效率较低,所以引入了窗口的概念,其值即无需等待确认应答,而可以继续发送数据的最大值,窗口的实现实际上是操作系统开辟的一个缓存空间,发送方主机在等到确认应答返回之前,必须在缓冲区中保留已发送的数据。如果按期收到确认应答,此时数据就可以从缓存区清除。
窗口大小由接收方决定
流量控制
TCP 提供一种机制可以让「发送方」根据「接收方」的实际接收能力控制发送的数据量,这就是所谓的流量控制
使用滑动窗口协议实现
拥塞控制
拥塞窗口 cwnd是发送方维护的一个的状态变量,它会根据网络的拥塞程度动态变化的。
只要「发送方」没有在规定时间内接收到 ACK
应答报文,也就是发生了超时重传,就会认为网络出现了拥塞
发送窗口 swnd
和接收窗口 rwnd
是约等于的关系,那么由于加入了拥塞窗口的概念后,此时发送窗口的值是swnd = min(cwnd, rwnd
),也就是拥塞窗口和接收窗口中的最小值。
拥塞窗口 cwnd
变化的规则:
- 只要网络中没有出现拥塞,
cwnd
就会增大; - 但网络中出现了拥塞,
cwnd
就减少;
使用
- 慢启动 : 刚开始发送数据时,发送方每收到一个
ACK
就给cwnd
+1,之后发包的个数指数增长,直到cwnd
到达门限值ssthresh
- 拥塞避免: 到达门限值后,每收到一个
ACK
,cwnd
增加1/cwnd
,发包速度减慢成线性增长 - 快重传 :即上文的快重传,此时
cwnd = cwnd/2
,ssthresh = cwnd
快恢复 :
cwnd = ssthresh + 3
,- 重传丢失的数据包 ,
- 如果再收到重复的 ACK,那么
cwnd
增加 1 - 如果收到新数据的 ACK 后,
cwnd =ssthresh
回到了拥塞避免状态
有序交付
- TCP保证接收方按序交付数据,即使数据包在传输过程中可能会乱序到达,接收方也会按照序列号重新排序,确保数据按正确的顺序交付给应用层
UDP
UDP是一种无连接的、不可靠的协议。它不提供数据的可靠传输保证,发送的数据包可能会丢失、重复、乱序或损坏,适用于实时性要求较高的应用场景
QUIC
一种基于UDP的传输层协议,尝试基于UDP实现可靠与安全(结合TLS)的连接
应用层重传
- QUIC在协议层面支持应用层的重传机制。当发送方发送数据包时,会在发送方和接收方之间维护一个重传队列。如果接收方未收到某个数据包或者收到了乱序的数据包,它可以通过通知发送方要求重新发送丢失或乱序的数据包。
快速连接迁移
- QUIC支持快速连接迁移机制,即客户端可以无感知地切换网络连接,而不会导致连接中断。例如,当客户端从一个网络切换到另一个网络时,QUIC可以快速地建立新的连接并恢复数据传输,而不需要重新建立TCP连接。
拥塞控制
流量控制
数据包确认
- QUIC使用应用层的确认机制来确保数据包的到达和正确性。接收方会对收到的数据包进行确认,以确保发送方已经成功发送了数据。如果发送方未收到确认应答,它会重新发送数据包,以确保数据的可靠传输。
MySQL
结构
MySQL 的架构共分为两层:Server 层和存储引擎层,
- Server 层负责建立连接、分析和执行 SQL。MySQL 大多数的核心功能模块都在这实现,主要包括连接器,查询缓存、解析器、预处理器、优化器、执行器等。另外,所有的内置函数(如日期、时间、数学和加密函数等)和所有跨存储引擎的功能(如存储过程、触发器、视图等。)都在 Server 层实现。
- 存储引擎层负责数据的存储和提取。支持 InnoDB、MyISAM、Memory 等多个存储引擎,不同的存储引擎共用一个 Server 层。现在最常用的存储引擎是 InnoDB,从 MySQL 5.5 版本开始, InnoDB 成为了 MySQL 的默认存储引擎。我们常说的索引数据结构,就是由存储引擎层实现的,不同的存储引擎支持的索引类型也不相同,比如 InnoDB 支持索引类型是 B+树 ,且是默认使用,也就是说在数据表中创建的主键索引和二级索引默认使用的是 B+ 树索引。
执行一条 SQL 查询语句,期间发生了什么?
- 连接器:建立连接,管理连接、校验用户身份;
- 查询缓存:查询语句如果命中查询缓存则直接返回,否则继续往下执行。MySQL 8.0 已删除该模块;
- 解析 SQL,通过解析器对 SQL 查询语句进行词法分析、语法分析,然后构建语法树,方便后续模块读取表名、字段、语句类型;
执行 SQL:执行 SQL 共有三个阶段:
- 预处理阶段:检查表或字段是否存在;将
select *
中的*
符号扩展为表上的所有列。 - 优化阶段:基于查询成本的考虑, 选择查询成本最小的执行计划;
执行阶段:根据执行计划执行 SQL 查询语句,从存储引擎读取记录,返回给客户端;
- 主键索引查询
- 全表扫描
- 索引下推 : 减少二级索引在查询时的回表操作,提高查询的效率(联合索引遇到><会停止匹配)
- 预处理阶段:检查表或字段是否存在;将
数据库存储在哪
我们每创建一个 database(数据库) 都会在 /var/lib/mysql/
目录里面创建一个以 database 为名的目录,而一张数据库表的数据是保存在「 表名.ibd 」的文件里的,其结构存在「 表名.frm」中
B+树和索引
和B树的区别
- 如果一个结点有k个key,那它有k个子结点
- 子结点中的数据大于等于左边的key小于右边的key
- 叶结点中以链表(有序)的形式存放了所有的数据,也包含中间结点的key(因此,B+树的搜索总是会到达叶结点,中间结点不保存数据,只是用来索引的)
- 叶结点之间是双向链表
优点
B+Tree 相比于 B 树和二叉树来说,最大的优势在于查询效率很高,因为即使在数据量很大的情况,查询一个数据的磁盘 I/O 依然维持在 3-4次
主键索引和二级索引的B+树
主键索引的 B+Tree 的叶子节点存放的是实际数据,所有完整的用户记录都存放在主键索引的 B+Tree 的叶子节点里
二级索引的 B+Tree 的叶子节点存放的是主键值,而不是实际数据
什么时候适用索引?
- 字段有唯一性限制的,比如商品编码;
- 经常用于
WHERE
查询条件的字段,这样能够提高整个表的查询速度,如果查询条件不是一个字段,可以建立联合索引。 - 经常用于
GROUP BY
和ORDER BY
的字段,这样在查询的时候就不需要再去做一次排序了,因为我们都已经知道了建立索引之后在 B+Tree 中的记录都是排序好的。
哪些情况下索引会失效?
- 以“%(表示任意0个或多个字符)”开头的LIKE语句;
- OR语句前后没有同时使用索引;
- 数据类型出现隐式转化(如varchar不加单引号的话可能会自动转换为int型);
- 对于联合索引,必须满足 最左匹配原则 (最左优先,eg:多列索引col1、col2和col3,则 索引生效的情形包括 col1或col1,col2或col1,col2,col3);
- 如果MySQL估计全表扫描比索引快,则不使用索引(比如非常小的表)
Drop/Delete/Truncate的区别?
- Delete用来删除表的全部或者部分数据,执行delete之后,用户需要提交之后才会执行,会触发表上的DELETE触发器(包含一个OLD的虚拟表,可以只读访问被删除的数据),DELETE之后表结构还在,删除很慢,一行一行地删,因为会记录日志,可以利用日志还原数据;
- Truncate删除表中的所有数据,这个操作不能回滚,也不会触发这个表上的触发器。操作比DELETE快很多(直接把表drop掉,再创建一个新表,删除的数据不能找回)。如果表中有自增(AUTO_INCREMENT)列,则重置为1;
- Drop命令从数据库中删除表,所有的数据行,索引和约束都会被删除;不能回滚,不会触发触发器;
MySQL的两种存储引擎 InnoDB 和 MyISAM 的区别?
- InnoDB支持事务,可以进行Commit和Rollback;
- MyISAM 只支持表级锁,而 InnoDB 还支持行级锁,提高了并发操作的性能;
- InnoDB 支持外键;
- MyISAM 崩溃后发生损坏的概率比 InnoDB 高很多,而且恢复的速度也更慢;
- MyISAM 支持压缩表和空间数据索引,InnoDB需要更多的内存和存储;
- InnoDB 支持在线热备份
Buffer Pool
提高查询性能的缓存池
Buffer Pool 是在 MySQL 启动的时候,向操作系统申请的一片连续的内存空间,默认配置下 Buffer Pool 只有 128MB
。
可以通过调整 innodb_buffer_pool_size
参数来设置 Buffer Pool 的大小,一般建议设置成可用物理内存的 60%~80%。
如何提高缓存命中率?
使用改进的LRU算法,维护old 区域 和 young 区域,而不是每次将新访问值加入LRU链表头部时删去尾部
同时当「页被访问」且「 old 区域停留时间超过 innodb_old_blocks_time
阈值(默认为1秒)」时,才会将页插入到 young 区域,否则还是插入到 old 区域,目的是为了解决批量数据访问,大量热数据淘汰的问题。
如何优化数据库
SQL语句优化
- 应尽量避免在 where 子句中使用!=、<、>操作符或对字段进行null值判断,否则将引擎放弃使用索引而进行全表扫描;
- 只返回必要的列:最好不要使用 SELECT * 语句;
- 只返回必要的行:使用 LIMIT 语句来限制返回的数据;
- 将一个大连接查询分解成对每一个表进行一次单表查询,然后在应用程序中进行关联,这样做的好处有:让缓存更高效。对于连接查询,如果其中一个表发生变化,那么整个查询缓存就无法使用。而分解后的多个查询,即使其中一个表发生变化,对其它表的查询缓存依然可以使用;
- 分解成多个单表查询,这些单表查询的缓存结果更可能被其它查询使用到,从而减少冗余的查询;减少锁竞争
索引的优化注意
- 会引起索引失效的情况,以及在适合的地方建立索引
数据库表结构的优化
- 设计表时遵循三大范式;选择合适的数据类型:尽可能不要存储NULL字段;使用简单的数据类型(int, varchar/ text);表的水平切分(Sharding):将同一个表中的记录拆分到多个结构相同的表中(策略:哈希取模;根据ID范围来分)。当一个表的数据不断增多时,Sharding 是必然的选择,它可以将数据分布到集群的不同节点上,从而缓解单个数据库的压力;表的垂直切分:将一张表按列切分成多个表。可以将不常用的字段单独放在同一个表中;把大字段独立放入一个表中;或者把经常使用的字段(关系密切的)放在一张表中。垂直切分之后业务更加清晰,系统之间整合或扩展容易,数据维护简单
系统配置的优化操作系统
- 增加TCP支持的队列数;MySQL配置文件优化:缓存池大小和个数设置
硬件的优化磁盘性能
- 固态硬盘;CPU:多核且高频;内存:增大内存
关系型数据库和非关系型数据库的区别?
- 前者高度组织化结构化数据;后者存储的数据结构不固定更加灵活,可以减少一些空间和时间的开销
- 后者更加容易水平扩展
- 前者支持结构化查询语言,支持复杂的查询功能和表关联。后者只能进行简单的查询
- 前者支持事务,具有ACID特性。后者则是BASE,最终一致性