📑 目录
- [1. 事务基础概念](#1. 事务基础概念)
- [1.1 什么是事务?](#1.1 什么是事务?)
- [1.2 为什么要使用事务?](#1.2 为什么要使用事务?)
- [1.3 事务四大ACID特性](#1.3 事务四大ACID特性)
- [2. MySQL事务实操语法](#2. MySQL事务实操语法)
- [2.1 支持事务的存储引擎](#2.1 支持事务的存储引擎)
- [2.2 事务基础语法](#2.2 事务基础语法)
- [2.3 事务保存点SAVEPOINT](#2.3 事务保存点SAVEPOINT)
- [3. 事务隔离性与隔离级别](#3. 事务隔离性与隔离级别)
- [3.1 并发与隔离性通俗理解](#3.1 并发与隔离性通俗理解)
- [3.2 四大隔离级别是什么?](#3.2 四大隔离级别是什么?)
- [3.3 隔离级别并发问题](#3.3 隔离级别并发问题)
- [read uncommitted](#read uncommitted)
- [read committed(读已提交)](#read committed(读已提交))
- [repeatable read(可重复读)](#repeatable read(可重复读))
- [串行化 serializable](#串行化 serializable)
- [3.4 性能与安全取舍](#3.4 性能与安全取舍)
前言:
数据库事务ACID是Java后端、软件测开面试核心考点,本文结合转账实操案例通俗拆解事务原理、实操代码、并发隔离问题。
1. 事务基础概念
1.1 什么是事务?

事物我们可以理解为将一个东西打包成一个快递箱子
对应到MySQL语句中:事物把一组SQL语句打包成一个整体,在这一组SQL语句要么全部执行成功要么全部不执行
[⬅️ 返回目录](#⬅️ 返回目录)
1.2 为什么要使用事务?
既然我们在知道什么是事物的基础上,那么为什么我们要使用事物呢?
举一个简单的例子:

两个滑稽老铁A,B进行转账操作,A手机中余额:3000,B手机中余额:1000
滑稽A给滑稽B转账1000元,正常情况下:A中余额减少1000减少成2000,B中余额增加1000,增加成2000;
总额并没有发生改变,即使转账了加起来任然是4000;
意外情况:假设没有事物的情况之下;
一天滑稽老铁A给B进行转账转账1000元,突然 滑稽老铁B手机坏了 ; 此时转账的程序还没跑完, 滑稽老铁A已经将钱给转过去了,钱已经在A中扣除了,但是滑稽老铁B由于手机问题,B没有收到资金转账;此时就造成了一定的损失;
此时总额就变成了:3000
所以: 在一些特定的的情况之下,事物是非常重要的,事物要么发生,要么不发生起到了非常重要的作用
此时候我们不得不使用事物来防止这样的事物发生;
[⬅️ 返回目录](#⬅️ 返回目录)
1.3 事务四大ACID特性
事物的四大ACID特性包括:
- Atomicity (原子性)
一个事物中所有的操作,一定是要么发生,或者失败这两种情况,不会出现一个发生了,一个不发生这种情况,如果事物在执行过程中发生错误,就会触发roll back(回滚),回滚到事物开始前的状态;
正如日常生活中的转账,这一个过程要么发生,要么失败;若 A 扣款后程序崩溃,会自动回滚,A、B 余额不变 - Consistency (一致性)
事物开始时与事物开始后,数据库的完整性不会被破坏,写入的数据必须完全符合预设的规则,包括精度,关联性,以及事物执行过程中服务器崩溃后如何恢复;
原子性是手段,一致性是最终目标;依靠原子性、隔离性、持久性共同保障一致性。
就像我们刚才举例:转账时滑稽老铁A扣除了金钱,而滑稽老铁B因为手机问题没有收到钱这样一种情况;(不一样的数据) - Isolation (隔离性)
数据库允许多个并发事物对数据进行读写和修改,隔离性可以防止多个事物在并发执行时由于多个交叉执行而导致数据不一致;事物可以指定不用的隔离级别,在不同场景之下分别有利弊;
| 隔离级别 | 脏读 | 不可重复读 | 幻读 |
|---|---|---|---|
| 读未提交 Read Uncommitted | 允许 | 允许 | 允许 |
| 读已提交 Read Committed(Oracle 默认) | 禁止 | 允许 | 允许 |
| 可重复读 Repeatable Read(MySQL InnoDB 默认) | 禁止 | 禁止 | 允许 |
| 串行化 Serializable | 禁止 | 禁止 | 禁止 |
隔离性可以防止多个事物的干扰;
日常中很简单的一个例子:
两一个隔音好房间中分别有两个工人,同一个房间中的工人吐槽另一个房间中的工人,另一房间中的工人不会听见,也就相当于工人的吐槽并没有对另一个工人进行任何的干扰;
具体例子会在下文讲解
- Durability (持久性)
事物在结束的时候一旦commit提交成功,对数据库的数据修改永久生效,即使数据库宕机、断电、服务器崩溃,数据也不会丢失。
依靠 redo log(重做日志):提交时先写入 redo 日志,再刷盘;宕机重启后,通过 redo 日志恢复已提交事务的数据。
简单的例子:我们创建一个记事本,我们在写完日记的时候,保存完之后重启电脑,我们重新上线的时候,记事本中的日记依旧存在,只要你不删除,可以保存到硬件报废;
[⬅️ 返回目录](#⬅️ 返回目录)
2. MySQL事务实操语法
2.1 支持事务的存储引擎
- InnoDB(主流)
MySQL 5.5 及之后默认存储引擎,完整支持事务、MVCC、行锁、外键,满足 ACID 四大特性。
绝大多数业务系统(电商、支付、后台管理)均使用该引擎。 - MyISAM(不支持事务)
无事务、无崩溃安全恢复机制,仅支持表锁,并发性能差。
优势:查询速度快,适用于静态只读场景(日志归档、纯统计报表)。 - 其他引擎
Memory:内存表,不支持事务,重启数据丢失;
CSV/Archive:仅特定场景使用,无事务支持。
[⬅️ 返回目录](#⬅️ 返回目录)
2.2 事务基础语法
开启事物:
sql
start transaction或者begin
提交事物:
sql
commit;
``
  执行后所有修改持久化到磁盘,其他事务可见本次修改
`回滚事物:`
```sql
rollback;
执行后放弃当前事务内所有 DML 操作,数据恢复至事务开启前状态。
我们创建一个A表来进行举例子:
sql
create table if not exists A (
id int primary key auto_increment,
name varchar(20) not null,
score int
)
当前我使用的MySQL版本是8.0以上的版本,默认存储引擎就是:InnoDB
开启我们的事物-> 我们要插入(张三,成绩是80分的数据);
sql
start transaction;
insert into A values (null,'张三',80);
select* from A;
此时查询的数据和我们想要插入的数据相同,但是此时我们并没有提交事物;

rollback进行返回;
sql
start transaction;
insert into A values (null,'张三',80);
select* from A;
rollback;
select* from A;
此时我们就能够观察到,我们写入的数据不见了,此时这一个操作就像我们QQ或者微信中的撤回;

我们再次重新插入:(张三,成绩是80分的数据)
sql
start transaction;
insert into A values (null,'张三',80);
select* from A;
commit;
查询数据:

在我们提交事物之后再次进行查询;
sql
start transaction;
insert into A values (null,'张三',80);
select* from A;
commit;
select* from A;
我们发现此时的结果是一模一样的;

执行后所有修改持久化到磁盘,其他事务可见本次修改。
[⬅️ 返回目录](#⬅️ 返回目录)
2.3 事务保存点SAVEPOINT
语法 :
sql
savepoint 保存点名字
我们再一次创建一张一模一样的B表来进行讲解:
sql
create table if not exists B (
id int primary key auto_increment,
name varchar(20) not null,
score int
)
示例:此时我们想要插入(张三,80分),(李四,90),(王五,95);并在每一个中间设定事物保存点;
sql
start transaction;
insert into B(name,score) values('张三',80);
savepoint pos1;
insert into B(name,score) values('李四',90);
savepoint pos2;
insert into B(name,score) values('王五',95);
select* from B;
查询结果;

此时我们指定返回到pos2位置进行查询;
sql
rollback to pos2;
select* from B;
我们可以看见结果少了王五;

我们再一次进行回滚到pos1位置;
sql
rollback to pos1;
select* from B;
结果显而易见;

最后回滚完;
sql
rollback;
select* from B;
结果就是没有任何的数据;

[⬅️ 返回目录](#⬅️ 返回目录)
3. 事务隔离性与隔离级别
3.1 并发与隔离性通俗理解
并发:多个客户端同时读写同一张表 / 同一行数据,会出现脏读、不可重复读、幻读问题
并发:通俗理解 就是多个人同时操作一份数据
我们可以理解为:两个人同时去取同一张银行卡里面的钱;
银行卡余额 1000 元:
用户 A 在 ATM 取钱,同时用户 B 用手机银行转账
- 两个人同时读取余额,都看到余额是 1000,各自扣 500,最后账户本该剩 0,结果变成 500,钱凭空多出来 ------ 这就是并发带来的数据错乱。
隔离性 : 事务四大特性 ACID 中的 I(Isolation),通过事务隔离级别控制并发冲突,平衡并发性能与数据安全:
- 读未提交、读已提交、可重复读(MySQL 默认)、串行化
隔离级别越高,并发性能越差;级别越低,并发问题越多,业务需按需取舍
设置和查看隔离级别:
查看隔离级别:
sql
-- 全局隔离级别
select @@GLOBAL.transaction_isolation;
-- 当前会话隔离级别(仅当前窗口生效)
select @@SESSION.transaction_isolation;
-- 简写
select @@transaction_isolation;
设置隔离级别:`
方式 1:临时设置(重启数据库失效)
sql
-- 设置全局(新连接生效)
set global transaction_isolation = '隔离级别名字';
SET SESSION transaction_isolation = 'REPEATABLE-READ'
-- 设置当前会话(立即生效,仅当前窗口)
set session transaction_isolation = '隔离级别名字';
SET @@SESSION.transaction_isolation='REPEATABLE-READ';
注意 : 设置隔离级别的时候,隔离级别中间要使用 - 来隔开,否则会报错 ;
方式 2:永久生效(修改配置文件 my.cnf/my.ini)
sql
[mysqld]
transaction-isolation = REPEATABLE-READ
第二种设置隔离级别的方式是我们以后工作中经常使用的方式;
- global 全局:改完之后新开的数据库连接才会使用新隔离级别,已经打开的窗口不受影
- session 会话:只改当前你正在操作的这个查询窗口,关闭窗口就失效;
- 绝大多数业务使用默认 repeatable-read;
- 支付、金融类业务可按需调整为 read-committed; (默认的隔离级别)
- 几乎不会使用 read-uncommitted, 性能极低场景才考虑;
- 报表、强一致性场景偶尔使用 serializable, 并发性能损耗大。
[⬅️ 返回目录](#⬅️ 返回目录)
3.2 四大隔离级别是什么?
| 隔离级别 | 脏读 | 不可重复读 | 幻读 |
|---|---|---|---|
| 读未提交 read uncommitted | 允许 | 允许 | 允许 |
| 读已提交 read committed(Oracle 默认) | 禁止 | 允许 | 允许 |
| 可重复读 repeatable read(MySQL InnoDB 默认) | 禁止 | 禁止 | 允许 |
| 串行化 serializable | 禁止 | 禁止 | 禁止 |
[⬅️ 返回目录](#⬅️ 返回目录)
3.3 隔离级别并发问题
read uncommitted
这是数据库事物中 隔离级别最低 的级别;
当前隔离级别会出现脏读的现象;
什么是脏读?
脏读:我们可以理解为,一个事务可以读取到另一个事务未提交的修改数据
还是以去银行取钱的例子为例:

一天滑稽老铁A和滑稽老铁B同时去了银行,但是去的不是同一家银行;(此时我们认为滑稽老铁A和B是家属关系)
卡里有1000块钱,滑稽老铁A取出了500元,此时账户的余额变成了500;
而此时滑稽老铁B去银行查看卡里还有多少钱?一查询发现卡了还有500元;
而此时滑稽老铁A又不想要这500元了,于是就把钱给重新存回去,然后滑稽老铁B又重新查询了一遍账户余额,发现账号余额多了500元变成了1000元;
此时我们就能认为滑稽老铁B查询到的500元是无效数据,此时我们就认为这是脏读;
用代码来进行表示:
代码前置要求:创建一张滑稽老铁的银行卡;
sql
create table if not exists A (
id int primary key,
name varchar(20) default '匿名',
amount decimal(10,2)
);
insert into A values (1,'滑稽老铁的银行卡',1000.00);
select* from A;

此时我们我们讲隔离级别修改为: read uncommitted
sql
select @@SESSION.transaction_isolation;

此时两个客户端的隔离级别都是相同的;
事物A中开启事物;
事物A
sql
start transaction;
update A set amount = 500.00 where id = 1;
select* from A;

此时在客户端B中开启事物;
sql
start transaction;
select* from A;

此时客户端2查询到的结果为:500;
执行完成之后,在A中进行回滚;
sql
start transaction;
update A set amount = 500 where id = 1;
select* from A;
rollback;
select* from A;
我们再一次在另一个客户端进行查询;
sql
start transaction;
select* from A;
select* from A;
commit;

我们此时就能看见在B中,同一个事物内,出现了两次不同的结果;
一个事务读取到另一个事务未提交、最终回滚作废的数据;
[⬅️ 返回目录](#⬅️ 返回目录)
read committed:(读已提交)
为了解决脏读问题,我们可以把隔离级别设置为read committed此时事物只能读到别人已提交过的事物,与此同时也会出现不可重复读的问题
此时,我们可以再一次重复脏读的流程,把隔离性修改成read committed
sql
SELECT @@GLOBAL.transaction_isolation;
SET @@GLOBAL.transaction_isolation='read-committed';

什么是不可重复读?
不可重复读:同一个事务内,多次读取同一行数据,中途被其他事务提交修改 / 更新,导致前后两次查询结果不一致。
脏读:读到未提交、回滚作废的数据
不可重复读:读到其他事务已经正常提交生效的修改数据
还是以转账为例子:
小 A:事务 1,自己查银行卡余额(RC 隔离)
小 B:事务 2,小 A 家人,转账操作
小 A 打开银行 APP 第一次查余额:账户显示 1000 元(第一次读取)
不关闭页面,暂时停留在余额页面,没有结束本次查询
家人小 B 另一边手机,给这张卡转入 500 元,点击确认转账(事务提交完成)
小 A 在自己页面刷新,第二次查余额:变成 1500 元
我们可以理解为:一个事物中,看到了两次不一样的结果我们就把这个就叫做不可重复读;
初始数据:小A开启事物并查询
sql
start transaction;
select* from A;

此时小B开启事物,给银行卡转账500元;
sql
start transaction;
update A set amount = amount+500 where id = 1;
commit;
select* from A;
然后小A继续查询数据;
sql
start transaction;
-- 第一次查询
select* from A;
-- 第二次查询
select* from A;

同一个事务(小 A 这次打开 APP 查看账户的操作),两次读取同一笔余额,中间没有自己修改任何数据,只是别人提交了更新,前后数值不一样,这就是不可重复读。
[⬅️ 返回目录](#⬅️ 返回目录)
repeatable read(可重复读)
为了解决不可重复读,我们把隔离性等级调整到repeatable read
sql
use boke;
SET @@GLOBAL.transaction_isolation='repeatable-read';
SELECT @@GLOBAL.transaction_isolation;

再次进行我们上述不可重复的操作;

repeatable read隔离级别引入了快照这一个机制来帮我们解决不可重复读的问题,我们可以理解为提前拍了一张照片,后续相关操作时与照片进行比较;
当然repeatable read 也会出现问题,会出现幻读的一种问题,当然MySQL中repeatable read也解决出了一部分幻读的问题;
什么是幻读?
同一个事务内,多次执行相同范围查询,中间被其他事务插入 / 删除了符合条件的数据,导致前后两次查询结果行数不一样,像出现了幻觉,这就是幻读。
此时我们重新来创建一张表:
sql
create table if not exists student (
id int primary key,
name varchar(20) default '匿名',
score int
);
我们此时在A客户端中学生表中插入数据,然后开启事物:
sql
insert into student value (1,'张三',90);
start transaction;
select* from student;

此时在去B中开启事物并查询;
sql
start transaction;
select* from student;

此时在客户端A中插入数据;
sql
insert into student value (1,'张三',90);
start transaction;
select* from student;
insert into student value (2,'李四',90);
select* from student;

插入完成之后,再去客户端B中查询:
sql
start transaction;
-- 第一次查询
select* from student;
-- 第二次查询
select* from student;

此时我们还是能看见,并没有出现幻读,我们刚才了解到repeatable read 解决了一定的幻读行为
此时我们可以这么去看:
我们在客户端A中开启事物并插入一条数据,然后客户端B中也开启事物,进行查询;

然后此时我们在客户端B中插入一条和客户端A一模一样的数据;

我们此时如果让客户端A中进行rollback,那么客户端B就会插入成功;

如果我们此时客户端A进行了commit客户端B中就会报错;

我们此时就能认为当前repeatable 隔离级别发生了幻读;
[⬅️ 返回目录](#⬅️ 返回目录)
串行化 serializable
- 核心特性:
MySQL 事务最高隔离级别
四大隔离问题全部解决:无脏读、不可重复读、幻读;
所有普通 SELECT 语句自动隐式加共享读锁,等价于 SELECT ... LOCK IN SHARE MODE;
读写冲突、写写冲突都会阻塞,事务只能串行执行,并发性能最差 - 锁机制
普通查询自动加共享锁,其他事务无法修改 / 插入区间数据;
范围查询会加临键锁,彻底封锁间隙,杜绝新增数据产生幻读;
只要事务未提交,锁定的数据 / 区间其他事务无法写入。
优点:数据一致性最强,不会出现任何事务并发异常,适合金融、对账等强一致性场景;
缺点:并发极低,大量锁等待,极易触发锁超时、死锁,线上业务极少使用(执行速度非常慢);
我们讲隔离级别修改成serializable;
sql
SET @@GLOBAL.transaction_isolation='serializable';
SELECT @@GLOBAL.transaction_isolation;

还是以我们转账为例子:
sql
create table if not exists A (
id int primary key,
name varchar(20),
account int
);
客户端A中开启事物,并插入一条数据;
然后再B中开启事物,修改金额为1500;
sql
start transaction;
update A set account = 1500 where id = 1;

此时我们客户端B中在进行等待,要让客户端A进行提交之后才会执行下一步操作;

如果客户端A长时间不操作,客户端B就会失败,超时了
只有当客户端A完成操作时,客户端B才会进行下一步操作;
[⬅️ 返回目录](#⬅️ 返回目录)
3.4 性能与安全取舍
隔离级别越高,数据一致性(安全)越强,但并发性能越差;二者反向权衡。
- 读未提交 RU
性能最高,安全最差;脏读、不可重复读、幻读全存在,线上不用。 - 读已提交 RC
性能较高,仅解决脏读;无间隙锁,会幻读,适合日志、埋点等弱一致业务。 - 可重复读 RR(MySQL 默认)
性能与安全均衡;杜绝脏读、不可重复读,加锁读可消除幻读,绝大多数业务首选。 - 串行化 Serializable
安全最强,无任何并发异常;性能最差,读写互堵、死锁多,仅低并发金融对账使用。
不升级隔离、兼顾性能与安全的优化手段
- 控制短事务,缩短锁持有时间;
- 保证索引精准,避免锁范围扩大;
- 查询分两种:普通快照读不加锁,范围更新手动加悲观锁;
- 高并发更新使用乐观锁(version 版本号)减少锁竞争;
- 大事务拆分批量提交,降低锁冲突。
问:为什么不直接用串行化保证数据安全?
答:串行化所有读自动加共享锁,读写互相阻塞,系统吞吐量暴跌,锁等待、死锁大幅增多,并发业务无法承受;业务层面可通过 RR + 手动加锁实现同等一致性,性能损耗更小。
[⬅️ 返回目录](#⬅️ 返回目录)