面试问题
1、项目中有没有使用过MySQL优化
使用过:
缓存层面: 使用缓存 \ 提高缓存的命中率
-
在查找专辑详情页时, 因为专辑详情页信息非常多, 是用户经常访问的请求, 所以我在处理这种请求时 ,使用了 redis作为缓存来降低对mysql 的访问.
并且为了处理数据库和缓存不一致的问题, 我采用了延迟双删的方式来,确保了数据库和缓存的最终一致性
业务维度层面: 减少不必要的sql \ 批量处理
- 在进行多个声音的查找时, 为了避免多次查找数据库, 我将在查找声音的详细信息时, 一次性将所有的全部id进行远程调用, 一次性查找全部的声音信息,避免了一些不必要的查找
索引优化:
-
建立索引 :
条件 \ order \ group 使用索引\区分度高的使用使用\经常查询的字段\使用连表查询,避免子查询- 因为在项目中,在对专辑详细进行查找时 ,进行使用 专辑的付费类型来查找专辑信息, 我对price_type这个字段设置了索引
- 在查看专辑的全部声音的接口中, 需要对声音信息进行排序, 而声音表中有个字段就是用来排序的, order这个字段, 我对这个字段也设置了索引
-
避免索引失效:
计算 + 函数\like以%开头\is null \ is not null\避免索引类型转换\!= \ <>\排序: 无过滤\建立和使用的顺序不一致\方法反 ASC \ DESC -
查询优化器:
手动指定驱动表\避免死锁和解决死锁- 在专辑和声音表进行联查时, 由于声音的信息比专辑表的行数据多, 所以, 我在进行连表查询时, 使用了STRAIGHT_JOIN关键字, 将这两个表的连表查询的驱动表设置为专辑表
-
**数据层面: **
分库分表\冷热数据分离\历史数据归档 -
数据库配置层面:
-
客户端配置:
最大连接池\最小空闲连接数\空闲连接超时时间\连接的最大生命周期\获取连接的超时时间 -
服务端层面:
max_connections服务端的最大连接数151\wait_timeout客户端连接超时时间\查询连接数 show processlist\终止连接kill connection-
优化I/O, 调整
innodb_buffer_pool_size的大小 , 是用于缓存数据和索引的区域 -
优化JOIN, 调整
join_buffer_size的大小 , 执行join时的数据缓冲区
-
-
- 因为后续项目可能会对声音和专辑这两个表进行分库分表, 所以, 我在对这两个表进行增删改查找时, 没有使用数据库的自增, 而是使用了mybatis-plus的雪花算法
- 在进行一些sql语句与条件查找时 , 我一般对不确定为字符串还是int类型的字段, 使用字符串来查找, 因为, 如果在数据库中这个字段为字符串 , 那它作为索引时, 不会失效, 即使它在数据库中为int类型, 发生了类型转换, 也是转换sql语句中的字符串转化为int类型, 并不影响索引的使用
- 尽量使连表查询, 少使用子查询(特列: 深分页)
==2、项目中的日志如何处理==
==3、如何和前端进行数据交互==
4、项目中遇到过哪些问题
-
使用
redis做缓存和分布式锁时- 死锁
- 误删
- 锁过期
- 数据库 和 缓存数据不一致
-
ThreadLocal导致内存泄漏 -
分布式事务问题: RabbitMq的 -
RabbitMq的三个问题- 消息的可靠性投递
- 消息的可靠性消费
- 消息的幂等性处理
-
@Tranctional 本地
事务失效-
异常被捕获
-
方法中调用
解决办法:
-
在类中循环依赖自己, 然后使用依赖对象调用, 在SpringBoot这样做,就会有循环依赖的问题, 解决办法:
allow-circular-references: true -
使用AopContext对象获取当前类的代理对象, 使用代理对象调用该方法
开启
@EnableAspectJAutoProxy(exposeProxy = true)暴露代理对象使用
AopContext.currentProxy()获取当前类的代理对象 -
使用Applicationcontext对象获取代理对象
ApplicationContext.getBean(MyClass.class)
-
-
-
多端
重复消费问题解决: 在做幂等性时, 不再使用随机字符串, 而是根据用户和商品的id生产token
原因: 在多端下, 由于是随机生成的幂等性的token, 不具有代表性, 当多端同一个用户购买同一件商品时, 并不能区分出来, 所以就导致了重复消费的问题,
-
相同文件不同名字文件的
重复上传问题解决: 在上传前通过MD5加密. 然后上传时, 使用MD作为minio对象的Object值, 每次上传之前,只有minio的
statObjectAPI判断桶中是否存在相同的对象.如果存在, 获取该对象返回, 如果不存在,直接上传
-
异步时,数据不完整解决: 使用join , Futruetask的get(); \ countdownLatch
-
异步时, 使用自定义线程池时, 使用了LinkedBlocingQueue,未设置大小,导致OOM异常 -
异步时,ThreadLocal中的数据获取不到 -
监听器SpringApplicationRunListener 时, 第一次获取不到Bean对象原因: 在第一次调用时, 是springcloud的nacos组件调用得到, 需要先判断是否存在
-
监听器SpringApplicationRunListenre未生效,
使用SPI机制注入
-
OpenFeign的请求头中数据丢失
原因: Openfeig 在生成新请求模板时, 没有负责原请求的请求头中的数据, 只对@RequestParam 和@RequestHeader 做了处理 ,
解决办法:
- 作为参数传递
- 使用@RequestHeader
- 使用RequestInterceptor: 在Open生成请求对象之前, 会调用RequestInterceptor的apply方法并传入新的请求模板, 我们可以通过实现该接口, 在重写的方法中,统一对远程调用的请求模板做处理, 此时, 就可以根据SpringMVC的RequestContextHolder对象获取到前端请求的请求头信息, 然后再将请求头设置进Openfein的请求模板中, 这样, OpenFeign在创建请求对象时, 请求头中就会有我们设置进去的数据了
-
Redisson导致大量线程被阻塞 \ 大量线程被死锁
- 在上锁时,不使用lock, 使用tryLock,
- 未正确使用Readisson造成 , 使用try-finally, 在finally块中释放锁, 确保锁能被正常是否和停止锁续期
-
Long, BigDecimal返回给前端时,精度丢失问题
解决流程: 在SpingMVC的配置文件中, 将他们的序列化器换为自定义的
5、项目中有没有遇到过内存泄漏和cpu被打满。
有:
- 内存泄漏: 使用ThreadLocal传递参数未移除
- CPU大满: 使用redis做缓存和分布式锁时, 在高并发下自旋造成的 \ 高并发下 大量消息多次重试 \
6、项目中有没有用到异步多线程以及遇到了什么问题。
-
异步时,数据不完整解决: 使用join , Futruetask的get(); \ countdownLatch
-
异步时, 使用自定义线程池时, 使用了LinkedBlocingQueue,未设置大小,导致OOM异常 -
异步时,ThreadLocal中的数据获取不到
7、项目中设计过哪些表,对表的设计如何考虑
在专辑详情模块中, 我主要设计了 album_info \ track_info \ 它们各自的统计表
- 对于album_info 和 track_info表设计的考虑,
- 名字, 采用小写 + 下划线
- 不使用外键, 保证表的独立性
- 考虑到未来可能会对这两个表进行分表操作,所以在对这两个表设计id时,并没有采用数据的自增, 而是使用了Mybatis的雪花算法.
- 对于这两个表的操作会经常进行连表查询,根据专辑的付费类型 进行查询, 对album_info的price_type建立索引
- 在专辑详情中, 展示声音列表时, 需要排序, 我单独设计了一个order字段, 并对这个字段建立了索引
- 对于既可以使用int型和string型的字段, 我一般设置为int型, 避免因为类型转换导致的索引失效问题
8、项目中用到过SpringCloud的哪些组件
nacos \ openfeign \ gateway \ Sentinel
9、谈谈你对SpringBoot的自动配置原理
-
核心注解SpringBootApplication
在这个核心注解下有三个注解
- SpringBootConfiguaration : 表明这是一个SpringBoot的配置类
- EnableAutoConfiguaration : 这是SpringBoot的自动配置的核心注解
- ComponentScan注解: 用于排除一些不需要的组件
-
SpringBootConfiguaration注解的原理
在SpringBoot项目中, 每个项目都会有直接或间接依赖于spring -boot-starter 包 ,在这个包中又依赖了spring -boot- autoconfiguaration这个包, 在这个包下
springboot已经编写好了142配置类, 它们全类名都存储在resources/META-INF/spring/org.springframeword.boot.autoconfig.AutoConfiguration.imports文件中,
但是这不代表这142个配置类都会被加载.
-
ConditionXXX,SpringBoot会根据配置类上的Condition注解,选择性的加载配置类, 例如ConditionOnClass, 只有在引入对应配置类所依赖的包时, 才会加载该配置类
ConditionOnProperties, 会根据配置文件中是否有对应的配置信息,来选择性的加载配置类
-
SpringBoot还会根据AutoConfigurationImportSelector来顺序性的加载配置文件
==10、SpringBoot和SpringCloud的配置文件加载顺序以及优先级==
优先级:
配置中心 > 环境变量> JVM系统属性 > 命令行 > bootstrap.properties > bootstrap.yml > applicatio.properties > application.yml > application-{profile}.properties >
applicatoin-{profile}.yml
加载顺序
bootstrap.properties > bootstarp.yml > 配置中心 > application.properties > application.yml > application-{profile}.properties > application-{profiles}.yml >
命令行> JVM配置 > 环境变量
简化:
加载顺序: bootstrap.properties > bootstrap.yml > 配置中心 > application.peoperties> application.yml > application-{profile}.bootstrap/yml
优先级: 配置中心 > bootstrap.properties > bootstrap.yml > application.properties > application.yml > application-{profiles}.bootstrap/yml
==11、jvm调优了解过哪些==
- 自定义堆大小
- -Xmx100m : 设置最大堆大小
- -Xms100m :设置初始堆大小
- 调整年轻代和老年代的大小
-XX:NewSize=1m和-XX:MaxNewSize=2m
- 设置虚拟机栈的大小
- -Xss256k
- 指定G1垃圾回收器
- -XX:+UseG1GC
12、运行时数据区的组成以及各自空间作用
-
- 非共享区域
这些区域是线程私有的,每个线程都有自己的独立副本。
1.1 虚拟机栈(JVM Stack)
每个线程在执行时都会创建一个虚拟机栈,栈中包含了每个方法调用的状态信息。每当一个方法被调用时,JVM 会为该方法分配一个栈帧(stack frame)。栈帧用于存储以下内容:
- 局部变量表:方法中的局部变量,包括基本数据类型(如
int、float等)和对象引用(例如对象的内存地址)。局部变量表的大小在编译时确定,因此非常高效。 - 动态链接:与方法相关的类的符号引用,允许在运行时进行方法调用的解析。
- 方法出口:记录方法执行完毕后返回的地址,用于方法执行完毕后跳转到正确的位置继续执行。
- 操作数栈:存储方法调用过程中的临时数据,比如计算结果,或者存储操作数。操作数栈的大小在编译时设定,在执行时动态变化。
每个线程拥有独立的虚拟机栈,且栈的深度由方法的调用深度决定。如果栈深度超过了 JVM 的栈大小限制,就会抛出
StackOverflowError。1.2 本地方法栈(Native Method Stack)
本地方法栈是专门为本地方法(Native Method)服务的区域。它与虚拟机栈类似,不同的是,本地方法栈是用来处理 Java 代码之外(通常是使用 C、C++ 等语言编写)的本地方法调用的。
本地方法栈和虚拟机栈一样,每个线程有一个独立的栈。它保存了 JNI(Java Native Interface)调用所需的本地方法信息。
1.3 PC 程序计数器(Program Counter Register)
每个线程都拥有一个独立的 PC 寄存器。程序计数器是一个指针,指向当前线程所执行的字节码指令的地址。因为 JVM 是基于栈的执行模型,程序计数器用于指示当前线程执行到哪条指令。
- 字节码指令:当线程执行的是 Java 方法时,PC 会指向当前方法的字节码地址。
- 本地方法:如果当前执行的是本地方法,PC 程序计数器值则为
0,表示本地方法的执行与 Java 代码无关。
PC 寄存器并不会影响方法栈的内容,但它帮助 JVM 定位当前线程正在执行的指令。
- 共享区域
这些区域是所有线程共享的,即所有线程访问相同的内存区域。
2.1 堆(Heap)
堆是 JVM 中最大的一块内存区域,几乎所有的对象实例都在堆中分配内存。堆的作用是存储对象的实例数据,并且也是垃圾回收器(GC)关注的重点区域。
- 对象分配:当我们通过
new关键字创建对象时,JVM 会在堆中为对象分配内存。堆中的内存是共享的,所有线程都可以访问。 - 垃圾回收:垃圾回收主要发生在堆中,通过标记清除、复制回收或 G1 GC 等方式回收不再使用的对象内存。
堆空间的大小可以通过 JVM 参数来控制,例如
-Xms设置初始堆大小,-Xmx设置最大堆大小。堆的大小影响垃圾回收的频率和性能。2.2 方法区(Method Area)(JDK8以前叫永久代) 都是元空间实现的一张方式
方法区存储的是类的相关信息、常量池、静态变量、即时编译(JIT)编译器的代码等。方法区是共享的,所有线程都可以访问。方法区是 JVM 内存的一个非常重要的区域,它包含以下内容:
- 类的字节码:方法区存储每个类的结构信息(如类的元数据),包括类的字段、方法、父类等。
- 运行时常量池:运行时常量池存储类加载过程中使用的常量,比如字符串字面量、数字常量等。常量池存储的是从类字节码文件中提取出来的常量信息。
- 静态变量和方法:静态变量属于类而不属于实例,保存在方法区中。
- JIT 编译后的代码:当 JVM 执行
java程序时,一些热点代码会被即时编译为机器码,这些编译后的机器码也存储在方法区中。
13、垃圾回收算法有哪些以及谈谈CMS垃圾回收器的了解
-
垃圾的标记算法:
引用计数法 \ 根可达性算法
-
垃圾回收算法:
- 标记清除法
- 复制算法
- 标记整理清除
- 分代整理: 就是前面两种算法的混合使用, 年轻代使用:复制算法 . 老年代使用标识整理清除算法
-
==CMS垃圾回收器 (并行)==
CMS 垃圾回收器主要由两个阶段组成:标记(Mark)阶段和清理(Sweep)阶段。与传统的串行垃圾回收器不同,CMS 旨在通过并发工作来缩短停顿时间,具体过程如下:
-
初始标记(Initial Mark):
- 这个阶段会标记所有的 “GC Root”(即活动对象)。它需要停止所有应用线程,因此会有较短的停顿。
-
并发标记(Concurrent Mark):
- 在此阶段,CMS 垃圾回收器会并发地扫描整个堆中的对象,标记出哪些对象是可达的。这个过程是并行的,不需要暂停应用线程。
-
并发预清理(Concurrent Preclean):
- 在并发标记阶段后,CMS 会进行一个预清理过程,清理一些由于并发标记产生的垃圾对象。
-
重新标记(Remark):
- 这个阶段需要暂停应用线程,标记在并发标记期间发生了变化的对象。由于这是短暂的,停顿时间会较短。
-
并发清理(Concurrent Sweep):
- 这是并发清理阶段,会清除那些未被标记的对象。与传统的垃圾回收器不同,CMS 在这个阶段仍然不需要暂停应用线程。
-
并发整理(Concurrent Compact)(可选):
- 对于一些较大的对象和碎片,CMS 还会进行整理阶段,避免内存碎片化。这个阶段有时会被禁用,以减少停顿时间。
优点
- 减少停顿时间:与其他垃圾回收器相比,CMS 的最大优势是可以减少停顿时间,尤其在应用需要高响应时。
- 并发标记和清理:通过并发的标记和清理工作,CMS 可以在大部分时间里继续应用线程的执行,避免长时间的停顿。
缺点
- 内存碎片:由于 CMS 不会进行压缩,可能导致堆内存中的碎片化问题。尤其在长时间运行后,这可能导致 Full GC 被触发。
- 较高的 CPU 占用:CMS 在执行并发标记时会消耗较多的 CPU 资源。
- 停顿时间不可预测:虽然 CMS 可以减少停顿时间,但某些情况下仍然会出现较长的停顿,尤其是在 Full GC 时。
配置
在启动 Java 程序时,可以通过以下参数来启用 CMS 垃圾回收器:
1
-XX:+UseConcMarkSweepGC
另外,可以通过一些选项来调整垃圾回收行为,如:
-XX:CMSInitiatingOccupancyFraction=<percentage>:设置触发 CMS 垃圾回收的堆内存占用阈值,默认值为 68%。-XX:+UseCMSInitiatingOccupancyOnly:只使用上述阈值触发垃圾回收。-XX:+DisableExplicitGC:禁用显式的垃圾回收调用,以避免对 CMS 垃圾回收产生干扰。
总结
CMS 垃圾回收器是专为降低垃圾回收停顿时间设计的。它通过并发标记和清理来减少停顿,适用于对延迟敏感的应用程序。然而,CMS 在内存碎片方面的缺点和高 CPU 占用使得它并不适用于所有场景,特别是在长期运行的系统中。在 Java 9 之后,JVM 推出了 G1 垃圾回收器,它被认为是对 CMS 的替代,提供了更好的性能和更稳定的响应时间。
-
14、创建线程池的方式有哪些execute和submit的区别
-
创建线程池的方法:
-
使用工具类Excutros.Executors.newCachedThreadPool(); 默认最大线程池的数量为Intege的最大值
-
使用工具类的Executors.newFixedThreadPool(100); 可以手动指定最大线程池
-
newSingleThreadExecutor(), 创建只有一个线程的线程池, 提交的任务会顺序的执行
-
自定义线程池,new ThreadPoolExecutor();
-
-
execute和submit的区别:
Executor接口提供了两种提交任务的方式:execute()和submit()。它们的区别主要体现在任务的类型、返回值和异常处理上。-
execute():
参数:
execute()方法接收一个实现了Runnable接口的任务。Runnable任务是没有返回值的任务,它的run()方法没有返回结果。返回值:
execute()方法没有返回值,它只能提交任务并执行,不关心任务执行的结果。 -
submit()
参数:
submit()方法既可以接收实现了Runnable接口的任务,也可以接收实现了Callable接口的任务。Runnable是不返回结果的任务。Callable是有返回值的任务,Callable.call()方法可以返回一个结果。
返回值:
- 如果传递的是
Callable类型任务,submit()方法返回的Future对象的get()方法会返回任务的结果。
-
15、如何实现两个线程通信,编写一段代码
-
模板:
1
2
3
4
5
61. 定义资源类、方法
2. 资源类方法:
判断(符合条件就运行,不符合就等待)
执行
唤醒
3. 通过多线程操作共享资源 -
实现代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85package online.zorange.test;
public class ThreadCommunicationTemplate {
public static void main(String[] args) {
SharedResource resource = new SharedResource();
// 启动多个消费者线程
for (int i = 0; i < 3; i++) {
new Thread(new Consumer(resource), "消费者-" + i).start();
}
// 启动一个生产者线程
new Thread(new Producer(resource), "生产者").start();
}
}
class SharedResource {
private int resource = 0; // 共享资源
private final int MAX_RESOURCE = 5; // 最大资源容量
// 获取资源(消费者)
synchronized void consume() throws InterruptedException {
while (resource <= 0) {
System.out.println(Thread.currentThread().getName() + " 等待资源...");
wait(); // 等待资源
}
resource--;
System.out.println(Thread.currentThread().getName() + " 消费资源,当前资源: " + resource);
notifyAll(); // 通知其他线程
}
// 生产资源(生产者)
synchronized void produce() throws InterruptedException {
while (resource >= MAX_RESOURCE) {
System.out.println(Thread.currentThread().getName() + " 资源已满,等待消费...");
wait(); // 等待消费
}
resource++;
System.out.println(Thread.currentThread().getName() + " 生产资源,当前资源: " + resource);
notifyAll(); // 通知其他线程
}
}
// 生产者
class Producer implements Runnable {
private final SharedResource resource;
public Producer(SharedResource resource) {
this.resource = resource;
}
public void run() {
try {
while (true) {
resource.produce(); // 生产资源
Thread.sleep(500); // 模拟生产过程的延迟
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
}
// 消费者
class Consumer implements Runnable {
private final SharedResource resource;
public Consumer(SharedResource resource) {
this.resource = resource;
}
public void run() {
try {
while (true) {
resource.consume(); // 消费资源
Thread.sleep(1000); // 模拟消费过程的延迟
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
}
16、volatile的作用是什么,如何理解JMM内存模型
-
valatile的作用 :
-
保证可见性
保证在多线程下, 变量的改变能被及时监听到
-
不能保证原子性
-
保证顺序性 : JVM在运行方法时, 会在不允许运行结果的前提下, 改变代码执行的顺序 ,但是在多线程下, 如果改变代码执行顺序,可能会影响其他线程的结果
-
-
==JVM内存模型==
VM 内存模型
JVM 内存模型主要由 主内存 和 工作内存 两部分组成。每个线程都有自己独立的工作内存,而共享的主内存则由所有线程共享。
- 主内存(Main Memory)
- 定义:主内存是所有线程共享的内存区域。所有线程都通过主内存来交换数据。
- 内容:主内存中存储了所有实例变量、静态变量、以及常量。
- 作用:主内存确保所有线程对共享变量的修改能够被其他线程看到,是线程间通信的基础。
- 工作内存(Working Memory)
- 定义:每个线程都有自己的工作内存。工作内存保存着线程正在执行的变量的副本,也就是线程私有的内存。
- 内容:线程在工作内存中存储了从主内存中读取的数据副本(通常是栈帧)。
- 作用:工作内存不直接操作主内存中的数据,而是从主内存复制数据到工作内存中,线程对数据的操作都是在自己的工作内存中进行的,修改数据后会同步到主内存。
- 内存交互与共享机制
- 原子性:JVM 内存模型中有一套原子性操作机制,确保基本数据类型操作(如加、减)能够在多个线程间正确同步。
- 可见性:为了保证线程之间的可见性,JVM 提供了各种同步机制(如
volatile关键字),保证线程对共享变量的更新能够立即被其他线程看到。 - 有序性:工作内存中的指令执行顺序和内存访问顺序在多线程环境下可能并不相同,JVM 提供了有序性保证,通过
synchronized等关键字来控制。
- 内存屏障与锁机制
- 内存屏障(Memory Barrier):它是保证线程对共享变量访问顺序的机制。通过在关键位置插入内存屏障,JVM 可以确保线程在执行某些操作时,不会将操作乱序执行。
- 锁机制:
synchronized、ReentrantLock等锁机制用于确保线程对共享数据的互斥访问。
17、MySQL事务的隔离级别以及不同隔离级别的问题
-
隔离级别
-
读未提交
问题: 脏读
-
读已提交
问题: 不可重复读
-
可重复读
问题: 幻读
-
串行化
-
18、MySQL索引失效的场景有哪些
-
索引失效的场景。(结论)
- 函数 和 计算
- is null \ is not null
- != <>
- 类型转换(可能)
- Like以%开头
- 定义索引和使用索引的顺序不同
- 排序中无过滤
- 范围查询右边的索引失效
- 排序时一个升序(ASE) 一个降序(DESC)
-
失效的原因(原理)
-
如何理解索引。(理解)
-
根据物理结构来区分:聚簇索引 \ 非聚簇索引 (叶子节点是否存储完整数据区分)
索引是一种数据结构, 在mysql中,索引的数据结构为B+数, B+数的底层,
-
四种类型的数据:
0 代表: 存储的是索引值和完整数据
1代表: 目录项
2 代表: 当前页中的最小值
3 代表: 当前页中的最大值
-
以页为单位, 每页大小为16kb
-
分为叶子节点和非叶子节点,
-
在非叶子节点中:
- 存储的是目录项 和 当前目录下的最大值和最小值
- 每个目录项中又存储着索引值和当前目录项下的最小值
-
在叶子节点中
- 当前页中的最大索引值和最小索引值
- 数据项: 存储着索引值和完整的数据
-
页中每个数据以单链表存储
-
在页于页之间则是通过双链表形式存储.
-
19、==MySQL事务的四大特性如何实现==
MySQL 事务的四大特性通常被称为 ACID 特性,它们包括 原子性 (Atomicity)、一致性 (Consistency)、隔离性 (Isolation) 和 持久性 (Durability)。每个特性都通过不同的机制和设计来保证事务在数据库操作中的正确性与可靠性。
原子性:通过日志和回滚机制确保事务的全部操作要么都成功,要么都失败。( redo log)
一致性:通过数据的完整性约束、验证以及事务隔离级别来确保数据库保持一致的状态。
隔离性:通过锁机制和事务隔离级别确保事务并发时互不干扰,避免数据的不一致。
持久性:通过事务日志和刷新机制确保已提交的事务数据不会丢失,即使系统崩溃也能恢复。
- 原子性 (Atomicity)
- 定义:原子性保证事务内的所有操作要么全部成功,要么全部失败。如果事务中的某个操作失败,整个事务会回滚,恢复到事务开始前的状态。事务被视为一个不可分割的单元。
- 实现方式
- 日志机制:MySQL 使用 事务日志(如 InnoDB 的 redo log)来保证原子性。事务操作的每一步都会被记录到日志中。如果事务失败或需要回滚,MySQL 会根据日志恢复到事务开始之前的状态。
- 回滚机制:通过回滚(rollback)命令或自动回滚来撤销未提交的事务操作,确保数据库的原子性。
- 写前日志(Write-Ahead Logging, WAL):MySQL 中的 InnoDB 存储引擎采用 WAL 协议,确保所有修改数据的操作在被提交之前会先写入日志,从而保证事务的原子性。
- 一致性 (Consistency)
- 定义:一致性保证事务的执行会使数据库从一个一致的状态转换到另一个一致的状态。在事务开始前和结束后,数据库中的数据必须满足所有的约束、触发器等。
- 实现方式
- 外键约束、CHECK 约束、唯一性约束等可以保证数据的一致性。事务的执行不会违反这些约束,即使在事务的处理中,所有的数据变化也必须保证这些约束不被破坏。
- 数据验证:MySQL 会在事务的开始和提交前验证数据的完整性。例如,事务中的 INSERT、UPDATE 和 DELETE 操作会确保数据不会违反数据库的完整性约束。
- 事务隔离级别:通过设置事务的隔离级别(如 READ COMMITTED、REPEATABLE READ 等)来避免脏读、不可重复读等问题,从而保证事务的一致性。
- 隔离性 (Isolation)
-
定义:隔离性保证多个事务并发执行时,每个事务的执行不会受到其他事务的干扰。每个事务看起来都是在独立执行的,其他事务无法看到当前事务的中间结果。
-
实现方式
-
事务隔离级别
:MySQL 提供了多种隔离级别来控制事务之间的可见性,主要包括:
- READ UNCOMMITTED:允许一个事务读取另一个事务未提交的数据(脏读)。
- READ COMMITTED:事务只能读取已提交事务的数据(避免脏读,但仍可能存在不可重复读)。
- REPEATABLE READ:保证事务中多次读取同一数据的结果是一样的(避免脏读和不可重复读),InnoDB 使用该隔离级别。
- SERIALIZABLE:最严格的隔离级别,强制事务串行执行,避免脏读、不可重复读和幻读。
-
锁机制
:MySQL 使用行锁和表锁来控制事务的隔离性。行锁可以防止其他事务对正在操作的记录进行修改,从而保证事务隔离性。
- 行级锁(InnoDB的行锁):可以对数据行加锁,允许并发操作不同数据行。
- 表级锁(MyISAM的表锁):对整个表加锁,适用于对表操作的事务。
-
- 持久性 (Durability)
- 定义:持久性保证一旦事务提交,其对数据库的修改是永久性的,即使系统崩溃也不会丢失数据。
- 实现方式
- 提交日志:MySQL 使用事务日志(如 redo log)来保证事务的持久性。即使数据库发生崩溃,已提交的事务也能通过日志恢复。
- 刷新日志:MySQL 会确保在事务提交时,所有操作记录都会被刷新到磁盘中的日志文件,确保数据持久化。
- 双写缓冲(Doublewrite Buffer):InnoDB 使用双写缓冲技术,在写入数据页时,先写入缓冲区,再写入磁盘,确保即使在崩溃的情况下也不会丢失数据。
==20、谈谈你对MvCC的理解==
MVCC(Multi-Version Concurrency Control,多版本并发控制)是一种数据库并发控制机制,用于提高数据库在多用户环境中的性能,避免事务间的冲突,并确保数据库的一致性和隔离性。MVCC 通过为每个事务提供数据的不同版本来避免加锁,从而实现更高效的并发访问。
MVCC 的基本原理:
- 版本控制
每条数据在数据库中都有多个版本,每个版本包含数据的一个状态。每当数据发生更新时,数据库不会直接修改现有数据,而是创建一个新的数据版本,并给该版本分配一个唯一的时间戳或版本号。 - 事务视图
每个事务在开始时,都会看到数据库的一个一致的快照视图,而这个视图是由所有已提交的事务的数据版本构成的。不同事务可以看到不同的数据版本,避免了因锁竞争而导致的性能瓶颈。 - 事务隔离
MVCC 的实现使得数据库在不同事务之间能够独立读取数据,并且每个事务只能看到它开始时的快照数据,不会被其他事务的修改影响。因此,MVCC 能够实现类似于 可重复读(Repeatable Read)和 读已提交(Read Committed)的事务隔离级别。 - 删除和回收
数据的旧版本不会立即删除,而是标记为已过时。当一个事务提交时,它会把对数据的修改写入一个新的版本,旧版本将被标记为无效。数据库会定期回收这些无效的数据版本,以释放存储空间。
MVCC 的工作机制:
- 读取数据: 当事务读取数据时,它会看到在该事务开始之前已经提交的所有数据版本,确保读取到的是一个一致的状态(快照)。
- 更新数据: 当事务修改数据时,数据库会为该数据创建一个新的版本,新的版本只对当前事务可见。其他事务不会看到该修改,直到该事务提交。
- 删除数据: 当事务删除数据时,数据库会将数据标记为删除,但实际的物理删除操作会延迟到数据不再被任何事务访问时才进行。
- 提交和回滚: 当事务提交时,它所修改的数据版本变成可见;当事务回滚时,所有它创建的版本都会被丢弃。
MVCC 的优势:
- 提高并发性:因为读操作不需要加锁,多个事务可以并发读取数据,提高系统的吞吐量。
- 减少死锁:由于读操作不涉及锁,事务间的冲突较少,从而减少了死锁的发生。
- 提高性能:事务对数据的修改不会立即影响到其他事务,因此避免了读写锁的争用,减少了性能瓶颈。
- 事务隔离性:MVCC 能保证事务的一致性和隔离性,多个事务之间互不干扰,能够保证“可重复读”或更高的隔离级别。
MVCC 的缺点:
- 空间消耗:每次更新数据都会产生新的数据版本,因此需要额外的存储空间来维护数据的多个版本。
- 版本回收复杂性:由于旧版本的数据不能立即删除,数据库需要额外的机制来定期清理这些过期版本。
- 复杂的并发控制:尽管 MVCC 允许多个事务并发执行,但在某些情况下(比如事务之间的写冲突)需要更复杂的冲突检测和解决机制。
补充:编写一个生成者消息者模型。【两个线程+一个阻塞队列】
1 | package online.zorange.test; |
复习:先原理在推结论
面试:先说结论在解释原理。
