并发

什么是进程和线程

  • 进程是程序的一次执行过程,是系统运行程序的基本单位,因此进程是动态的。系统运行一个程序即是一个进程从创建,运行到消亡的过程。
  • 线程与进程相似,但线程是一个比进程更小的执行单位。一个进程在其执行的过程中可以产生多个线程。与进程不同的是同类的多个线程共享进程的方法区资源,但每个线程有自己的程序计数器虚拟机栈本地方法栈

Java 程序天生就是多线程程序,我们可以通过 JMX 来看看一个普通的 Java 程序有哪些线程,代码如下。

1
2
3
4
5
6
7
8
9
10
11
12
public class MultiThread {
public static void main(String[] args) {
// 获取 Java 线程管理 MXBean
ThreadMXBean threadMXBean = ManagementFactory.getThreadMXBean();
// 不需要获取同步的 monitor 和 synchronizer 信息,仅获取线程和线程堆栈信息
ThreadInfo[] threadInfos = threadMXBean.dumpAllThreads(false, false);
// 遍历线程信息,仅打印线程 ID 和线程名称信息
for (ThreadInfo threadInfo : threadInfos) {
System.out.println("[" + threadInfo.getThreadId() + "] " + threadInfo.getThreadName());
}
}
}

上述程序输出如下(输出内容可能不同,不用太纠结下面每个线程的作用,只用知道 main 线程执行 main 方法即可):

1
2
3
4
5
[5] Attach Listener //添加事件
[4] Signal Dispatcher // 分发处理给 JVM 信号的线程
[3] Finalizer //调用对象 finalize 方法的线程
[2] Reference Handler //清除 reference 线程
[1] main //main 线程,程序入口

从上面的输出内容可以看出:一个 Java 程序的运行是 main 线程和多个其他线程同时运行

总结:线程是进程划分成的更小的运行单位。线程和进程最大的不同在于基本上各进程是独立的,而各线程则不一定,因为同一进程中的线程极有可能会相互影响。线程执行开销小,但不利于资源的管理和保护;而进程正相反。

并发与并行的区别

  • 并发:两个及两个以上的作业在同一 时间段 内执行。
  • 并行:两个及两个以上的作业在同一 时刻 执行。

最关键的点是:是否是 同时 执行。

同步和异步的区别

  • 同步:发出一个调用之后,在没有得到结果之前, 该调用就不可以返回,一直等待。
  • 异步:调用在发出之后,不用等待返回结果,该调用直接返回。

使用多线程可能带来什么问题?

并发编程的目的就是为了能提高程序的执行效率进而提高程序的运行速度,但是并发编程并不总是能提高程序运行速度的,而且并发编程可能会遇到很多问题,比如:内存泄漏、死锁、线程不安全等等。

如何理解线程安全和不安全?

线程安全和不安全是在多线程环境下对于同一份数据的访问是否能够保证其正确性和一致性的描述。

  • 线程安全指的是在多线程环境下,对于同一份数据,不管有多少个线程同时访问,都能保证这份数据的正确性和一致性。
  • 线程不安全则表示在多线程环境下,对于同一份数据,多个线程同时访问时可能会导致数据混乱、错误或者丢失。

如何创建线程?

一般来说,创建线程有很多种方式,例如继承Thread类、实现Runnable接口、实现Callable接口、使用线程池、使用CompletableFuture类等等。

不过,这些方式其实并没有真正创建出线程。准确点来说,这些都属于是在 Java 代码中使用多线程的方法。

严格来说,Java 就只有一种方式可以创建线程,那就是通过new Thread().start()创建。不管是哪种方式,最终还是依赖于new Thread().start()

说说线程的生命周期和状态?

Java 线程在运行的生命周期中的指定时刻只可能处于下面 6 种不同状态的其中一个状态:

  • NEW: 初始状态,线程被创建出来但没有被调用 start()
  • RUNNABLE: 运行状态,线程被调用了 start()等待运行的状态。
  • BLOCKED:阻塞状态,需要等待锁释放。
  • WAITING:等待状态,表示该线程需要等待其他线程做出一些特定动作(通知或中断)。
  • TIME_WAITING:超时等待状态,可以在指定的时间后自行返回而不是像 WAITING 那样一直等待。
  • TERMINATED:终止状态,表示该线程已经运行完毕。

线程在生命周期中并不是固定处于某一个状态而是随着代码的执行在不同状态之间切换。

Java 线程状态变迁图

什么是乐观锁和悲观锁?

  • 悲观锁总是假设最坏的情况,认为共享资源每次被访问的时候就会出现问题(比如共享数据被修改),所以每次在获取资源操作的时候都会上锁,这样其他线程想拿到这个资源就会阻塞直到锁被上一个持有者释放。也就是说,共享资源每次只给一个线程使用,其它线程阻塞,用完后再把资源转让给其它线程

像 Java 中synchronizedReentrantLock等独占锁就是悲观锁思想的实现。

  • 乐观锁总是假设最好的情况,认为共享资源每次被访问的时候不会出现问题,线程可以不停地执行,无需加锁也无需等待,只是在提交修改的时候去验证对应的资源(也就是数据)是否被其它线程修改了(具体方法可以使用版本号机制或 CAS 算法)。

如何实现乐观锁?

  • 版本号机制

一般是在数据表中加上一个数据版本号 version 字段,表示数据被修改的次数。当数据被修改时,version 值会加一。当线程 A 要更新数据值时,在读取数据的同时也会读取 version 值,在提交更新时,若刚才读取到的 version 值为当前数据库中的 version 值相等时才更新,否则重试更新操作,直到更新成功。

  • CAS 算法

CAS 的全称是 Compare And Swap(比较与交换) ,用于实现乐观锁,被广泛应用于各大框架中。CAS 的思想很简单,就是用一个预期值和要更新的变量值进行比较,两值相等才会进行更新。

CAS 是一个原子操作,底层依赖于一条 CPU 的原子指令。

CAS 涉及到三个操作数:

  • V:要更新的变量值(Var)
  • E:预期值(Expected)
  • N:拟写入的新值(New)

当且仅当 V 的值等于 E 时,CAS 通过原子方式用新值 N 来更新 V 的值。如果不等,说明已经有其它线程更新了 V,则当前线程放弃更新。

CAS 算法存在哪些问题:ABA 问题、循环时间长开销大

公平锁和非公平锁有什么区别?

  • 公平锁 : 锁被释放之后,先申请的线程先得到锁。性能较差一些,因为公平锁为了保证时间上的绝对顺序,上下文切换更频繁。
  • 非公平锁:锁被释放之后,后申请的线程可能会先获取到锁,是随机或者按照其他优先级排序的。性能更好,但可能会导致某些线程永远无法获取到锁。

共享锁和独占锁有什么区别?

  • 共享锁:一把锁可以被多个线程同时获得。
  • 独占锁:一把锁只能被一个线程获得。

JVM

JVM运行时数据区域

JDK1.8

Java 运行时数据区域(JDK1.8 )

线程私有的:

  • 程序计数器
  • 虚拟机栈
  • 本地方法栈

线程共享的:

  • 方法区
  • 直接内存 (非运行时数据区的一部分)

计算机基础

OSI 七层模型是什么?每一层的作用是什么?

OSI 七层模型 是国际标准化组织提出的一个网络分层模型,其大体结构以及每一层提供的功能如下图所示:

OSI 七层模型

osi七层模型2

TCP/IP 四层模型是什么?每一层的作用是什么?

TCP/IP 四层模型 是目前被广泛采用的一种模型,我们可以将 TCP / IP 模型看作是 OSI 七层模型的精简版本,由以下 4 层组成:

  1. 应用层
  2. 传输层
  3. 网络层
  4. 网络接口层

需要注意的是,我们并不能将 TCP/IP 四层模型 和 OSI 七层模型完全精确地匹配起来,不过可以简单将两者对应起来,如下图所示:

TCP/IP 四层模型

常见网络协议

1. 应用层有哪些常见的协议?

应用层常见协议

  • HTTP(Hypertext Transfer Protocol,超文本传输协议):基于 TCP 协议,是一种用于传输超文本和多媒体内容的协议,主要是为 Web 浏览器与 Web 服务器之间的通信而设计的。当我们使用浏览器浏览网页的时候,我们网页就是通过 HTTP 请求进行加载的。
  • SMTP(Simple Mail Transfer Protocol,简单邮件发送协议):基于 TCP 协议,是一种用于发送电子邮件的协议。注意 ⚠️:SMTP 协议只负责邮件的发送,而不是接收。要从邮件服务器接收邮件,需要使用 POP3 或 IMAP 协议。
  • POP3/IMAP(邮件接收协议):基于 TCP 协议,两者都是负责邮件接收的协议。IMAP 协议是比 POP3 更新的协议,它在功能和性能上都更加强大。IMAP 支持邮件搜索、标记、分类、归档等高级功能,而且可以在多个设备之间同步邮件状态。几乎所有现代电子邮件客户端和服务器都支持 IMAP。
  • FTP(File Transfer Protocol,文件传输协议) : 基于 TCP 协议,是一种用于在计算机之间传输文件的协议,可以屏蔽操作系统和文件存储方式。注意 ⚠️:FTP 是一种不安全的协议,因为它在传输过程中不会对数据进行加密。建议在传输敏感数据时使用更安全的协议,如 SFTP。
  • Telnet(远程登陆协议):基于 TCP 协议,用于通过一个终端登陆到其他服务器。Telnet 协议的最大缺点之一是所有数据(包括用户名和密码)均以明文形式发送,这有潜在的安全风险。这就是为什么如今很少使用 Telnet,而是使用一种称为 SSH 的非常安全的网络传输协议的主要原因。
  • SSH(Secure Shell Protocol,安全的网络传输协议):基于 TCP 协议,通过加密和认证机制实现安全的访问和文件传输等业务
  • RTP(Real-time Transport Protocol,实时传输协议):通常基于 UDP 协议,但也支持 TCP 协议。它提供了端到端的实时传输数据的功能,但不包含资源预留存、不保证实时传输质量,这些功能由 WebRTC 实现。
  • DNS(Domain Name System,域名管理系统): 基于 UDP 协议,用于解决域名和 IP 地址的映射问题。

2. 传输层有哪些常见的协议?

传输层常见协议

  • TCP(Transmission Control Protocol,传输控制协议 ):提供 面向连接 的,可靠 的数据传输服务。
  • UDP(User Datagram Protocol,用户数据协议):提供 无连接 的,尽最大努力 的数据传输服务(不保证数据传输的可靠性),简单高效。

3. 网络层有哪些常见的协议?

网络层常见协议

  • IP(Internet Protocol,网际协议):TCP/IP 协议中最重要的协议之一,属于网络层的协议,主要作用是定义数据包的格式、对数据包进行路由和寻址,以便它们可以跨网络传播并到达正确的目的地。目前 IP 协议主要分为两种,一种是过去的 IPv4,另一种是较新的 IPv6,目前这两种协议都在使用,但后者已经被提议来取代前者。
  • ARP(Address Resolution Protocol,地址解析协议):ARP 协议解决的是网络层地址和链路层地址之间的转换问题。因为一个 IP 数据报在物理上传输的过程中,总是需要知道下一跳(物理上的下一个目的地)该去往何处,但 IP 地址属于逻辑地址,而 MAC 地址才是物理地址,ARP 协议解决了 IP 地址转 MAC 地址的一些问题。
  • ICMP(Internet Control Message Protocol,互联网控制报文协议):一种用于传输网络状态和错误消息的协议,常用于网络诊断和故障排除。例如,Ping 工具就使用了 ICMP 协议来测试网络连通性。
  • NAT(Network Address Translation,网络地址转换协议):NAT 协议的应用场景如同它的名称——网络地址转换,应用于内部网到外部网的地址转换过程中。具体地说,在一个小的子网(局域网,LAN)内,各主机使用的是同一个 LAN 下的 IP 地址,但在该 LAN 以外,在广域网(WAN)中,需要一个统一的 IP 地址来标识该 LAN 在整个 Internet 上的位置。
  • OSPF(Open Shortest Path First,开放式最短路径优先) ):一种内部网关协议(Interior Gateway Protocol,IGP),也是广泛使用的一种动态路由协议,基于链路状态算法,考虑了链路的带宽、延迟等因素来选择最佳路径。
  • RIP(Routing Information Protocol,路由信息协议):一种内部网关协议(Interior Gateway Protocol,IGP),也是一种动态路由协议,基于距离向量算法,使用固定的跳数作为度量标准,选择跳数最少的路径作为最佳路径。
  • BGP(Border Gateway Protocol,边界网关协议):一种用来在路由选择域之间交换网络层可达性信息(Network Layer Reachability Information,NLRI)的路由选择协议,具有高度的灵活性和可扩展性。

从输入 URL 到页面展示到底发生了什么?(非常重要)

总体来说分为以下几个步骤:

  1. 在浏览器中输入指定网页的 URL。
  2. 浏览器通过 DNS 协议,获取域名对应的 IP 地址。
  3. 浏览器根据 IP 地址和端口号,向目标服务器发起一个 TCP 连接请求。
  4. 浏览器在 TCP 连接上,向服务器发送一个 HTTP 请求报文,请求获取网页的内容。
  5. 服务器收到 HTTP 请求报文后,处理请求,并返回 HTTP 响应报文给浏览器。
  6. 浏览器收到 HTTP 响应报文后,解析响应体中的 HTML 代码,渲染网页的结构和样式,同时根据 HTML 中的其他资源的 URL(如图片、CSS、JS 等),再次发起 HTTP 请求,获取这些资源的内容,直到网页完全加载显示。
  7. 浏览器在不需要和服务器通信时,可以主动关闭 TCP 连接,或者等待服务器的关闭请求。
img

HTTP 和 HTTPS 有什么区别?(重要)

HTTP 和 HTTPS 对比

  • 端口号:HTTP 默认是 80,HTTPS 默认是 443。
  • URL 前缀:HTTP 的 URL 前缀是 http://,HTTPS 的 URL 前缀是 https://
  • 安全性和资源消耗:HTTP 协议运行在 TCP 之上,所有传输的内容都是明文,客户端和服务器端都无法验证对方的身份。HTTPS 是运行在 SSL/TLS 之上的 HTTP 协议,SSL/TLS 运行在 TCP 之上。所有传输的内容都经过加密,加密采用对称加密,但对称加密的密钥用服务器方的证书进行了非对称加密。所以说,HTTP 安全性没有 HTTPS 高,但是 HTTPS 比 HTTP 耗费更多服务器资源。
  • SEO(搜索引擎优化):搜索引擎通常会更青睐使用 HTTPS 协议的网站,因为 HTTPS 能够提供更高的安全性和用户隐私保护。使用 HTTPS 协议的网站在搜索结果中可能会被优先显示,从而对 SEO 产生影响。

URI 和 URL 的区别是什么?

  • URI(Uniform Resource Identifier) 是统一资源标志符,可以唯一标识一个资源。
  • URL(Uniform Resource Locator) 是统一资源定位符,可以提供该资源的路径。它是一种具体的 URI,即 URL 可以用来标识一个资源,而且还指明了如何 locate 这个资源。

URI 的作用像身份证号一样,URL 的作用更像家庭住址一样。URL 是一种具体的 URI,它不仅唯一标识资源,而且还提供了定位该资源的信息。

什么是 WebSocket?

WebSocket 是一种基于 TCP 连接的全双工通信协议,即客户端和服务器可以同时发送和接收数据。

WebSocket 协议在 2008 年诞生,2011 年成为国际标准,几乎所有主流较新版本的浏览器都支持该协议。不过,WebSocket 不只能在基于浏览器的应用程序中使用,很多编程语言、框架和服务器都提供了 WebSocket 支持。

WebSocket 协议本质上是应用层的协议,用于弥补 HTTP 协议在持久通信能力上的不足。客户端和服务器仅需一次握手,两者之间就直接可以创建持久性的连接,并进行双向数据传输。

WebSocket 和 HTTP 有什么区别?

WebSocket 和 HTTP 两者都是基于 TCP 的应用层协议,都可以在网络中传输数据。

下面是二者的主要区别:

  • WebSocket 是一种双向实时通信协议,而 HTTP 是一种单向通信协议。并且,HTTP 协议下的通信只能由客户端发起,服务器无法主动通知客户端。
  • WebSocket 使用 ws:// 或 wss://(使用 SSL/TLS 加密后的协议,类似于 HTTP 和 HTTPS 的关系) 作为协议前缀,HTTP 使用 http:// 或 https:// 作为协议前缀。
  • WebSocket 可以支持扩展,用户可以扩展协议,实现部分自定义的子协议,如支持压缩、加密等。
  • WebSocket 通信数据格式比较轻量,用于协议控制的数据包头部相对较小,网络开销小,而 HTTP 通信每次都要携带完整的头部,网络开销较大(HTTP/2.0 使用二进制帧进行数据传输,还支持头部压缩,减少了网络开销)。

TCP 与 UDP 的区别(重要)

  1. 是否面向连接:UDP 在传送数据之前不需要先建立连接。而 TCP 提供面向连接的服务,在传送数据之前必须先建立连接,数据传送结束后要释放连接。
  2. 是否是可靠传输:远地主机在收到 UDP 报文后,不需要给出任何确认,并且不保证数据不丢失,不保证是否顺序到达。TCP 提供可靠的传输服务,TCP 在传递数据之前,会有三次握手来建立连接,而且在数据传递时,有确认、窗口、重传、拥塞控制机制。通过 TCP 连接传输的数据,无差错、不丢失、不重复、并且按序到达。
  3. 是否有状态:这个和上面的“是否可靠传输”相对应。TCP 传输是有状态的,这个有状态说的是 TCP 会去记录自己发送消息的状态比如消息是否发送了、是否被接收了等等。为此 ,TCP 需要维持复杂的连接状态表。而 UDP 是无状态服务,简单来说就是不管发出去之后的事情了(这很渣男!)。
  4. 传输效率:由于使用 TCP 进行传输的时候多了连接、确认、重传等机制,所以 TCP 的传输效率要比 UDP 低很多。
  5. 传输形式:TCP 是面向字节流的,UDP 是面向报文的。
  6. 首部开销:TCP 首部开销(20 ~ 60 字节)比 UDP 首部开销(8 字节)要大。
  7. 是否提供广播或多播服务:TCP 只支持点对点通信,UDP 支持一对一、一对多、多对一、多对多;
  8. ……

我把上面总结的内容通过表格形式展示出来了!确定不点个赞嘛?

TCP UDP
是否面向连接
是否可靠
是否有状态
传输效率 较慢 较快
传输形式 字节流 数据报文段
首部开销 20 ~ 60 bytes 8 bytes
是否提供广播或多播服务

什么时候选择 TCP,什么时候选 UDP?

  • UDP 一般用于即时通信,比如:语音、 视频、直播等等。这些场景对传输数据的准确性要求不是特别高,比如你看视频即使少个一两帧,实际给人的感觉区别也不大。
  • TCP 用于对传输准确性要求特别高的场景,比如文件传输、发送和接收邮件、远程登录等等。

MySQL

如何定位慢查询 ?

需要在MySQL的配置文件(/etc/my.cnf)中配置如下信息:

1
2
3
4
# 开启MySQL慢日志查询开关
slow_query_log=1
# 设置慢日志的时间为2秒,SQL语句执行时间超过2秒,就会视为慢查询,记录慢查询日志
long_query_time=2

配置完毕之后,通过以下指令重新启动MySQL服务器进行测试,查看慢日志文件中记录的信息 /var/lib/mysql/localhost-slow.log

image-20240407153408047

当然,也有相关的工具:

调试工具:Arthas
运维工具:Prometheus 、Skywalking

image-20240407153456512

如何分析SQL语句?

SQL执行很慢,可能有一下原因:

  • 聚合查询
  • 多表查询
  • 表数据量过大查询
  • 深度分页查询
1
2
- 直接在select语句之前加上关键字 explain / desc
EXPLAIN SELECT 字段列表 FROM 表名 WHERE 条件 ;

image-20240407153708794

然后需要关注以下字段:

  • possible_key:当前sql可能会使用到的索引

  • key:当前sql实际命中的索引

  • key_len :索引占用的大小

    通过它们两个查看是否可能会命中索引

  • Extra:额外的优化建议

    Using where; Using Index:查找使用了索引,需要的数据都在索引列中能找到,不需要回表查询数据
    Using index condition:查找使用了索引,但是需要回表查询数据

  • type 这条sql的连接的类型,性能由好到差为NULL、system、const、eq_ref、ref、range、 index、all

    system:查询系统中的表
    const:根据主键查询
    eq_ref:主键索引查询或唯一索引查询
    ref:索引查询
    range:范围查询
    index:索引树扫描
    all:全盘扫描

MySQL支持的存储引擎有哪些, 有什么区别?

存储引擎就是存储数据、建立索引、更新/查询数据等技术的实现方式 。存储引擎是基于表的,而不是基于库的,所以存储引擎也可被称为表类型。

在MySQL中提供了很多的存储引擎,比较常见有InnoDBMyISAMMemory

  • InnoDB:存储引擎是mysql5.5之后是默认的引擎,它支持事务、外键、表级锁和行级锁。DML操作遵循ACID模型,支持事务。有行级锁,提高并发访问性能。支持外键,保证数据的完整性和正确性。
  • MyISAM:是早期的引擎,它不支持事务、只有表级锁、也没有外键,用的不多
  • Memory:主要把数据存储在内存,支持表级锁,没有外键和事务,用的也不多
特性 MyISAM InnoDB MEMORY
事务安全 不支持 支持 不支持
锁机制 表锁 表锁/行锁 表锁
外键 不支持 支持 不支持

什么是索引?索引的底层数据结构是什么?

索引(index)是帮助MySQL高效获取数据的数据结构。在数据之外,数据库系统还维护着满足特定查找算法的数据结构(B+树),这些数据结构以某种方式引用(指向)数据, 这样就可以在这些数据结构上实现高级查找算法,这种数据结构就是索引。

  • 索引(index)是帮助MySQL高效获取数据的数据结构(有序)
  • 提高数据检索的效率,降低数据库的IO成本(不需要全表扫描)
  • 通过索引列对数据进行排序,降低数据排序的成本,降低了CPU的消耗

MySQL默认使用的索引底层数据结构是B+树。

  • 阶数更多,路径更短
  • 磁盘读写代价B+树更低,非叶子节点只存储指针,叶子阶段存储数据
  • B+树便于扫库和区间查询,叶子节点是一个双向链表

B树与B+树的区别是什么?

  • B树

image-20240407154800153

  • B+树

image-20240407154859696

B树与B+树的区别:

  1. 在B树中,非叶子节点和叶子节点都会存放数据,而B+树的所有的数据都会出现在叶子节点,在查询的时候,B+树查找效率更加稳定
  2. 在进行范围查询的时候,B+树效率更高,因为B+树都在叶子节点存储,并且叶子节点是一个双向链表

B树与B+树对比:

  1. 磁盘读写代价B+树更低;
  2. 查询效率B+树更加稳定;
  3. B+树便于扫库和区间查询

什么是聚簇索引,非聚簇索引?

聚集索引:将数据存储与索引放到了一块,索引结构B+树的叶子节点保存了行数据。有且只有一个。

非聚集索引:将数据与索引分开存储,索引结构B+树的叶子节点关联的是对应的主键。可以有多个。

聚集索引选取规则:

  • 如果存在主键,主键索引就是聚集索引。
  • 如果不存在主键,将使用第一个唯一(UNIQUE)索引作为聚集索引。
  • 如果表没有主键,或没有合适的唯一索引,则InnoDB会自动生成一个rowid作为隐藏的聚集索引。

image-20240407155510989

什么是回表查询?

回表查询:通过二级索引找到对应的主键值,到聚集索引中查找整行数据,这个过程就是回表

image-20240407155539271

什么是覆盖索引?怎么处理超大分页?

覆盖索引:是指查询使用了索引,返回的列,必须在索引中全部能够找到。

  • 使用id查询,直接走聚集索引查询,一次索引扫描,直接返回数据,性能高。
  • 如果返回的列中没有创建索引,有可能会触发回表查询,尽量避免使用select *

在数据量比较大时,limit分页查询,需要对数据进行排序,效率低,通过创建覆盖索引能够比较好地提高性能,可以通过覆盖索引+子查询形式进行优化。

例如,该查询语句可以这样优化:

1
select * from user limit 9000000,10;

采用子查询先对id进行覆盖索引排序,然后执行sql语句

1
2
3
select * from user u,
(select id from user order by id limit 9000000,10) a
where u.id = a.id;

索引的创建原则?什么情况下索引会失效?

  • 索引创建原则

    1. 数据量较大,且查询比较频繁的表
    2. 常作为查询条件、排序、分组的字段
    3. 字段内容区分度高
    4. 内容较长,使用前缀索引
    5. 尽量创建联合索引
    6. 控制索引的数量
    7. 如果索引列不能存储NULL值,请在创建表时使用NOT NULL约束它
  • 索引失效情况

    1. 违反最左前缀法则
    2. 范围查询右边的列
    3. 在索引列上进行运算操作
    4. 字符串不加单引号
    5. 以%开头的Like模糊查询

有哪些优化SQL的方法?

  • 表的设计优化

    根据实际情况选择合适的数值类型(tinyint、int、bigint)

    根据实际情况选择合适的字符串类型(char、varchar)

  • 索引优化

    对数据量打的表创建索引

    对常作为查询条件、排序、分组的字段创建索引

    尽量创建联合索引

    控制索引的数量

    ……

  • SQL语句优化

    合理编写SQL语句(避免直接使用select *、用union all代替union、能用inner join 就不用left join、right join、避免在where子句中对字段进行表达式操作)

    避免SQL语句造成索引失效的写法(使用函数或表达式处理索引列、隐式类型转换、使用不等于(<> 或 !=)操作……)

  • 主从复制、读写分离

  • 分库分表

什么是redo log,undo log?

  • redo log: 记录的是数据页的物理变化,服务宕机可用来同步数据
  • undo log :记录的是逻辑日志,当事务回滚时,通过逆操作恢复原来的数据
  • redo log保证了事务的持久性,undo log保证了事务的原子性和一致性

redo log:重做日志,记录的是事务提交时数据页的物理修改,是用来实现事务的持久性。redo log是物理日志。

image-20240407162732189

undo log:回滚日志,用于记录数据被修改前的信息 , 作用包含两个 : 提供回滚MVCC(多版本并发控制) ,是用来实现事务的一致性原子性。undo log是逻辑日志。

事务的特性?并发事务会带来哪些问题?怎么解决这些问题呢?

  • 事务的特性:

    • 原子性(Atomicity):事务是不可分割的最小操作单元,要么全部成功,要么全部失败。
    • 一致性(Consistency):事务完成时,必须使所有的数据都保持一致状态。
    • 隔离性(Isolation):数据库系统提供的隔离机制,保证事务在不受外部并发操作影响的独立环境下运行。
    • 持久性(Durability):事务一旦提交或回滚,它对数据库中的数据的改变就是永久的。
  • 并发事务问题:

    • 并发事务问题:脏读、不可重复读、幻读

    • 隔离级别:读未提交、读已提交、可重复读、串行化

问题 描述
脏读 一个事务读到另外一个事务还没有提交的数据。
不可重复读 一个事务先后读取同一条记录,但两次读取的数据不同,称之为不可重复读。
幻读 一个事务按照条件查询数据时,没有对应的数据行,但是在插入数据时,又发现这行数据已经存在,好像出现了”幻影”。
  • 解决方案:对事务进行隔离
隔离级别 脏读 不可重复读 幻读
Read Uncommitted 未提交读 × × ×
Read Committed 读已提交 × ×
Repeatable Read(默认) 可重复读 ×
Serializable 串行化
  • 如何保证隔离性:
    • 锁:排他锁(如一个事务获取了一个数据行的排他锁,其他事务就不能再获取该行的其他锁)
    • MVCC: 多版本并发控制

事务中的隔离性是如何保证?请解释一下MVCC

MVCC:Multi-Version Concurrency Control,多版本并发控制。指维护一个数据的多个版本,使得读写操作没有冲突MVCC的具体实现,主要依赖于数据库记录中的隐式字段undo log日志ReadView

  • 隐式字段
隐藏字段 含义
DB_TRX_ID 最近修改事务ID,记录插入这条记录或最后一次修改该记录的事务ID。
DB_ROLL_PTR 回滚指针,指向这条记录的上一个版本,用于配合undo log,指向上一个版本。
DB_ROW_ID 隐藏主键,如果表结构没有指定主键,将会生成该隐藏字段。
  • undo log

    • 回滚日志,在insert、update、delete的时候产生的便于数据回滚的日志。

    • undo log版本链:不同事务或相同事务对同一条记录进行修改,会导致该记录的undo log生成一条记录版本链表,链表的头部是最新的旧记录,链表尾部是最早的旧记录

      image-20240407163613418

  • ReadView(解决一个事务查询选择版本的问题)

    • 根据ReadView的匹配规则和当前的一些事务id判断该访问那个版本的数据。
    • 不同的隔离级别快照读是不一样的,最终的访问的结果不一样
      • RC :每一次执行快照读时生成ReadView
      • RR:仅在事务中第一次执行快照读时生成ReadView,后续复用
    • 含义:快照读SQL执行时MVCC提取数据的依据,记录并维护系统当前活跃的事务(未提交的)id。

    • 当前读:读取的是记录的最新版本,读取时还要保证其他并发事务不能修改当前记录,会对读取的记录进行加锁。对于我们日常的操作,如:select … lock in share mode(共享锁),select … for update、update、insert、delete(排他锁)都是一种当前读。

    • 快照读:简单的select(不加锁)就是快照读,快照读,读取的是记录数据的可见版本,有可能是历史数据,不加锁,是非阻塞读。

      • Read Committed:每次执行select,都生成一个快照读。
      • Repeatable Read(默认):仅在事务中第一次执行select时生成ReadView,后续复用。
    • ReadView中包含了四个核心字段:

      字段 含义
      m_ids 当前活跃的事务ID集合
      min_trx_id 最小活跃事务ID
      max_trx_id 预分配事务ID,当前最大事务ID+1(因为事务ID是自增的)
      creator_trx_id ReadView创建者的事务ID

主从同步原理是什么?

主从复制的核心就是二进制日志

主从复制步骤:

  1. Master 主库在事务提交时,会把数据变更记录在二进制日志文件Binlog中。
  2. 从库读取主库的二进制日志文件Binlog,写入到从库的中继日志Relay Log
  3. Slave重做中继日志中的事件,将改变反映它自己的数据。

image-20240407164836130

分库分表时机、分表分库策略

分库分表的时机:

  1. 前提,项目业务数据逐渐增多,或业务发展比较迅速
  2. 优化已解决不了性能问题(主从读写分离、查询索引…)
  3. IO瓶颈(磁盘IO、网络IO)、CPU瓶颈(聚合查询、连接数太多)

分表分库策略:

  1. 垂直分库,根据业务进行拆分,高并发下提高磁盘IO和网络连接数
  2. 垂直分表,冷热数据分离,多表互不影响
  3. 水平分库,将一个库的数据拆分到多个库中,解决海量数据存储和高并发的问题
  4. 水平分表,解决单表存储和性能的问题

Redis

缓存穿透

缓存穿透:查询一个不存在的数据,mysql查询不到数据也不会直接写入缓存,就会导致每次请求都查数据库

解决方案:

  • 缓存空数据,查询返回的数据为空,仍把这个空结果进行缓存(简单,但是消耗内存,且可能会发生不一致的问题)
  • 布隆过滤器(内存占用较少,没有多余key,但是实现复杂,存在误判)

布隆过滤器原理:布隆过滤器是一个以(bit)位为单位的很长的数组,数组中每个单元只能存储二进制数0或1。当一个key来了之后经过3次hash计算,模于数组长度找到数据的下标然后把数组中原来的0改为1,这样一来,三个数组的位置就能标明一个key的存在。

缓存击穿

缓存击穿:key过期的时候,恰好这时间点对这个key有大量的并发请求过来,这些并发的请求可能会瞬间把DB压垮

解决方案:

  • 互斥锁(强一致性,但是性能差)
  • 逻辑过期(高可用性、性能优,但是有一致性问题)

image-20240407212348197

缓存雪崩

缓存雪崩:是指在同一时段大量的缓存key同时失效或者Redis服务宕机,导致大量请求到达数据库,带来巨大压力。

解决方案:

  • 给不同的Key的TTL添加随机值
  • 利用Redis集群提高服务的可用性(哨兵模式、集群模式)
  • 给缓存业务添加降级限流策略(ngxin或spring cloud gateway)
  • 给业务添加多级缓存(Guava或Caffeine)

先删除缓存,还是先修改数据库?

  • 先修改数据库,再删除缓存

    1. 原子性:数据库操作通常是原子的,这意味着它可以作为一个单一的工作单元执行,要么完全成功,要么完全失败。因此,先修改数据库可以确保数据的一致性。
    2. 降低脏读的风险:如果在修改数据库之前删除了缓存,那么在缓存被重新填充之前,其他请求可能会读取到旧的(或脏)数据。
    3. 简化逻辑:通常,在修改数据库后,删除缓存是一个简单的操作,因为缓存中的条目可以通过其键来直接定位。
  • 先删除缓存,再修改数据库,再删除一遍缓存

    1. 降低延迟:在某些场景中,先删除缓存可以减少缓存与数据库之间的数据不一致时间,因为一旦缓存被删除,后续请求将直接从数据库读取数据。
    2. 避免并发问题:在某些高并发的场景下,如果先修改数据库再删除缓存,可能会出现一个请求A修改数据库但还未删除缓存,此时另一个请求B读取到旧的缓存数据并基于旧数据进行了某些操作,然后请求A删除了缓存,此时如果请求B的数据操作依赖于最新的数据库数据,就可能出现问题。

哪些场景需要使用Redis?

缓存:穿透、击穿、雪崩、双写一致、持久化、数据过期策略,数据淘汰策略
分布式锁:setnx、redisson
消息队列、延迟队列

如何确保MySQL与Redis的双写一致性?

  • 使用Canal组件实现数据同步:不更改业务代码,部署一个Canal服务。Canal服务把自己伪装成MySQL的一个从节点,当MySQL数据更新以后,Canal会读取bin log数据,然后在通过Canal的客户端获取到数据,更新缓存即可。
  • 采用Redisson实现读写锁,在读的时候添加共享锁,可以保证共享读操作,互斥读写操作。当更新数据的时候,添加排他锁,互斥读写和读操作,确保在写数据的避免读脏数据。

Redis的数据持久化是怎么做的?

  • RDB(Redis Database Backup file:Redis数据备份文件)

    RDB:是一个二进制的快照文件,它是把Redis内存存储的数据写到磁盘上,当Redis实例宕机恢复数据的时候,方便从RDB的快照文件中恢复数据

    开启RDB:在redis.conf文件中找到,格式如下:

    1
    2
    3
    save 900 1     # 900秒内,如果至少有1个key被修改,则执行bgsave 
    save 300 10 # 原理同上
    save 60 10000 # 原理同上

    RDB执行原理:bgsave开始时会fork主进程得到子进程,子进程共享主进程的内存数据。完成fork后读取内存数据并写入 RDB 文件。

image-20240407214533706

  • AOF(Append Only File:追加文件)

    AOF:Redis处理的每一个写命令都会记录在AOF文件,可以看做是命令日志文件

    开启AOF:

    1
    2
    3
    4
    # 是否开启AOF功能,默认是no
    appendonly yes
    # AOF文件的名称
    appendfilename "appendonly.aof"

    修改AOF的记录频率:

    1
    2
    3
    4
    5
    6
    # 表示每执行一次写命令,立即记录到AOF文件
    appendfsync always
    # 写命令执行完先放入AOF缓冲区,然后表示每隔1秒将缓冲区数据写到AOF文件,是默认方案
    appendfsync everysec
    # 写命令执行完先放入AOF缓冲区,由操作系统决定何时将缓冲区内容写回磁盘
    appendfsync no
    配置项 刷盘时机 优点 缺点
    always 同步刷盘 可靠性高,几乎不丢数据 性能影响大
    everysec 每秒刷盘 性能适中 最多丢失1秒数据
    no 操作系统控制 性能最好 可靠性较差,可能丢失大量数据

    修改AOF的自动去重写阈值:

    1
    2
    3
    4
    5
    6
    # 表示每执行一次写命令,立即记录到AOF文件
    appendfsync always
    # 写命令执行完先放入AOF缓冲区,然后表示每隔1秒将缓冲区数据写到AOF文件,是默认方案
    appendfsync everysec
    # 写命令执行完先放入AOF缓冲区,由操作系统决定何时将缓冲区内容写回磁盘
    appendfsync no
  • RDB与AOF对比:

    ** ** RDB AOF
    持久化方式 定时对整个内存做快照 记录每一次执行的命令
    数据完整性 不完整,两次备份之间会丢失 相对完整,取决于刷盘策略
    文件大小 会有压缩,文件体积小 记录命令,文件体积很大
    宕机恢复速度 很快
    数据恢复优先级 低,因为数据完整性不如AOF 高,因为数据完整性更高
    系统资源占用 高,大量CPU和内存消耗 低,主要是磁盘IO资源但AOF重写时会占用大量CPU和内存资源
    使用场景 可以容忍数分钟的数据丢失,追求更快的启动速度 对数据安全性要求较高常见

Redis数据删除策略

  • 惰性删除,在设置该key过期时间后,我们不去管它,当需要该key时,我们在检查其是否过期,如果过期,我们就删掉它,反之返回该key。

  • 定期删除,就是说每隔一段时间,我们就对一些key进行检查,删除里面过期的key

    定期清理的两种模式:

    • SLOW模式是定时任务,执行频率默认为10hz,每次不超过25ms,以通过修改配置文件redis.conf 的 hz 选项来调整这个次数
    • FAST模式执行频率不固定,每次事件循环会尝试执行,但两次间隔不低于2ms,每次耗时不超过1ms

Redis中的数据淘汰策略

  • noeviction: 不淘汰任何key,但是内存满时不允许写入新数据,默认就是这种策略。
  • volatile-ttl: 对设置了TTL的key,比较key的剩余TTL值,TTL越小越先被淘汰
  • allkeys-random:对全体key ,随机进行淘汰。
  • volatile-random:对设置了TTL的key ,随机进行淘汰。
  • allkeys-lru: 对全体key,基于LRU算法进行淘汰
  • volatile-lru: 对设置了TTL的key,基于LRU算法进行淘汰
  • allkeys-lfu: 对全体key,基于LFU算法进行淘汰
  • volatile-lfu: 对设置了TTL的key,基于LFU算法进行淘汰

LRU(Least Recently Used):最少最近使用,用当前时间减去最后一次访问时间,这个值越大则淘汰优先级越高。

LFU(Least Frequently Used):最少频率使用。会统计每个key的访问频率,值越小淘汰优先级越高

数据淘汰策略-使用建议:

  1. 优先使用 allkeys-lru 策略。充分利用 LRU 算法的优势,把最近最常访问的数据留在缓存中。如果业务有明显的冷热数据区分,建议使用。
  2. 如果业务中数据访问频率差别不大,没有明显冷热数据区分,建议使用 allkeys-random ,随机选择淘汰。
  3. 如果业务中有置顶的需求,可以使用 volatile-lru 策略,同时置顶数据不设置过期时间,这些数据就一直不被删除,会淘汰其他设置过期时间的数据。
  4. 如果业务中有短时高频访问的数据,可以使用 allkeys-lfuvolatile-lfu 策略。

如何保证Redis中的数据都是热点数据?

可以使用 allkeys-lru (挑选最近最少使用的数据淘汰)淘汰策略,那留下来的都是经常访问的热点数据

如何用Redis实现分布式锁?

在redisson中需要手动加锁,并且可以控制锁的失效时间和等待时间,当锁住的一个业务还没有执行完成的时候,在redisson中引入了一个看门狗机制,就是说每隔一段时间就检查当前业务是否还持有锁,如果持有就增加加锁的持有时间,当业务执行完成之后需要使用释放锁就可以了

Redis集群有哪些方案?

主从复制、哨兵模式、Redis分片集群

主从同步:单节点Redis的并发能力是有上限的,要进一步提高Redis的并发能力,可以搭建主从集群,实现读写分离。一般都是一主多从,主节点负责写数据,从节点负责读数据,主节点写入数据之后,需要把数据同步到从节点中

主从同步数据的流程:

  • 全量同步:全量同步是指从节点第一次与主节点建立连接的时候使用全量同步

    1. 从节点请求主节点同步数据,其中从节点会携带自己的replication id和offset偏移量。
    2. 主节点判断是否是第一次请求,主要判断的依据就是,主节点与从节点是否是同一个replication id,如果不是,就说明是第一次同步,那主节点就会把自己的replication id和offset发送给从节点,让从节点与主节点的信息保持一致。
    3. 在同时主节点会执行bgsave,生成rdb文件后,发送给从节点去执行,从节点先把自己的数据清空,然后执行主节点发送过来的rdb文件,这样就保持了一致

    当然,如果在rdb生成执行期间,依然有请求到了主节点,而主节点会以命令的方式记录到缓冲区,缓冲区是一个日志文件,最后把这个日志文件发送给从节点,这样就能保证主节点与从节点完全一致了,后期再同步数据的时候,都是依赖于这个日志文件,这个就是全量同步

增量同步:当从节点服务重启之后,数据就不一致了,所以这个时候,从节点会请求主节点同步数据,主节点还是判断不是第一次请求,不是第一次就获取从节点的offset值,然后主节点从命令日志中获取offset值之后的数据,发送给从节点进行数据同步

什么是Redis集群脑裂,如何解决?

有的时候由于网络等原因可能会出现脑裂的情况,就是说,由于redis master节点和redis salve节点和sentinel处于不同的网络分区,使得sentinel没有能够心跳感知到master,所以通过选举的方式提升了一个salve为master,这样就存在了两个master,就像大脑分裂了一样,这样会导致客户端还在old master那里写入数据,新节点无法同步数据,当网络恢复后,sentinel会将old master降为salve,这时再从新master同步数据,这会导致old master中的大量数据丢失。

解决方案:在redis的配置中可以设置:第一可以设置最少的salve节点个数,比如设置至少要有一个从节点才能同步数据,第二个可以设置主从数据复制和同步的延迟时间,达不到要求就拒绝请求,就可以避免大量的数据丢失

Redis的分片集群有什么作用?分片集群中数据是怎么存储和读取的?

分片集群主要解决的是,海量数据存储的问题,集群中有多个master,每个master保存不同数据,并且还可以给每个master设置多个slave节点,就可以继续增大集群的高并发能力。同时每个master之间通过ping监测彼此健康状态,就类似于哨兵模式了。当客户端请求可以访问集群任意节点,最终都会被转发到正确节点

Redis 集群引入了哈希槽的概念,有 16384 个哈希槽,集群中每个主节点绑定了一定范围的哈希槽范围, key通过 CRC16 校验后对 16384 取模来决定放置哪个槽,通过槽找到对应的节点进行存储。

Redis是单线程的,但是为什么还那么快?能解释一下I/O多路复用模型?

有以下几个原因:

  1. 完全基于内存的,C语言编写
  2. 采用单线程,避免不必要的上下文切换可竞争条件
  3. 使用多路I/O复用模型,非阻塞IO

例如:bgsave 和 bgrewriteaof 都是在后台执行操作,不影响主线程的正常使用,不会产生阻塞

I/O多路复用是指利用单个线程来同时监听多个Socket ,并在某个Socket可读、可写时得到通知,从而避免无效的等待,充分利用CPU资源。目前的I/O多路复用都是采用的epoll模式实现,它会在通知用户进程Socket就绪的同时,把已就绪的Socket写入用户空间,不需要挨个遍历Socket来判断是否就绪,提升了性能。

其中Redis的网络模型就是使用I/O多路复用结合事件的处理器来应对多个Socket请求,比如,提供了连接应答处理器、命令回复处理器,命令请求处理器;

在Redis6.0之后,为了提升更好的性能,在命令回复处理器使用了多线程来处理回复事件,在命令请求处理器中,将命令的转换使用了多线程,增加命令转换速度,在命令执行的时候,依然是单线程

Spring

Spring框架中的bean是单例的吗?

不是线程安全的。

如果注入的对象是无状态,没有线程安全问题,如果在bean中定义了可修改的成员变量,是要考虑线程安全问题的,可以使用多例或者加锁来解决,Spring框架中有一个@Scope注解,默认的值就是singleton,可以把singleton改为prototype保证线程安全。

IOC

什么是AOP?你有没有用过AOP?

AOP:将对多个对象产生影响的公共行为和逻辑,抽取并封装为一个可重用的模块,这个模块被命名为“切面”(Aspect),它可以减少系统中的重复代码,降低了模块间的耦合度,同时提高了系统的可维护性。

常见的AOP使用场景:

  • 记录操作日志
  • 缓存处理
  • Spring中内置的事务处理

记录操作日志,缓存,spring实现的事务
核心是:使用aop中的环绕通知+切点表达式(找到要记录日志的方法),通过环绕通知的参数获取请求方法的参数(类、方法、注解、请求方式等),获取到这些参数以后,保存到数据库

事务是如何实现的?事务失效的场景有哪些?

事务分为编程式事务声明式事务

  • 编程式事务控制:需使用TransactionTemplate来进行实现,对业务代码有侵入性,项目中很少使用
  • 声明式事务管理:声明式事务管理建立在AOP之上的。其本质是通过AOP功能,对方法前后进行拦截,将事务处理的功能编织到拦截的方法中,也就是在目标方法开始之前加入一个事务,在执行完目标方法之后根据执行情况提交或者回滚事务。

其本质是通过AOP功能,对方法前后进行拦截,在执行方法之前开启事务,在执行完目标方法之后根据执行情况提交或者回滚事务。

事务失效:

  • 异常捕获处理
  • 抛出检查异常
  • 非public方法