自学内容网 自学内容网

后端开发面经系列 -- 滴滴一二面

滴滴Java后端面经

公众号:阿Q技术站

来源:https://www.nowcoder.com/discuss/604604903868092416

计算机网络

1、TCP三次握手和四次挥手?

三次握手

三次握手

  1. 第一次握手(SYN-1):
    • 客户端发送一个带有 SYN 标志的 TCP 报文段给服务器,表示客户端请求建立连接。
    • 客户端选择一个初始序列号(ISN)并将其放入报文段中,进入 SYN_SENT 状态。
  2. 第二次握手(SYN + ACK):
    • 服务器收到客户端发送的 SYN 报文段后,如果同意建立连接,会发送一个带有 SYN 和 ACK 标志的报文段给客户端,表示服务器接受了客户端的请求,并带上自己的 ISN。
    • 服务器进入 SYN_RCVD 状态。
  3. 第三次握手(ACK):
    • 客户端收到服务器发送的 SYN+ACK 报文段后,会发送一个带有 ACK 标志的报文段给服务器,表示客户端确认了服务器的响应。
    • 客户端和服务器都进入 ESTABLISHED 状态,连接建立成功,可以开始进行数据传输。
四次挥手

四次挥手

  1. 第一次挥手(FIN-1):
    • 客户端发送一个 FIN 报文段给服务器,表示客户端已经没有数据要发送了,请求关闭连接。
    • 客户端进入 FIN_WAIT_1 状态,等待服务器的确认。
  2. 第二次挥手(ACK):
    • 服务器收到客户端的 FIN 报文段后,发送一个 ACK 报文段作为应答,表示已经接收到了客户端的关闭请求。
    • 服务器进入 CLOSE_WAIT 状态,等待自己的数据发送完毕。
  3. 第三次挥手(FIN-2):
    • 服务器发送一个 FIN 报文段给客户端,表示服务器也没有数据要发送了,请求关闭连接。
    • 服务器进入 LAST_ACK 状态,等待客户端的确认。
  4. 第四次挥手(ACK):
    • 客户端收到服务器的 FIN 报文段后,发送一个 ACK 报文段作为应答,表示已经接收到了服务器的关闭请求。
    • 客户端进入 TIME_WAIT 状态,等待可能出现的延迟数据。
    • 服务器收到客户端的 ACK 报文段后,完成关闭,进入 CLOSED 状态。
    • 客户端在 TIME_WAIT 状态结束后,关闭连接,进入 CLOSED 状态。

2、从输入URL到页面发生了啥?

  1. DNS 解析:浏览器首先将输入的 URL 解析成对应的 IP 地址,这个过程称为 DNS 解析。浏览器会首先查找本地 DNS 缓存,如果没有找到则向本地 DNS 服务器发送查询请求,依次向上级 DNS 服务器查询,直到找到对应的 IP 地址。
  2. 建立 TCP 连接:一旦浏览器获得了服务器的 IP 地址,就会尝试与服务器建立 TCP 连接。TCP 连接的建立使用了三次握手的过程,确保通信双方都准备好进行数据传输。
  3. 发起 HTTP 请求:TCP 连接建立后,浏览器会向服务器发送一个 HTTP 请求。请求中包含了要访问的页面路径、请求方法(如 GET、POST 等)、请求头等信息。
  4. 服务器处理请求:服务器接收到 HTTP 请求后,会根据请求的路径和方法进行相应的处理,如读取文件、查询数据库等。处理完成后,服务器会将处理结果封装成 HTTP 响应返回给客户端。
  5. 接收 HTTP 响应:浏览器接收到服务器返回的 HTTP 响应后,会对响应进行解析,包括状态码、响应头和响应体等内容。
  6. 渲染页面:浏览器根据收到的响应数据开始渲染页面,这个过程包括解析 HTML、加载 CSS、执行 JavaScript、构建 DOM 树、生成渲染树和绘制页面等步骤。
  7. 显示页面:最后,浏览器根据渲染好的页面内容显示页面,用户可以看到页面的内容和样式。

操作系统

1、用户态和内核态的区别?

  1. 权限级别
    • 用户态(User Mode):程序在用户态下运行时,只能访问受限资源,无法直接访问操作系统的核心功能和硬件资源。例如,普通应用程序在用户态下运行,只能通过系统调用来请求内核完成特权操作,如文件操作、网络通信等。
    • 内核态(Kernel Mode):程序在内核态下运行时,具有更高的特权级别,可以直接访问操作系统的核心功能和硬件资源,如内存管理、进程调度、设备驱动等。
  2. 特权指令
    • 用户态:由于受到限制,用户态下的程序不能直接执行特权指令,需要通过系统调用进入内核态才能执行。特权指令是一些只有在内核态下才能执行的指令,如修改内存映射、改变进程状态等。
    • 内核态:内核态下的程序可以直接执行特权指令,可以完成各种操作系统核心功能,如修改硬件状态、管理进程等。
  3. 异常和中断处理
    • 用户态:当发生异常或中断时,处理程序会在内核态下执行,以确保对异常或中断的正确处理。用户态下的程序无法直接处理异常或中断。
    • 内核态:内核态下的程序可以直接处理异常或中断,并根据需要采取相应的措施,如中断服务程序可以保存现场、执行相应的处理逻辑,然后恢复现场继续执行。
  4. 资源访问
    • 用户态:程序在用户态下运行时,只能访问用户空间的资源,如进程的用户态内存空间。
    • 内核态:程序在内核态下运行时,可以访问所有的资源,包括内核空间的资源和硬件资源。

2、线程和进程的区别?

  1. 定义
    • 进程(Process):进程是程序的一次执行过程,是操作系统进行资源分配和调度的基本单位。每个进程拥有独立的内存空间和系统资源。
    • 线程(Thread):线程是进程中的一个执行单元,是操作系统进行调度的基本单位。线程共享进程的内存空间和系统资源,但拥有独立的执行栈和程序计数器。
  2. 资源占用
    • 进程:每个进程有独立的内存空间和系统资源,包括代码、数据、堆栈、打开的文件、信号量等。
    • 线程:线程共享进程的内存空间和系统资源,包括代码、数据、堆栈,但拥有独立的执行栈和程序计数器。
  3. 切换开销
    • 进程:进程间的切换开销较大,因为需要保存和恢复整个进程的状态,包括内存空间、寄存器等。
    • 线程:线程间的切换开销较小,因为线程共享进程的内存空间和系统资源,只需要保存和恢复执行栈和程序计数器。
  4. 通信机制
    • 进程:进程间通信比较复杂,可以使用管道、信号、消息队列、共享内存等机制。
    • 线程:线程间通信比较简单,可以直接共享进程的内存空间来进行通信。
  5. 并发性
    • 进程:进程之间是独立的,拥有自己的内存空间和系统资源,因此进程间的并发性较低。
    • 线程:线程共享进程的内存空间和系统资源,可以更方便地实现并发执行,提高程序的响应速度和效率。

Java基础

1、HashSet 和 HashMap的区别和实现原理?

  1. 区别
    • HashSet:HashSet 是基于哈希表实现的 Set 集合,不允许集合中有重复元素。HashSet 不保证集合中元素的顺序,即不保证元素的存储顺序和插入顺序一致。
    • HashMap:HashMap 是基于哈希表实现的 Map 集合,用于存储键值对。HashMap 允许键和值都为 null,并且允许有一个键为 null 的键值对。HashMap 不保证键值对的顺序,即不保证键值对的存储顺序和插入顺序一致。
  2. 实现原理
    • HashSet 的实现原理是基于 HashMap 实现的。HashSet 内部维护了一个 HashMap 实例,将集合中的元素作为 HashMap 的键,所有的值都存储在一个特殊的对象中(如 PRESENT = new Object()),然后将这个特殊的对象作为 HashMap 的值。这样,在 HashSet 中添加元素实际上是将元素作为键存储到 HashMap 中。
    • HashMap 的实现原理是基于数组和链表(或红黑树)实现的。HashMap 内部维护了一个数组,称为哈希桶(buckets),每个桶存储一个链表(或红黑树),用于解决哈希冲突。当向 HashMap 中添加键值对时,先根据键的哈希值确定存储位置,然后将键值对存储在对应的桶中。如果发生哈希冲突,即多个键具有相同的哈希值,这些键值对会以链表(或红黑树)的形式存储在同一个桶中,通过比较键的值来确定具体的键值对。

2、HashMap插入元素的过程?

  1. 计算哈希值:首先,根据要插入元素的键计算其哈希值。HashMap 使用键的哈希值来确定元素在内部数组中的存储位置。
  2. 计算存储位置:将计算得到的哈希值映射到内部数组的索引位置。HashMap 使用取余运算将哈希值映射到数组的索引位置,即 index = hash % array.length
  3. 检查是否存在冲突:在确定了存储位置之后,需要检查该位置是否已经存在元素。如果该位置已经有元素存在,则发生了哈希冲突,需要处理冲突。
  4. 处理哈希冲突:如果发生了哈希冲突,HashMap 使用链表或红黑树来解决冲突。新元素将被插入到链表或红黑树中,作为该位置的新节点。
  5. 插入元素:如果没有发生哈希冲突,直接将新元素插入到计算得到的存储位置。如果该位置已经存在元素,则将新元素插入到链表或红黑树中。
  6. 检查是否需要扩容:在插入元素后,需要检查是否需要对 HashMap 进行扩容。如果当前元素个数超过了负载因子(load factor)乘以数组长度的阈值,则需要进行扩容操作。
  7. 扩容:如果需要扩容,HashMap 将创建一个新的更大的数组,并将所有元素重新分配到新数组中。这个过程涉及到重新计算元素的存储位置,并将元素移动到新数组中。
  8. 更新数组和元素计数器:最后,HashMap 更新内部数组的引用为新数组,并更新元素的计数器,增加插入的元素数量。

3、Synchronized 和 Volatile的区别和解决了什么?

  1. Synchronized
    • 用于实现同步代码块和同步方法。它可以确保在同一时刻只有一个线程可以访问被 Synchronized 修饰的代码块或方法,从而保证了多线程情况下的数据安全性。
    • Synchronized 通过获取对象的锁(即内置锁或监视器锁)来实现同步。当一个线程获取了对象的锁时,其他线程将被阻塞,直到锁被释放。
    • Synchronized 可以解决多线程访问共享资源时的数据竞争和同步问题,确保线程安全性。
  2. Volatile
    • 用于修饰变量。被 Volatile 修饰的变量可以保证在多线程环境下的可见性,即当一个线程修改了 Volatile 变量的值后,其他线程能够立即看到修改后的值。
    • Volatile 并不能保证原子性,即不能保证复合操作的线程安全性。例如,对一个 Volatile 变量的自增操作并不能保证原子性,可能会出现竞态条件(Race Condition)。
    • Volatile 主要用于简单变量的状态标记或者作为触发器,在某些场景下可以代替 Synchronized 实现线程间的通信和同步。
  3. 区别
    • Synchronized 是一种排它性的锁机制,只允许一个线程访问共享资源,其他线程需要等待锁释放;而 Volatile 是一种轻量级的同步机制,保证变量的可见性,但不能保证原子性。
    • Synchronized 可以用于代码块和方法的同步,提供了更强的线程安全性保证;而 Volatile 主要用于保证变量的可见性,适用于简单的状态标记和变量同步。
    • Synchronized 是一种悲观锁机制,需要获取和释放锁,会引入一定的性能开销;而 Volatile 是一种轻量级的机制,对性能影响较小。

4、Synchronized的锁升级过程?

  1. 偏向锁(Biased Locking)
    • 初始状态下,对象的锁是偏向锁。当一个线程访问一个同步代码块时,会尝试获取对象的偏向锁。如果对象的偏向锁是可用的(即没有被其他线程占用),则当前线程会获取偏向锁,并将对象头中的线程ID设置为当前线程的ID。
    • 如果对象的偏向锁已经被其他线程占用,则需要撤销偏向锁,将对象的锁升级为轻量级锁。
  2. 轻量级锁(Lightweight Lock)
    • 当一个线程无法获取对象的偏向锁时,会尝试升级为轻量级锁。线程会在栈帧中创建一个锁记录(Lock Record),并尝试使用 CAS(Compare and Swap)操作将对象的标记字段替换为指向锁记录的指针。
    • 如果 CAS 操作成功,表示线程成功获取了轻量级锁,并继续执行同步代码块。如果 CAS 操作失败,说明存在竞争,需要进一步升级为重量级锁。
  3. 重量级锁(Heavyweight Lock)
    • 如果轻量级锁的 CAS 操作失败,表示存在竞争,锁会升级为重量级锁。重量级锁会将对象的锁信息存储在对象监视器(Monitor)中,通过操作系统的互斥量来实现锁的获取和释放。
    • 重量级锁采用阻塞的方式来保证同一时刻只有一个线程可以获取锁,其他线程需要等待锁释放后才能继续执行。

5、Synchronized是公平锁吗?ReentrantLock是怎么实现公平锁的?

Synchronized 不是公平锁,它是一种非公平锁。在 Synchronized 中,当一个线程释放锁时,JVM 会从等待队列中随机选择一个等待线程获取锁,而不考虑等待线程等待的时间长短。

ReentrantLock 是 Java 中提供的显式锁(Explicit Lock),可以实现公平锁和非公平锁。ReentrantLock 默认是非公平锁,但可以通过构造函数参数指定为公平锁。实现公平锁的关键在于 ReentrantLock 内部维护了一个等待队列(Wait Queue),并且在获取锁时会先判断当前线程是否应该优先获取锁(即判断是否是等待队列中的第一个线程)。

ReentrantLock 实现公平锁的原理:

  1. 等待队列:ReentrantLock 内部维护了一个等待队列,用于存放等待获取锁的线程。
  2. 判断是否公平:在构造 ReentrantLock 对象时,可以通过参数指定是否为公平锁。如果指定为公平锁,在获取锁时会先判断当前线程是否应该优先获取锁。
  3. 获取锁:当一个线程尝试获取锁时,如果是公平锁且等待队列中有等待线程,则当前线程会加入等待队列尾部,并设置标记表示当前线程在等待队列中。如果当前线程是等待队列中的第一个线程,则当前线程可以获取锁;否则,当前线程会进入阻塞状态等待唤醒。
  4. 释放锁:当持有锁的线程释放锁时,会检查等待队列中是否有等待线程。如果有等待线程且是公平锁,则会唤醒等待队列中的第一个线程,使其尝试获取锁。

6、Java中创建对象的过程?

  1. 类加载:在使用一个类之前,需要先将该类加载到内存中。类加载的过程包括加载、链接和初始化三个阶段。加载阶段是指将类的字节码文件加载到内存中;链接阶段包括验证、准备和解析三个步骤;初始化阶段是指执行类构造器 <clinit> 方法的过程。
  2. 类加载器:Java 中的类加载器负责加载类的字节码文件,Java 中的类加载器有三种:Bootstrap ClassLoader、Extension ClassLoader 和 Application ClassLoader。
  3. 创建对象
    • 类加载器加载类:首先,类加载器根据类的全限定名(包括包名和类名)加载类的字节码文件到内存中。
    • 分配内存空间:在加载类的过程中,JVM 会为对象分配内存空间。根据对象的大小(由类的实例变量和对齐方式决定),在堆内存中分配一块连续的内存空间。
    • 初始化对象头:在分配内存空间后,JVM 会初始化对象头,包括对象的哈希码、锁信息、垃圾回收信息等。
    • 执行构造方法:最后,JVM 调用对象的构造方法(构造函数)来初始化对象的实例变量。构造方法会执行对象的初始化操作,可以为对象的实例变量赋初值。
  4. 返回对象引用:在对象初始化完成后,JVM 返回对象的引用(即对象的地址),该引用可以被赋值给变量,从而可以通过变量来访问对象的实例变量和方法。

7、垃圾回收的过程?

垃圾回收(Garbage Collection)是指在运行时自动查找和释放不再被程序使用的内存的过程。Java 使用垃圾回收机制来管理内存,使开发人员不需要手动管理内存分配和释放,从而减少了内存泄漏和野指针等问题。

垃圾回收的过程

  1. 标记:垃圾回收器首先会标记出所有活动对象(即仍然被引用的对象)。它从根对象开始,递归地遍历对象图,标记所有可以从根对象访问到的对象。
  2. 清除:在标记完成后,垃圾回收器会扫描堆内存,清除所有未被标记的对象。这些未被标记的对象被认为是不再被程序使用的垃圾对象。
  3. 压缩(可选):在清除垃圾对象后,如果需要,垃圾回收器会对堆内存进行压缩,将存活对象移到一端,以便后续的内存分配更加高效。
  4. 回收资源:在清除垃圾对象后,垃圾回收器会释放这些对象所占用的内存,并将内存返回给堆内存的可用空间。

Java 中的垃圾回收器采用了不同的算法来实现垃圾回收,主要包括以下几种:

  • 标记-清除算法(Mark-Sweep):最基本的垃圾回收算法,先标记出所有活动对象,然后清除所有未被标记的对象。
  • 复制算法(Copying):将存活对象复制到另一个区域,并在复制过程中进行压缩,最后清除原来的内存区域。
  • 标记-整理算法(Mark-Compact):标记出所有活动对象,然后将存活对象移动到一端,最后清除剩余的内存空间。

数据库

1、CHAR 和 VARCHAR有什么区别?

  1. CHAR
    • CHAR 是一种固定长度的字符类型,需要指定固定的长度,例如 CHAR(10) 表示存储长度为 10 的字符。
    • CHAR 类型的数据会在存储时被右侧填充空格,保证存储的字符串长度达到指定的长度。
  2. VARCHAR
    • VARCHAR 是一种可变长度的字符类型,需要指定最大长度,例如 VARCHAR(255) 表示最大长度为 255 的字符串。
    • VARCHAR 类型的数据只会存储实际的字符数据,不会进行填充。

区别

  • 存储空间:由于 CHAR 是固定长度,所以存储空间是固定的,而 VARCHAR 是可变长度,存储实际字符数据,所以存储空间是根据实际数据长度变化的,VARCHAR 在存储长度不定的字符串时,通常占用的存储空间比 CHAR 少。
  • 空格处理:CHAR 类型在存储时会填充空格,而 VARCHAR 不会填充空格,只存储实际字符数据,因此在存储大量长度不定的字符串时,VARCHAR 更节省空间。
  • 适用场景:CHAR 适用于长度固定的字符串存储,例如存储固定长度的国家代码、性别代码等;VARCHAR 适用于长度不定的字符串存储,例如存储用户的姓名、地址等。

2、为什么MySQL索引结构是B+树?

  1. 查询效率高:B+ 树是一种多路平衡查找树,具有良好的平衡性质,每个节点可以存储多个关键字和对应的指针,使得在范围查找和单值查找中都能够快速定位到目标记录。
  2. 磁盘 I/O 优化:B+ 树的叶子节点构成了一个有序链表,可以按照顺序遍历叶子节点来实现范围查询,减少了磁盘 I/O 次数,提高了查询效率。
  3. 支持范围查询:B+ 树的有序性使得范围查询变得简单高效,只需从起始位置开始顺序遍历叶子节点即可。
  4. 支持快速插入和删除:B+ 树的平衡性和节点分裂合并的特性使得在插入和删除操作时能够保持树的平衡,不会出现明显的不平衡现象,保证了高效的插入和删除操作。
  5. 适应大数据量:B+ 树的高度相对较低,能够很好地适应大数据量的索引需求,减少了索引的存储空间和维护成本。

3、介绍一下索引?

索引是数据库中用于加快数据检索速度的一种数据结构。索引类似于书籍的目录,可以根据关键字快速找到对应的数据行,从而提高查询效率。在数据库中,索引通常是在表的某个列上创建的,用于加速对该列的查询操作。

作用

  1. 加快数据检索速度:通过索引,数据库可以直接定位到符合条件的数据行,而不需要扫描整个表,从而大大减少了数据检索的时间。
  2. 加速排序:索引可以帮助数据库在排序操作中快速定位数据,提高排序的效率。
  3. 加速连接操作:在连接操作中,如果连接的列上有索引,数据库可以利用索引快速定位到需要连接的数据行,从而提高连接操作的效率。
  4. 唯一性约束:索引可以用于实现唯一性约束,确保某个列或列组合的值在表中是唯一的。

常见的索引类型

  • 单列索引:针对单个列创建的索引。
  • 多列索引:针对多个列创建的索引,可以用于加速多列条件查询和排序操作。
  • 唯一索引:保证索引列的值在表中是唯一的。
  • 复合索引:包含多个列的索引,可以提高查询效率,但需要注意索引列的顺序。

4、介绍一下日志?

  1. 事务日志(Transaction Log):记录了数据库中所有事务的操作序列,包括事务的开始、提交或回滚操作。事务日志是数据库恢复和并发控制的重要基础。通过事务日志,可以在数据库发生崩溃或异常情况时恢复数据库的一致性。
  2. 归档日志(Archive Log):数据库事务日志的一种备份形式,用于在数据库崩溃时或进行数据库恢复时使用。归档日志通常会被定期备份到磁盘或磁带中,以防止日志文件过大或丢失导致的数据不一致性。
  3. 重做日志(Redo Log):数据库用于实现事务的持久性和恢复性的重要手段。当事务提交时,数据库会将事务修改的数据记录到重做日志中,以便在数据库崩溃时重新应用这些修改,确保事务的持久性。
  4. 回滚日志(Undo Log):记录事务的回滚操作,即撤销事务所做的修改。当事务需要回滚时,数据库会使用回滚日志中的信息将数据恢复到事务开始前的状态。
  5. 日志缓冲区(Log Buffer):数据库用于缓存事务日志的内存区域,用于提高日志记录的效率。数据库将事务操作记录到日志缓冲区后,会定期将日志写入到物理的日志文件中。
  6. 日志刷写(Log Flush):将日志缓冲区中的日志内容刷写到磁盘或归档设备中。日志刷写操作通常在事务提交时执行,以确保事务的持久性和一致性。

5、redo log怎么保证持久性?

  1. 事务提交:当一个事务提交时,数据库会将事务的所有修改操作记录到 redo log 中,包括对数据页的修改等。
  2. 写入redo log缓冲区:redo log 会首先将事务的修改操作写入到 redo log 缓冲区中,这个操作是在内存中进行的,速度非常快。
  3. 刷写redo log到磁盘:虽然redo log 缓冲区是在内存中的,但是为了保证持久性,数据库会定期将 redo log 缓冲区的内容刷写到磁盘中。这个操作通常是异步的,数据库会根据一定的策略和算法来决定何时将 redo log 刷写到磁盘中,以保证性能和可靠性的平衡。
  4. redo log的重放:当数据库发生故障或者崩溃时,需要通过 redo log 来恢复数据。数据库会根据 redo log 中的记录,重新执行事务的修改操作,从而将数据恢复到崩溃前的状态。
  5. 持久性保证:通过将 redo log 的内容刷写到磁盘中,并在发生故障时利用 redo log 进行数据恢复,数据库可以保证事务的持久性。即使数据库发生崩溃,redo log 中的操作也能够确保事务的修改能够被正确地应用到数据库中,从而保证数据的一致性和可靠性。

6、能不能只用binlog不用relo log?

在 MySQL 中,一般情况下是同时使用 binlog 和 redo log 的,以实现数据的高可用性和持久性保证。如果只使用 binlog 而不使用 redo log,可能会导致数据库在发生崩溃时无法正确恢复数据,因为 binlog 只记录了逻辑操作而没有记录物理操作。

  1. binlog(二进制日志)
    • binlog 是 MySQL 中用于记录数据库中所有的数据修改操作(如 INSERT、UPDATE、DELETE)的日志文件。
    • binlog 记录的是逻辑日志,即记录了对数据的逻辑修改操作,而不是物理修改。
    • binlog 可以用于数据复制(replication)、恢复、数据恢复到指定时间点(point-in-time recovery)等操作。
    • binlog 的缺点是它是以文本格式记录的,写入速度较慢,并且会占用较多的磁盘空间。
  2. redo log(重做日志)
    • redo log 是 InnoDB 存储引擎特有的日志文件,用于保证事务的持久性。
    • redo log 记录的是物理日志,即记录了对数据页的物理修改操作。
    • redo log 可以在数据库发生崩溃时,通过重放 redo log 中的操作来恢复数据。
    • redo log 的优点是它是以二进制格式记录的,写入速度快,并且占用较少的磁盘空间。

7、ACID特性?

  1. 原子性(Atomicity):原子性要求事务中的所有操作要么全部成功,要么全部失败回滚,不能只执行其中的一部分操作。这样可以确保事务不会在执行过程中被中断,从而避免了数据不一致的情况。
  2. 一致性(Consistency):一致性要求事务执行前后数据库的状态保持一致。即使在事务执行过程中发生了错误,也要保证数据库的状态能够从一个一致性状态转换到另一个一致性状态。
  3. 隔离性(Isolation):隔离性要求数据库系统要能够同时处理多个事务,并且每个事务的操作对其他事务是隔离的,不会互相影响。隔离性可以防止并发事务导致的一些问题,如脏读、不可重复读和幻读。
  4. 持久性(Durability):持久性要求一旦事务提交,其修改的数据将会被永久保存在数据库中,即使发生系统故障或者重启,数据也不会丢失。持久性通常通过将事务的修改操作记录到日志中,并在事务提交后将日志刷新到磁盘中来实现。

8、四个事务隔离级别?

  1. 读未提交(Read Uncommitted)
    • 最低的隔离级别,允许事务读取其他事务未提交的数据。
    • 可能导致脏读(Dirty Read),即一个事务读取到另一个事务未提交的数据,可能会导致数据不一致。
  2. 读已提交(Read Committed)
    • 事务只能读取已经提交的数据,不能读取其他事务未提交的数据。
    • 可能导致不可重复读(Non-Repeatable Read),即在同一个事务中,两次读取同一数据可能得到不同的结果。
  3. 可重复读(Repeatable Read)
    • 事务在执行期间多次读取同一数据会得到相同的结果,即保证了同一事务内读取的数据是一致的。
    • 可能导致幻读(Phantom Read),即在同一个事务中,两次查询的结果集不一致,可能会出现新增或删除的数据。
  4. 串行化(Serializable)
    • 最高的隔离级别,要求事务串行执行,即一个事务在执行时会对整个表加锁,防止其他事务对其进行修改。
    • 可以避免脏读、不可重复读和幻读,但会降低并发性能。

9、可重复读是由什么实现的?

可重复读(Repeatable Read)隔离级别是通过数据库管理系统(DBMS)中的多版本并发控制(MVCC)机制来实现的。

10、介绍一下MVCC原理?

MVCC 是一种用于在并发环境下保证事务隔离性的技术,它可以确保同一事务内多次读取同一数据时,得到的结果是一致的。

MVCC 的实现原理:

  1. 版本号:在 MVCC 中,每个数据都会有一个版本号或者时间戳,用于标识数据的版本。当数据被更新时,会生成一个新的版本,并保留原始版本的快照。
  2. 读取操作:当事务需要读取数据时,数据库会根据事务的隔离级别来确定读取的数据版本。在可重复读隔离级别下,事务会读取已提交的数据版本,而不会读取其他事务未提交的数据版本。
  3. 写入操作:当事务需要修改数据时,数据库会生成一个新的数据版本,并将事务的事务标识(如事务 ID)与数据版本关联起来。其他事务在读取数据时,会根据事务的隔离级别来决定是否能够读取该数据版本。
  4. 事务提交:当事务提交时,数据库会将事务所修改的数据版本标记为已提交,并且将事务生成的新数据版本对应的版本号或时间戳更新为最新的。

11、介绍一下锁?

  1. 行级锁(Row-level Lock)
    • 行级锁是针对数据表中的单行记录进行的锁定操作。
    • 当事务需要修改某行数据时,可以对该行应用行级锁,防止其他事务同时修改同一行数据,保证数据的一致性和完整性。
    • 行级锁可以减少锁的粒度,提高并发性能,但也可能增加锁的数量,导致锁冲突的可能性增加。
  2. 表级锁(Table-level Lock)
    • 表级锁是针对整个数据表进行的锁定操作。
    • 当事务需要对整个表进行操作时,可以对整个表应用表级锁,防止其他事务对该表进行修改。
    • 表级锁的粒度较粗,会降低并发性能,但可以简化锁管理,减少锁冲突的可能性。
  3. 数据库级锁(Database-level Lock)
    • 数据库级锁是针对整个数据库进行的锁定操作。
    • 当需要对整个数据库进行操作时,可以对数据库应用数据库级锁,防止其他事务对数据库进行修改。
    • 数据库级锁的使用较少,通常在需要进行数据库备份或维护时才会使用。
  4. 意向锁(Intent Lock)
    • 意向锁是为了提高并发性能而引入的一种锁机制,用于表明事务对数据的意图(读或写)。
    • 意向锁可以协助数据库管理系统判断是否需要加锁,从而减少锁冲突和提高并发性能。
  5. 共享锁(Shared Lock)和排他锁(Exclusive Lock)
    • 共享锁用于读取操作,多个事务可以同时持有共享锁,但排他锁互斥。
    • 排他锁用于写入操作,事务持有排他锁时,其他事务不能同时持有共享锁或排他锁。

12、update语句的具体执行过程?

  1. 语法解析:数据库管理系统(DBMS)首先会对 update 语句进行语法解析,检查语句是否符合语法规范,以及是否具有执行权限。
  2. 查询执行计划:DBMS 根据 update 语句中的条件以及表的结构信息,生成查询执行计划,确定如何访问表和哪些索引将被使用。
  3. 获取锁:在执行 update 之前,DBMS 会获取需要修改的数据行的适当锁,以确保在修改数据时不会发生并发问题。通常会使用行级锁或表级锁。
  4. 执行更新操作:DBMS 根据生成的执行计划,执行实际的更新操作。这包括根据 update 语句中的条件查找要更新的数据行,并将数据更新为指定的值。
  5. 事务日志记录:在执行更新操作时,DBMS 会将更新操作记录到事务日志中,以便在发生故障时可以通过重做日志(redo log)来恢复数据。
  6. 释放锁:一旦更新操作完成,DBMS 会释放之前获取的锁,允许其他事务访问被修改的数据行。
  7. 提交事务:如果 update 语句处于一个事务中,DBMS 在更新操作完成后会等待事务的提交。事务提交后,更新操作将变为永久性的,并且对其他事务可见。
  8. 返回结果:最后,DBMS 返回 update 语句执行的结果,通常是影响的行数或执行状态信息。

13、讲一下SQL优化方法 - 多个维度?

  1. 查询语句优化
    • 选择合适的字段:只选择需要的字段,避免不必要的字段查询。
    • 使用合适的操作符:使用等值查询(=)而不是范围查询(BETWEEN、>、<)。
    • 避免使用通配符:避免使用 ‘%’ 通配符在 LIKE 查询中,如果必须使用,也应该尽量避免在开头使用。
    • 避免在 WHERE 子句中对字段进行函数操作:这会导致索引失效,应该在查询之前对数据进行预处理。
  2. 索引优化
    • 为经常查询的字段创建索引:根据查询的字段和条件,创建合适的索引。
    • 使用覆盖索引:如果查询只需要索引中的数据,可以使用覆盖索引避免访问表的数据行。
    • 避免过多的索引:过多的索引会增加写操作的开销,并可能导致索引失效。
  3. 表结构优化
    • 避免使用过多的字段:只保留必要的字段,避免表过度冗余。
    • 合理设计主键和外键:使用适当的主键和外键,以提高数据的完整性和查询效率。
  4. 查询计划优化
    • 使用 EXPLAIN 分析查询计划:通过分析查询计划,可以了解查询是如何执行的,有助于优化查询。
  5. 避免使用子查询
    • 使用 JOIN 替代子查询:尽量使用 JOIN 操作替代子查询,因为 JOIN 操作通常比子查询效率更高。
  6. 缓存查询结果
    • 使用缓存:对于相同的查询结果,可以考虑使用缓存来提高查询性能。
  7. 分页优化
    • 使用 LIMIT 进行分页:在分页查询时,使用 LIMIT 关键字限制返回的数据量,避免一次性返回过多数据。
  8. 统计信息优化
    • 收集统计信息:定期收集表和索引的统计信息,以便优化查询计划。

14、如何解决深度分页的问题呢?

深度分页是指在大数据量的情况下,需要查询结果的某一页数据,但是传统的分页方式(比如使用 LIMIT offset, limit)可能会导致性能问题,因为数据库需要扫描大量数据并跳过前面的数据才能获取到需要的数据。

解决方法

  1. 使用游标分页:
    • 使用游标(cursor)来记录当前查询的位置,每次查询时根据游标位置获取数据。
    • 可以通过设置游标的初始位置和每次移动的步长来实现分页,避免了每次查询都需要扫描大量数据。
  2. 使用主键范围查询:
    • 如果数据表的主键是递增的,可以使用主键范围查询来实现分页。
    • 每次查询时指定主键范围,避免了扫描大量数据,提高了查询效率。
  3. 使用缓存:
    • 对于静态数据或者数据更新不频繁的情况,可以使用缓存来缓存查询结果,避免重复查询。
    • 缓存可以减轻数据库的压力,提高页面加载速度。
  4. 使用全文搜索引擎:
    • 对于需要全文搜索的情况,可以考虑使用全文搜索引擎(如Elasticsearch、Solr等),这些搜索引擎支持高效的分页查询。
  5. 数据预处理:
    • 对于需要频繁查询的数据,可以进行数据预处理,将结果存储在临时表或者缓存中,以提高查询效率。
  6. 使用分区表:
    • 对于数据量非常大的表,可以考虑使用分区表来分割数据,然后根据分区来进行分页查询,提高查询效率。

15、Redis的Zset底层怎么实现的?

Redis 的有序集合(Sorted Set,简称 Zset)是一种数据结构,它是基于跳跃表(Skip List)和哈希表(Hash Table)实现的。

底层实现

  1. 跳跃表(Skip List)
    • 跳跃表是一种有序链表的扩展结构,可以快速查找和插入数据,其查询和插入的时间复杂度均为 O(logN)。
    • 跳跃表通过维护多级索引来加快查找速度,每一级索引都是原始链表的子集,每个节点包含多个指针,指向下一级索引的对应节点。
  2. 有序集合的节点结构
    • 在 Redis 中,有序集合的每个节点包含两部分数据:成员(Member)和分值(Score)。
    • 成员是一个唯一的字符串,用来标识一个元素;分值是一个浮点数,表示元素的排序权重。
  3. 跳跃表的使用
    • Redis 的有序集合通过跳跃表来实现成员的有序存储和快速查找。
    • 每个有序集合都有一个跳跃表作为底层数据结构,用来存储成员和分值。
  4. 哈希表的使用
    • 为了快速定位成员在跳跃表中的位置,Redis 使用哈希表来保存成员和对应的跳跃表节点的指针。
    • 哈希表的键是成员,值是指向跳跃表节点的指针。
  5. 元素的插入和删除
    • 当向有序集合中插入一个元素时,Redis 首先在哈希表中查找成员对应的节点指针,然后将节点插入到跳跃表中,并更新哈希表。
    • 当删除一个元素时,Redis 首先在哈希表中查找成员对应的节点指针,然后从跳跃表中删除节点,并更新哈希表。

算法

在旋转排序数组中找一个数 - 要用一次循环nlogn写出来

LeetCode.33

思路
  1. 使用二分查找找到数组中的最小元素的索引,也就是旋转点的索引。这个过程中,我们可以使用类似标准二分查找的方法,通过比较中间元素和右边界元素的大小关系来判断最小元素的位置。
  2. 确定目标值所在的区间。如果数组的第一个元素小于等于目标值,说明目标值在旋转点的右侧有序部分;否则,目标值在旋转点的左侧有序部分。
  3. 在确定的区间内使用二分查找找到目标值。
参考代码
C++
#include <vector>
using namespace std;

int search(vector<int>& nums, int target) {
    int n = nums.size();
    if (n == 0) return -1;
    
    int left = 0, right = n - 1;
    while (left < right) {
        int mid = left + (right - left) / 2;
        if (nums[mid] > nums[right]) {
            left = mid + 1;
        } else {
            right = mid;
        }
    }
    
    int pivot = left;
    left = 0;
    right = n - 1;
    while (left <= right) {
        int mid = left + (right - left) / 2;
        int rotatedMid = (mid + pivot) % n;
        if (nums[rotatedMid] == target) {
            return rotatedMid;
        } else if (nums[rotatedMid] < target) {
            left = mid + 1;
        } else {
            right = mid - 1;
        }
    }
    
    return -1;
}

int main() {
    vector<int> nums = {4, 5, 6, 7, 0, 1, 2};
    int target = 0;
    int result = search(nums, target);
    return 0;
}
Java
public class Main {
    public static int search(int[] nums, int target) {
        int n = nums.length;
        if (n == 0) return -1;
        
        int left = 0, right = n - 1;
        while (left < right) {
            int mid = left + (right - left) / 2;
            if (nums[mid] > nums[right]) {
                left = mid + 1;
            } else {
                right = mid;
            }
        }
        
        int pivot = left;
        left = 0;
        right = n - 1;
        while (left <= right) {
            int mid = left + (right - left) / 2;
            int rotatedMid = (mid + pivot) % n;
            if (nums[rotatedMid] == target) {
                return rotatedMid;
            } else if (nums[rotatedMid] < target) {
                left = mid + 1;
            } else {
                right = mid - 1;
            }
        }
        
        return -1;
    }
    
    public static void main(String[] args) {
        int[] nums = {4, 5, 6, 7, 0, 1, 2};
        int target = 0;
        int result = search(nums, target);
        System.out.println(result);
    }
}

原文地址:https://blog.csdn.net/qq_44630255/article/details/138008971

免责声明:本站文章内容转载自网络资源,如本站内容侵犯了原著者的合法权益,可联系本站删除。更多内容请关注自学内容网(zxcms.com)!