后端Java面试题(java基础、多线程、集合框架、数据库Mysql)
前言
在毕业前就一直在看工作—刷面试题—面试—做总结,反反复复去做这些事情,由于记性不太好,所以每次都会记住那么一两个问题,回到家自己去查询做笔记,这里是一部分给大家分享出来,仅供参考,希望可以帮助到有需要的小伙伴。
java基础
1.面向对象的三大特征
封装、继承、多态
-
封装就是把对象的属性和行为封装到一起,隐藏对象对实现的内部细节,只需要暴露必要的接口让外部访问
-
继承就是允许一个类继承另一个类的属性和方法
-
多态就是一个方法可以在不同的对象上表现出不同的行为
多态的底层实现是动态绑定,即在运行时才把方法调用与方法实现关联起来。
一种是编译时多态,被称为静态分派,比如方法的重载;
一种是运行时多态,被称为动态分派,比如方法的重写和接口的实现。
2.静态变量和成员变量的区别?
(1)成员变量存放于堆内存中。静态变量存在于方法区中。
(2)成员变量与对象共存亡,随着对象创建而存在,随着对象回收而释放。静态变量与类共存亡,随着类的加载而存在,随着类的消失而消失。
(3)成员变量所属于对象,所以也称为实例变量。静态变量所属于类,所以也称为类变量。
(4)成员变量只能被对象所调用。静态变量可以被对象调用,也可以被类名调用。
3.是否可以从一个静态方法内部发出对非静态方法的调用?
(1)没有显示创建对象实例:不可以发起调用,非静态方法只能被对象所调用,静态方法可以通过对象调用,也可以通过类名调用,所以静态方法被调用时,可能还没有创建任何实例对象。因此通过静态方法内部发出对非静态方法的调用,此时可能无法知道非静态方法属于哪个对象。
(2)显示创建对象实例:可以发起调用,在静态方法中显示的创建对象实例,则可以正常的调用
4.==和equals的区别是什么?
==
对于基本类型的数据,比较的是数据的值是否相等。
对于引用类型的数据,比较的是对象的内存地址是否相等。
equals
通常比较两个对象的值是否相等。
5.深拷贝和浅拷贝区别是什么?
深拷贝:对于引用数据类型,开辟新的内存空间,在新的内存空间里复制一个一模一样的对象,新老对象不共享内存,修改其中一个对象的值,不会影响另一个对象。
浅拷贝:对于引用数据类型,只是复制了对象的内存地址,新旧对象指向同一个内存地址,修改其中一个,另一个随之改变。
6.String、StringBuilder、StringBuffer的区别?
String:String的值创建后不可被修改。任何对String的修改都会引发新的String对象发生。频繁的修改字符串,会出现大量的临时对象,影响性能。
StringBuffer:跟String类似,但是值可以被修改,使用synchronized来保证线程安全。
StringBuilder:StringBuilder的非线程安全版本,没有使用synchronized,具有更高的性能,推荐优先
7.重载和重写的区别?
重载:一个类中有多个同名的方法,但是具有不同的参数列表(参数类型不同、参数个数不同或者二者都不同)。
重写:发生在子类与父类之间,子类对父类的方法进行重写,参数都不能改变,返回值类型可以不相同,但是必须是父类返回值的派生类。
8.抽象类和接口有什么区别?
-
抽象类只能单继承,接口可以多实现。
-
抽象类可以有构造方法,接口中不能有构造方法。
-
抽象类中可以有成员变量,接口中没有成员变量,只能有常量(默认就是publicstaticfinal)。
-
抽象类中可以包含非抽象的方法,在Java 7之前接口中的所有方法都是抽象的,在Java 8之后,接口支持非抽象方法:default 方法、静态方法等。Java 9支持私有方法、私有静态方法。
-
抽象类中的方法类型可以是任意修饰符,Java8之前接口中的方法只能是public类型,Java9支持
private类型。
9.Java中的final关键字有哪些用法?
(1)修饰类:该类不能再派生出新的子类,不能作为父类被继承。因此,一个类不能同时被声明为abstract和final.
(2)修饰方法:该方法不能被子类重写。
(3)修饰变量:该变量必须在声明时给定初值,而在以后只能读取,不可修改。如果变量是对象,则指的是引用不可修改,但是对象的属性还是可以修改的。
10.类的记载过程
-
加载: 先找到字节码⽂件(.class⽂件),加载到JVM内存中的⽅法区⾥。 然后在内存中的体
现就是⼀个Class对象。
-
验证: 验证加载到内存⾥的.class⽂件是否被篡改过,确认没有安全问题,以及符合JVM规
范。
-
准备: 为类中的⼀些变量分配内存空间,并且设置⼀下默认值。
-
解析: 将常量池内的符号引⽤转为直接引⽤。
符号引⽤:符号引⽤是⼀种泛指,com/mashibing/A-findAll()void
直接引⽤:直接指向的内库的具体位置,直接就是内存偏移量。后⾯调⽤会更快。
-
初始化:对所有静态变量复制,执⾏静态代码块,初始化好⽗类
前⾯⾛完,到这,这个.class就可以在Java程序中使⽤了,new⼀个对象,类名.静态⽅法都可以
了
11.ava中为什么要有包装类
实现对象化操作
首先就是Java是宣传完全面向对象的,但是呢基本数据类型不包含对象性质。
包装类呢就是将基本数据类型封装为对象。使得基本数据类型可以像对象一样使用。
-
以
Integer包装类为例,它封装了int类型的数据。如果有一个方法需要接收一个Object类型的参数,那么可以直接传递一个Integer对象,而不能直接传递一个int类型的变量。
支持泛型
-
泛型是Java中用于实现类型安全的重要机制。在Java的泛型中,只能使用对象类型,而不能使用基本数据类型。包装类的存在使得基本数据类型可以被包装成对象,从而可以作为泛型参数使用。
-
例如,
ArrayList是一个泛型集合类,它只能存储对象类型的元素。如果需要在ArrayList中存储整数,就需要使用Integer包装类。例如:
ArrayList<Integer> list = new ArrayList<>();
list.add(1); // 自动装箱
提供额外的方法和功能
-
包装类为基本数据类型提供了许多额外的方法和功能。这些方法可以用于进行类型转换、比较、解析等操作。
-
例如,
Integer类提供了parseInt方法,可以将字符串解析为整数:
int num = Integer.parseInt("123");
-
还有一些方法用于比较、转换等,比如
compareTo、toString等。
实现自动装箱和拆箱
ava从JDK 1.5开始引入了自动装箱(Autoboxing)和自动拆箱(Unboxing)机制。自动装箱是指将基本数据类型自动转换为对应的包装类对象,而自动拆箱则是将包装类对象自动转换为对应的基本数据类型。
Integer i = 10; // 自动装箱
int j = i; // 自动拆箱
多线程

一、创建多线程的三种方式
1、继承Thread类,重写run方法。
public class productThread extends Thread{
public productThread() {
}
public productThread(String name) {
super(name);
}
@Override
public void run() {
for (int i = 0; i < 100; i++) {
System.out.println(getName()+"正在执行Thread");
// 礼让线程
Thread.yield();
}
}
}
public class ThreadDemo {
public static void main(String[] args) {
// 第一种创建线程方式
// 继承Thread类
// 利用构造函数命名线程Alt+Insert
productThread productThread = new productThread("productThread一");
productThread productThread2 = new productThread("productThread二");
// 给线程命名
/*productThread.setName("productThread一");
productThread2.setName("productThread二");*/
// 启动线程
productThread.start();
productThread2.start();
}
}
2、实现Runnable接口,重写run方法。
public class ProductRunnable implements Runnable{
@Override
public void run() {
for (int i = 0; i < 100; i++) {
System.out.println(Thread.currentThread().getName()+"正在实现Runnable接口");
}
}
}
public static void main(String[] args) throws InterruptedException {
// 第二种创建线程方式
// 实现Runnable接口
ProductRunnable p1 = new ProductRunnable();
// 创建线程对象
Thread t1 = new Thread(p1,"productThread一");
Thread t2 = new Thread(p1,"productThread二");
// 设置线程优先级
t1.setPriority(10);
t2.setPriority(1);
// 给线程命名
// t1.setName();
// t2.setName();
// 启动线程
t1.start();
// 抢夺线程/插入线程
t1.join();
t2.start();
// 获取当前线程的对象
/* Thread t1 = Thread.currentThread();
System.out.println(t1.getName());*/
}
}
3、实现Callable接口和Future接口,重写call方法。
public class ProductCallable implements Callable<Integer> {
//首先实现Callable接口
@Override
public Integer call() throws Exception {
int sum = 0;
for (int i = 0; i <= 100; i++) {
sum+=i;
}
return sum;
}
}
public class ThreadDemo3 {
public static void main(String[] args) throws ExecutionException, InterruptedException {
// 第三种创建线程方式
// 实现Callable接口
// 创建线程对象要执行的任务
ProductCallable p1 = new ProductCallable();
// 创建FutureTask对象管理线程运行结果
FutureTask<Integer> ft1 = new FutureTask<>(p1);
// 创建线程对象
Thread t1 = new Thread(ft1);
// 启动线程
t1.start();
Integer i = ft1.get();
System.out.println(i);
}
}
二、Java实现线程同步的方式
1. synchronized关键字
synchronized 是Java中最基本的线程同步机制,它可以用于方法或代码块,确保同一时刻只有一个线程可以执行特定的代码段。
public class UserThread extends Thread{
/*
* 多线程售票100张
* 每次售票显示售票的线程名称和正在售票的票数
* */
// 共享资源100张票
static int a = 0;
@Override
public void run() {
while ( true){
// 同步代码块
synchronized (UserThread.class){
if(a<100){
a++;
try {
Thread.sleep(100);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println(getName()+"正在售票"+a);
}else {
break;
}
}
}
}
}
synchronized 的优点是简单易用,缺点是不够灵活,例如无法设置超时时间等。
2. ReentrantLock
ReentrantLock 是java.util.concurrent.locks包中的一个类,它提供了比synchronized更灵活的锁操作,例如尝试非阻塞地获取锁、尝试获取锁并能被中断、可定时地获取锁等
例子:同样是售票的多线程使用look,unlook
public class UserRunnable implements Runnable{
int ticket = 0;
// Lock锁
Lock lock = new ReentrantLock();
@Override
public void run() {
while( true) {//1//2//3
// 同步方法
// 开启锁
lock.lock();
try {
if (ticket == 100) {
break;
} else {
Thread.sleep(10);
ticket++;
System.out.println(Thread.currentThread().getName() + "正在售票" + ticket);
}
} catch (InterruptedException e) {
throw new RuntimeException(e);
} finally {
// 释放锁
// finally保证每次释放锁
lock.unlock();
}
}
}
}
3. volatile关键字
虽然volatile本身不能直接实现线程同步,但它可以保证变量的可见性和禁止指令重排序。它常用于确保线程之间共享变量的值能够及时更新。
public class Counter {
private volatile int count = 0;
public void increment() {
count++;
}
public int getCount() {
return count;
}
}
需要注意的是,volatile不能保证操作的原子性。例如,count++操作实际上包括读取、修改和写入三个步骤,volatile无法保证这三个步骤的原子性。因此,volatile通常用于简单的变量读写操作,而不是复杂的同步场景
4.原子类(Atomic Classes)
Java提供了java.util.concurrent.atomic包中的原子类,例如AtomicInteger、AtomicLong、AtomicReference等,这些类通过硬件级别的原子操作来保证线程安全。
import java.util.concurrent.atomic.AtomicInteger;
public class Counter {
private AtomicInteger count = new AtomicInteger(0);
public void increment() {
count.incrementAndGet();
}
public int getCount() {
return count.get();
}
}
原子类的优点是使用简单且性能较高,它们通过compare-and-swap(CAS)机制来实现线程安全,避免了锁的开销。
三、线程池
1.没有上限的线程池和有上限的线程池
* public static ExecutorService newCachedThreadPool()创建一个没有上限的线程池
* public static ExecutorService newFixedThreadPool(int nThreads)创建有上限的线程
public class MyThreadPoolDemo {
public static void main(String[] args) {
// 1、创建线程池
ExecutorService pool1 = Executors.newCachedThreadPool();
// 2、提交任务
pool1.submit(new MyRunnable());
pool1.submit(new MyRunnable());
pool1.submit(new MyRunnable());
pool1.submit(new MyRunnable());
pool1.submit(new MyRunnable());
// 3、销毁线程池
pool1.shutdown();
}
2.自定义线程池
-
临时线程什么时候创建:当核心线程已满,而且阻塞队列也已经满了。这时候后面还有任务要处理,就会创建临时线程来处理后面的线程
-
什么时候会触发拒绝策略:
当核心线程、阻塞队列和临时线程都已经满了的情况下,会触发拒绝策略。
拒绝策略的四种:
-
默认丢弃并抛出异常(ThreadPoolExecutor.AbortPolicy)
-
直接丢弃,不抛出异常(不推荐)(ThreadPoolExecutor.DiscardPolicy)
-
抛弃队伍中等待最近的任务,然后把当前任务加入到队列(ThreadPoolExecutor.DiscardoldestPolicy)
-
调用任务的run()方法绕过线程池直接执行(ThreadPoolExecutor.CallerRunsPolicy)
public class MyThreadPoolDemo2 {
public static void main(String[] args) {
ThreadPoolExecutor pool = new ThreadPoolExecutor(
3,//核心线程
6,//最大线程
60,//线程空闲时间
TimeUnit.SECONDS,//时间单位
new ArrayBlockingQueue<>(3),//阻塞队列
Executors.defaultThreadFactory() ,//线程工厂
new ThreadPoolExecutor.DiscardOldestPolicy()//拒绝策略
);
pool.submit(new MyRunnable());
pool.submit(new MyRunnable());
pool.submit(new MyRunnable());
pool.shutdown();
}
}
四、阻塞队列
ArrayBlockingQueue
以厨师做面条和食客吃面条为例,厨师做一碗放到队列里面,食客检查队列里面是否有面条,有就直接吃。
这个是一个死循环,没有判断最多吃多少碗跳出循环。
public class ThreadDemo {
public static void main(String[] args) {
// 创建阻塞队列指定队列大小
ArrayBlockingQueue<String> arrayBlockingQueue = new ArrayBlockingQueue<>(1);
Cook cook = new Cook(arrayBlockingQueue);
Foodie foodie = new Foodie(arrayBlockingQueue);
cook.start();
foodie.start();
}
}
食客
public class Foodie extends Thread{
// 使用阻塞队列
ArrayBlockingQueue<String> queue;
public Foodie(ArrayBlockingQueue<String> queue){
this.queue = queue;
}
@Override
public void run() {
while ( true){
try {
// 从队列里面去出一条数据
String take = queue.take();
System.out.println(take);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
}
厨师
public class Cook extends Thread{
// 使用阻塞队列
ArrayBlockingQueue<String> queue;
public Cook(ArrayBlockingQueue<String> queue){
this.queue = queue;
}
@Override
public void run() {
while ( true){
try {
// 往队列里面添加一条数据
queue.put("面条");
System.out.println("厨师放了一碗面条");
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
}
集合框架

1.集合和数组的区别
-
数组长度固定并且无法保存映射关系的数据。集合类可以用来保存数量不确定并且具有映射关系的数据。
-
数组可以保存基本类型的数据,也可以是引用类型的数据;而集合只能保存对象。
2.ArraList和LinkeList的区别
-
ArraList底层是基于动态数组实现的,LinkeList底层是基于链表实现的。
-
对于查询时:ArraList只需要根据index索引直接定位到数据,效率快。而LinkeList需要从头节点或从尾节点,按照顺序进行遍历,直到需要的数据。效率慢。
-
对于随机插入和删除:ArraList每删除或者增加一个数据,需要移动目标节点后面的所有节点。而LinkeList只需要修改目标节点的前后节点即可。因此效率上LinkeList优于ArraList。
-
对于顺序插入和删除:由于ArrayList不需要移动节点,因此在效率上比LinkedList 更好。
3、ArrayList和Vector的区别?
Vector和ArrayList几乎一致,唯一的区别是Vector在方法上使用了synchronized来保证线程安全,因此在性能上ArrayList具有更好的表现。
4.HashMap
-
底层是由“数组+链表+红黑树”组成。
-
默认初始容量是16。HashMap的容量必须是2的N次方,HashMap会根据传入的容量计算一个大于等于该容量的最小的2的N次方,例如传9,容量为16。
-
对于插入,默认情况下是使用链表节点。当同一个索引位置的节点在新增后超过8个(阈值8),如果此时数组长度大于等于64,则会触发链表节点转红黑树节点,而如果数组长度小于64,则不会触发链表转红黑树,而是会进行扩容,因为此时的数据量还比较小。
-
对于移除,当同一个索引位置的节点在移除后达到6个,并且该索引位置的节点为红黑树节点,会触发红黑树节点转链表节点
5.HashMap的扩容流程
(1)当HashMap中的元素数量达到容量乘以加载因子时,就会触发扩容操作。加载因子默认为0.75可以通过构造方法设置。
(2)当需要扩容时,HashMap 会创建一个新的数组,其大小为原数组的两倍。
(3)对于原数组中的每个非空位置(即存储了键值对的位置),会重新计算它们在新数组中的位置。
重新计算的方法是通过对原位置的哈希值取模新数组长度
(4)对于每个原数组的位置,如果它的链表长度小于等于阈值(默认为8),则直接将链表中的键值对按照顺序放入新数组的相应位置。如果链表长度超过阈值,会将该链表转化为红黑树,然后再将红黑的节点按照顺序放入新数组的相应位置。
(5)当新数组中的所有元素都被放入新的位置后,新数组取代了原来的数组,成为新的存储结构。(6)扩容操作完成后,原数组会被垃圾回收。
6.HashMap的插入流程
(1)首先,通过键的hashcode()方法计算出键的哈希值。哈希值是一个整数,用于确定该键值对在哈希表中的位置。
(2)确定存储位置。使用哈希值和哈希表的容量(数组的长度)来计算出该键值对在数组中的索引位置。通常会使用(hash &(capacity-1))的位运算来计算位置,其中hash 是键的哈希值capacity 是数组的长度。
(3)处理哈希冲突。如果发生了哈希冲突(即不同的键计算出了相同的位置),则会使用链表或红黑权来解决冲突。在JDK8及之前的版本中,使用链表来存储具有相同哈希值的键值对,称为链地址法。从JDK 8开始,当链表的长度超过一定阈值(默认为8),链表会转化为红黑树,以提高查找效率,
(4)插入键值对。如果该位置没有元素,直接将键值对插入到该位置。如果该位置已经存在元素,会据键的哈希值进行比较:如果哈希值相等且键相等(调用 equals()方法返回true),则更新对应的值。否则,将该键值对插入到链表或红黑树的未尾?
7.HashMap为什么使用红黑树

AVL树:
一般用平衡因子判断是否平衡并通过旋转来实现平衡,左右子树树高不超过1,和红黑树相比,AVL 树是高度平衡的二叉树,平衡条件必须满足(所有节点的左右子树高度差不超过1)。不管我们是执行插入还是删除操作,只要不满足上面的条件,就要通过旋转来保持平衡而的由于旋转比较耗时,由此我们可以知道 AVL 树适合用于插入与删除次数比较少,但查找多的情况
在计算机科学中,AVL树是最先发明的自平衡二叉查找树。在 AVL 树中任何节点的两个子树的高度最大差别为1,所以它也被称为高度平衡树。增加和删除可能需要通过一次或多次树旋转来重新平衡这个树
红黑树:
也是一种平衡二叉树,但每个节点有一个存储位表示节点的颜色,可以是红或黑。通过对任何一条从根到叶子的路径上各个节点着色的方式的限制,红黑树确保没有一条路径会比其它路径长出两倍,因此,红黑树是一种弱平衡二叉树红黑树从根到叶子的最长路径不会超过最短路径的 2倍(由于是弱平衡,可以看到,在相同的节点情况下,AVL树的高度<=红黑树)相对于要求严格的 AVL 树来说,它的旋转次数少,所以对于搜索,插入,删除操作较多的情况下,用红黑树

8.HashMap和Hashtable的区别?
(1)HashMap允许key和value为null,Hashtable不允许。
(2)HashMap的默认初始容量为16,Hashtable 为11。
(3)HashMap的扩容为原来的2倍,Hashtable的扩容为原来的2倍加1。
(4)HashMap是非线程安全的,Hashtable是线程安全的。
(5)HashMap的hash值重新计算过,Hashtable直接使用hashCode。
(6)HashMap去掉了Hashtable中的contains方法。
(7)HashMap继承自AbstractMap类,Hashtable继承自Dictionary类。
9、hashcode是干什么用的?可以不重写hashcode方法吗?会出现什么问题?
-
hashcode是java中的一个方法,它用于计算对象的哈希码。在Java中,hashcode方法被用于哈希集合(如 HashSet)、哈希映射(如 HashMap)等数据结构中,以及在对象比较中用于提高性能
-
如果不重写hashcode方法,而只重写了equals方法,可能会导致以下问题:
(1)在哈希集合中无法准确查找:如果对象的equals方法判断为相等,但哈希码不同,那么在哈希集合中查找对象时可能无法找到对应的项。
(2)哈希映射中的问题: 在哈希映射中,相同的键应该具有相同的哈希码,否则可能导致无法正确地
查找或删除映射项。
(3)不稳定的哈希集合和映射行为: 如果对象被修改后导致哈希码发生变化,它可能无法正确地从哈希集合或映射中移除或查找。
10.什么是反射?
反射是指在运行状态中,对于任意一个类都能够知道这个类的所有的属性和方法,并且对于任意一个对象,部能够调用它的任意一个方法,这种动态获取传息以及动态调用对象方法的功能称为反射机制。
Mysql面试题
1.什么是事务
定义:在Mysql中,事务是一系列操作的集合,这些操作要么全部成功完成,要么全部不完成,从而保证数据的完整性和一致性。它是数据库并发控制和恢复机制的基础。
事务的四大特性特性
-
原子性(Atomicity):事务中的所有操作要么全部完成,要么全部不完成。例如,银行转账操作,从一个账户扣款和向另一个账户存款必须同时成功或同时失败。
如何保证数据的原子性使用:通过undo log回滚日志来实现,一个SQL执行失败,前面的全部回滚。
-
一致性(Consistency):事务执行前后,数据库的状态必须保持一致。例如,转账操作完成后,两个账户的总金额应该保持不变。
-
隔离性(Isolation):多个事务并发执行时,每个事务应该独立运行,互不干扰。例如,两个用户同时购买同一商品,事务隔离性可以防止出现数据冲突。
-
持久性(Durability):事务一旦提交,其结果就会永久保存到数据库中,即使系统出现故障也不会丢失。
持久性怎么实现:通过redo log来实现,每一次提交之后,都把这个修改写入redo log 中,即便Mysql没有把数据持久化到磁盘中,也能读取redo log来恢复数据
事务的使用场景
假设有一个电商系统,用户下单时需要完成以下操作:
-
从库存表中扣除商品数量。
-
在订单表中插入一条订单记录。
-
从用户账户中扣除金额。
如果这些操作不使用事务,可能会出现以下问题:
-
扣除了库存,但订单记录没有插入,导致库存减少但没有实际订单。
-
扣除了用户金额,但库存没有扣除,导致用户被多扣款。
通过事务可以将这些操作作为一个整体执行,确保要么全部成功,要么全部失败。
事务的使用示例
以下是一个简单的MySQL事务示例,假设我们有一个accounts表,用于存储用户账户余额,表结构如下:
CREATE TABLE product (
id INT AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(50) NOT NULL,
balance DECIMAL(10, 2) NOT NULL
);
插入一些初始数据:
INSERT INTO product(name,balance) VALUES('zs',500),('sy',1000);
现在,我们想从sy的账户转账300元给zs,使用事务来完成这个操作:
-- 开始事务
START TRANSACTION;
-- 从Alice账户扣除300元
UPDATE product SET balance = balance -300 WHERE name = 'sy'
-- 给Bob账户增加300元
UPDATE accounts SET balance = balance + 300 WHERE name = 'zs';
-- 检查操作是否成功
SELECT balance FROM accounts WHERE name = 'sy'; -- 应该是700
SELECT balance FROM accounts WHERE name = 'zs'; -- 应该是800
-- 提交事务
COMMIT;
如果在事务执行过程中,某一步操作失败(例如,更新语句执行错误),可以通过ROLLBACK语句回滚事务,撤销所有已经执行的操作:
-- 开始事务
START TRANSACTION;
-- 从Alice账户扣除300元
UPDATE product SET balance = balance - 300 WHERE name = 'sy';
-- 假设这里出现错误,需要回滚
ROLLBACK;
-- 查询余额,应该还是初始状态
SELECT balance FROM product WHERE name = 'sy'; -- 应该是1000
SELECT balance FROM product WHERE name = 'zs'; -- 应该是500
事务的隔离级别
MySQL支持多种事务隔离级别,包括:
-
READ UNCOMMITTED(读未提交):最低隔离级别,允许读取未提交的数据,可能出现脏读。
-
READ COMMITTED(读已提交):只能读取已提交的数据,避免脏读,但可能出现不可重复读。
-
REPEATABLE READ(可重复读)(默认级别):保证在同一个事务中多次读取同一数据的结果是一致的,但可能出现幻读。
-
SERIALIZABLE(串行化):最高隔离级别,完全隔离并发事务,避免所有并发问题,但性能开销最大。
如何实现,加锁,只有拿到锁的事务才能执行
可以通过以下语句设置事务隔离级别:
SET SESSION TRANSACTION ISOLATION LEVEL [隔离级别];
例如:
SET SESSION TRANSACTION ISOLATION LEVEL REPEATABLE READ;//(默认级别)
2.关系型数据库与非关系型数据库有什么区别
-
关系型数据库:适用于数据结构固定、对一致性要求高、需要复杂查询的场景。
-
非关系型数据库:适用于数据结构灵活、需要高扩展性和高性能、对一致性要求不高的场景。
更多推荐




所有评论(0)