文章目录
[〇. 什么是CURD](#〇. 什么是CURD)
[1.1 为什么需要事务?](#1.1 为什么需要事务?)
[二、ACID 特性](#二、ACID 特性)
[三. 事务提交方式](#三. 事务提交方式)
[1. 查看事务提交方式](#1. 查看事务提交方式)
[2.用 SET 来改变 MySQL 的自动提交模式:](#2.用 SET 来改变 MySQL 的自动提交模式:)
[4.2 四种标准的事务隔离级别](#4.2 四种标准的事务隔离级别)
[4.2.1 读未提交](#4.2.1 读未提交)
[4.2.2 读已提交](#4.2.2 读已提交)
[4.2.3 可重复读](#4.2.3 可重复读)
[4.2.4 串行化](#4.2.4 串行化)
[五. 数据库并发的场景](#五. 数据库并发的场景)
[5.1.1 第一类更新丢失(回滚覆盖)](#5.1.1 第一类更新丢失(回滚覆盖))
[5.1.2 第二类更新丢失(提交覆盖)](#5.1.2 第二类更新丢失(提交覆盖))
[5.1.3 总结与对比(第一类回滚,第二类并发提交)](#5.1.3 总结与对比(第一类回滚,第二类并发提交))
〇. 什么是CURD
CURD 是四个核心操作的缩写:
-
C - Create(创建): 向系统中添加新的数据记录或资源。
- 示例 :
INSERT INTO users ...(SQL),POST /users(REST API)
- 示例 :
-
U - Update(更新): 修改系统中已存在的数据记录或资源。
- 示例 :
UPDATE users SET ...(SQL),PUT /users/{id}(REST API)
- 示例 :
-
R - Read(读取): 从系统中检索、查询数据记录或资源。
- 示例 :
SELECT * FROM users ...(SQL),GET /users/{id}(REST API)
- 示例 :
-
D - Delete(删除): 从系统中移除数据记录或资源。
- 示例 :
DELETE FROM users ...(SQL),DELETE /users/{id}(REST API)
- 示例 :
一、事务
1.1 为什么需要事务?
我们之所以需要事务,是为了保证在并发操作 和可能发生故障 的情况下,数据库仍然能保持数据的 正确性 和 一致性。这通过事务的四个基本特性来保证,即 ACID 特性。
总结:事务 是一个不可分割的数据库操作序列,这些操作要么全部成功 ,要么全部失败 。它被看作一个单一的工作单元。你可以把它想象成一个 "全有或全无" 的包裹。
二、ACID 是什么?
A - 原子性(Atomicity)
-
含义:事务是最小的工作单元,不可再分。事务中的所有操作要么全部提交成功,要么全部失败回滚。
-
如上例:扣款和加款这两个操作,必须同时成功或同时失败。不可能只发生其中一个。
-
实现机制 :通常通过数据库的 Undo Log(回滚日志) 实现。如果事务失败,系统可以利用回滚日志将数据恢复到事务开始前的状态。
C - 一致性(Consistency)
-
含义:事务必须使数据库从一个一致性状态转变到另一个一致性状态。
-
如上例:转账前后,小明和小红的账户总额应该是不变的(忽略手续费)。事务不能破坏这种业务逻辑的完整性约束。如果事务成功,总额不变;如果事务失败,数据库会回滚到转账前的状态,总额依然不变。
I - 隔离性(Isolation)
-
含义:多个事务并发执行时,一个事务的执行不应影响其他事务的执行。数据库系统提供了不同的隔离级别来控制干扰程度。
-
问题举例:
-
脏读 :事务A读到了事务B未提交的数据,然后事务B回滚了,那么A读到的就是无效的"脏"数据。
-
不可重复读 :事务A多次读取同一数据,在此期间事务B修改并提交了该数据,导致事务A多次读取的结果不一致。
-
幻读 :事务A多次读取一个范围内的数据,在此期间事务B插入或删除 了该范围内的数据并提交,导致事务A多次读取时,发现记录数量发生了变化(像出现了幻觉)。
-
-
实现机制 :通常通过锁机制 或多版本并发控制(MVCC) 来实现。
D - 持久性(Durability)
-
含义:一旦事务提交成功,它对数据库中数据的改变就是永久性的,即使后续系统发生故障(如断电、崩溃),数据也不会丢失。
-
实现机制 :通常通过 Redo Log(重做日志) 实现。提交事务时,会先将数据变更写入日志,然后再写入磁盘。即使系统崩溃,重启后也能通过重做日志恢复已提交的事务数据。
补充:
事务的版本支持
在MySQL中只有使用了Innodb数据库引擎的数据库 才支持事务,MyISAM不支持。
三. 事务提交方式
事务的提交方式常见的有两种:
-
自动提交
-
手动提交
3.1事务操作:
1. 查看事务提交方式
sql
mysql> show variables like 'autocommit';

2.用 SET 来改变 MySQL 的自动提交模式:
sql
mysql> SET AUTOCOMMIT=0; #SET AUTOCOMMIT=0 禁止自动提交
mysql> SET AUTOCOMMIT=1; #SET AUTOCOMMIT=1 开启自动提交
3.开始事务和提交事务
sql
mysql> begin;
mysql> commit;
四.事务的隔离级别
4.1为什么会有事务的隔离级别
-
脏读 :一个事务读到了另一个 未提交事务修改的数据。如果那个事务回滚了,那么第一个事务读到的数据就是无效的。
-
不可重复读 :在同一个事务中,两次读取 同一条 数据,得到的结果不一致。这通常是因为在两次读取之间,另一个 已提交的事务修改了该数据。
-
幻读 :在同一个事务中,两次执行 相同的查询 ,返回的 结果集 不一致(行数变了)。这通常是因为在两次查询之间,另一个已提交 的事务 插入 或 删除 了符合查询条件的记录
不可重复读 vs 幻读:
不可重复读 的重点是修改 或删除(同一行数据的内容变了)。
幻读 的重点是新增 或删除(结果集的行数变了)
4.2 四种标准的事务隔离级别
SQL标准定义了四种隔离级别,从低到高,隔离性越来越强,但并发性能通常会随之下降。
| 隔离级别 | 脏读 | 不可重复读 | 幻读 | 说明 |
|---|---|---|---|---|
| 读未提交 | 可能 | 可能 | 可能 | 最低的隔离级别,一个事务能读到另一个未提交事务的修改。性能高,但数据一致性无保障。 |
| 读已提交 | 不可能 | 可能 | 可能 | 只能读到已提交事务的修改。这是Oracle、PostgreSQL、SQL Server 等数据库的默认级别。 |
| 可重复读 | 不可能 | 不可能 | 可能 | 确保在同一个事务中,多次读取同一数据的结果是一致的。这是MySQL InnoDB引擎 的默认级别。 |
| 串行化 | 不可能 | 不可能 | 不可能 | 最高的隔离级别,所有事务逐个依次执行,完全避免并发问题。性能最低,但数据一致性最有保障。 |
4.2.1 读未提交
场景:事务A修改数据但未提交,事务B可以读到这个未提交的修改。
过程:
事务A:(设置余额变为300,未提交)
sql
UPDATE accounts SET balance = 300 WHERE id = 1;
事务B:(读到了300,即脏读)
sql
SELECT balance FROM accounts WHERE id = 1;
事务A: (余额回滚到100)
sql
ROLLBACK;
结果:事务B读到了一个无效的数据(300)。
4.2.2 读已提交
场景:事务B只能读到事务A已提交的修改。
过程:
事务A: (设置余额变为300,未提交)
sql
UPDATE accounts SET balance = 300 WHERE id = 1;
事务B:(读到的是100,避免了脏读)
sql
SELECT balance FROM accounts WHERE id = 1;
事务A:COMMIT; (提交修改,余额确认为300)
事务B:(读到了300)
sql
SELECT balance FROM accounts WHERE id = 1;
结果:避免了脏读 ,但 事务B 在同一个事务内 两次读取同一数据,结果不一致,即不可重复读。
4.2.3 可重复读
场景:在事务开始时创建一个"快照",整个事务期间都基于这个快照进行读取,因此看不到其他已提交事务的修改。
过程:
事务B开始,创建快照 (在事务开始时创建"快照"这个动作是 自动的、由数据库系统自动完成的)。
事务A: (设置余额变为300 并且 提交)
sql
UPDATE accounts SET balance = 300 WHERE id = 1;
COMMIT;
事务B: (读到的仍然是100,因为读取的是快照数据)
sql
SELECT balance FROM accounts WHERE id = 1;
结果:在同一个事务B中,多次读取同一数据的结果是一致的,即避免了不可重复读。
4.2.4 串行化
场景:通过强制事务串行执行来解决所有问题。它会在读取的表和数据集上加锁,阻止其他事务的写入甚至读取。
过程:
事务B:BEGIN;(显式地开始一个事务)
事务B:
sql
SELECT * FROM accounts WHERE age < 30;
(这个查询会在符合条件的所有记录以及它们所在的索引范围上加共享锁,甚至在表级别加锁,具体取决于数据库的实现)
事务A:BEGIN;(此时开始另一个事务)
事务A:
sql
INSERT INTO accounts (name, balance, age) VALUES ('王五', 500, 25);
(这个插入操作因为试图修改被事务B锁定的范围,所以会被阻塞,进入等待状态,直到事务B释放锁)
事务B:COMMIT;(事务B提交,释放它持有的所有锁)
**事务A插入操作:**锁被释放后,才得以继续执行并成功。
结果:通过牺牲所有并发性能,换来了绝对的数据一致性。
五. 数据库并发的场景
数据库并发的场景有三种:
读-读: 不存在任何问题,也不需要并发控制
读-写: 有线程安全问题,可能会造成事务隔离性问题,可能遇到脏读,幻读,不可重复读
**写·写:**有线程安全问题,可能会存在更新丢失问题,比如第一类更新丢失,第二类更新丢失。
5.1 核心问题:写-写冲突
"更新丢失"本质上是两个或多个事务都成功提交了,但其中一个事务的写操作覆盖了另一个事务的写操作,导致 数据变更部分 或 全部丢失。
5.1.1 第一类更新丢失(回滚覆盖)
别称:脏写覆盖
定义 :两个事务(T1和T2)同时更新同一数据,T2的提交覆盖了T1的已提交数据,随后T1发生了回滚,导致T2的有效更新也连带被丢失了。
核心特征 :一个事务的回滚操作 ,影响到了另一个已提交事务 的数据。这在现代数据库的任何标准隔离级别下都不允许发生(包括读未提交)。
场景 :账户余额初始为 1000 元。事务T1取款100元,事务T2存款200元。
| 时间 | 事务T1 (取款100) | 事务T2 (存款200) | 数据库余额 | 说明 |
|---|---|---|---|---|
| t1 | BEGIN; |
1000 | 初始状态 | |
| t2 | UPDATE accounts SET balance = 900; |
900 | T1将余额直接改为900(这是一种简化写法,实际是 balance - 100) |
|
| t3 | BEGIN; |
900 | T2开始 | |
| t4 | UPDATE accounts SET balance = 1100; |
1100 | 关键点:T2基于当前值900,加上200,将余额更新为1100。此时它覆盖了T1的更新。 | |
| t5 | COMMIT; |
1100 | T2成功提交!此时余额1100是正确且应被持久化的。 | |
| t6 | ROLLBACK; |
1000 | 灾难发生:T1回滚。数据库将T1修改过的数据行恢复为原始值1000。这个操作无情地覆盖了T2已经提交的1100。 |
可以看到我们存入的200变成100了,这就会造成错误 第一类更新丢失 。
正是因为第一类更新丢失如此严重,所有主流数据库在最基本的层面就通过排他写锁(Exclusive Lock) 来防止它。
(排他写锁的机制:当一个事务(T1)更新某行数据时,它会立即获得该行的排他锁。在T1提交或回滚之前,任何其他事务(T2)都无法对这行数据进行修改(Update/Delete),只能等待。这就从根本上杜绝了两个事务同时修改同一行数据的可能性,从而避免了第一类更新丢失。)
总结 :
第一类更新丢失的特点是 "回滚覆盖了已提交的数据" ,其效果是一个成功提交的事务所做的修改被完全抹去。
5.1.2 第二类更新丢失(提交覆盖)
别称:读-修改-写覆盖
定义 :两个事务(T1和T2)读取同一数据 ,然后基于最初读取的值进行计算和更新,T2的提交覆盖了T1的已提交数据。
核心特征 :两个事务都成功提交 了,但后提交的事务覆盖了先提交事务的结果。这是在实际开发中非常常见 的 并发Bug。
| 时间 | 事务T1 (在末尾添加 "B") | 事务T2 (在末尾添加 "C") | 数据库内容 |
|---|---|---|---|
| t1 | BEGIN; |
A | |
| t2 | SELECT content FROM wiki WHERE id=1; (读到 "A") |
A | |
| t3 | BEGIN; |
A | |
| t4 | SELECT content FROM wiki WHERE id=1; (读到 "A") |
A | |
| t5 | UPDATE wiki SET content = 'A B' WHERE id=1; |
A B | |
| t6 | COMMIT; (事务T1成功提交) |
A B | |
| t7 | UPDATE wiki SET content = 'A C' WHERE id=1; (基于t4读到的旧值"A"进行更新) |
A C | |
| t8 | COMMIT; (事务T2成功提交) |
A C |
- 最终结果 :事务T1添加的 "B" 丢失了!最终内容只有 "A C",而不是预期的 "A B C"
如何防止第二类更新丢失?
1. 使用更高的隔离级别(可重复读或串行化)
2. 使用悲观锁
- 在事务开始时,使用
SELECT ... FOR UPDATE对要修改的行加排他锁。这样T1在读取时锁住行,T2必须等待T1提交后才能进行读取和更新。3. 使用乐观锁
在数据表中增加一个版本号(
version)字段或时间戳。更新时,带上之前读取的版本号作为条件:
UPDATE table SET ..., version = version + 1 WHERE id = ? AND version = @old_version。如果更新返回的影响行数为0,说明有其他事务已经更新了数据,本次更新失败,需要重新读取数据并重试业务逻辑。
5.1.3 总结与对比(第一类回滚,第二类并发提交)
| 特性 | 第一类更新丢失(回滚覆盖) | 第二类更新丢失(提交覆盖) |
|---|---|---|
| 根本原因 | 一个事务的回滚,影响了已提交的数据。 | 多个事务基于相同的旧基准值进行更新。 |
| 是否涉及回滚 | 是,由回滚操作引发。 | 否,两个事务都成功提交。 |
| 现代数据库 | 在任何隔离级别 下都被禁止(通过写锁解决)。 | 在读已提交 和读未提交 隔离级别下可能发生。在可重复读及以上级别被解决。 |
| 现实类比 | 你刚存完钱,银行系统崩溃,恢复后你的存款记录没了。 | 两个人同时编辑同一个在线文档,后保存的人覆盖了先保存的人的内容。 |