数据库事务

1. 什么是事务

事务(Transaction)是一组要么全部成功、要么全部失败的数据库操作。

以转账为例:

  • A 账户扣 100
  • B 账户加 100

这两步必须作为一个整体执行,不能只成功一半。


2. ACID 四大特性

  1. 原子性(Atomicity)
  • 事务内操作不可分割,要么全成功,要么全回滚。
  1. 一致性(Consistency)
  • 事务前后,数据库从一个一致状态转移到另一个一致状态。
  1. 隔离性(Isolation)
  • 并发事务之间互不干扰。
  1. 持久性(Durability)
  • 事务提交后结果持久保存。

InnoDB 常见实现对应关系(简化):

  • 原子性:undo log
  • 持久性:redo log
  • 隔离性:MVCC + 锁
  • 一致性:由原子性、隔离性、持久性共同保障

3. 并发异常:脏读、不可重复读、幻读

  1. 脏读
  • 读到了别的事务未提交的数据。
  1. 不可重复读
  • 同一事务中,两次读取同一行,结果不同。
  1. 幻读
  • 同一事务中,两次范围查询,结果集行数变化(多了或少了"幻影行")。

4. 四种隔离级别

  1. 读未提交(Read Uncommitted)
  • 允许脏读、不可重复读、幻读。
  • 最低的隔离级别,事务可以读取其他事务尚未提交的数据,虽然拥有超高的并发处理能力及很低的系统开销,但很少用于实际应用,因为可能导致数据不一致性。
  1. 读已提交(Read Committed,RC)
  • 避免脏读。
  • 仍可能不可重复读、幻读。
  • 事务只能读取已经提交的数据,避免了脏读问题,但可能导致不可重复读和幻读。这是大多数数据库系统的默认隔离级别(如 Oracle 和 SQL Server),但不是 MySQL 的默认。
  1. 可重复读(Repeatable Read,RR,InnoDB 默认)
  • 避免脏读。
  • 在快照读语义下避免不可重复读。
  • 配合 next-key 锁可抑制很多幻读场景。
  • 事务在整个事务期间保持一致的快照,其他事务的修改不会影响正在运行的事务,从而防止不可重复读问题。这是 MySQL InnoDB 默认的事务隔离级别。
  1. 串行化(Serializable)
  • 隔离最强,但并发能力最低。解决所有事务并发问题。
  • 最高的隔离级别,通过强制事务排序,使之不可能相互冲突,从而防止所有并发问题。虽然这个隔离级别可以解决上面提到的所有并发问题,由于事务是串行执行,所以效率会大大下降,应用程序的性能会急剧降低。最直观的体现就是,当数据库隔离级别设置为串行化后,A 事务在未提交之前,B 事务对 A 事务数据的操作都会被阻塞。通常不会使用这个隔离级别,我们需要其他机制来解决这些问题:比如乐观锁和悲观锁。

快速对照(经典结论):

严重级别
隔离水平高低

针对不同的隔离级别,并发事务可能发生的现象也会不同。


5. 先讲最容易混淆的一点:RC 和 RR 的"共同点"

你在对话里反复追问的核心非常关键:

"行锁是不是事务结束才释放?"

标准答案:

  • 对于 UPDATE、DELETE、SELECT ... FOR UPDATE 这类当前读写操作,InnoDB 的行级排他锁通常都要等到 COMMIT 或 ROLLBACK 才释放。
  • 这点在 RC 和 RR 下都成立。

所以:

  • 同一行并发 UPDATE,不会并行成功。
  • 谁先拿到锁谁先执行,其他事务阻塞等待。

此外,下面这个场景也要牢记:

  • UPDATE t SET c = c + 0 WHERE id = 1

即使值"看起来没变",它仍然是写操作,仍要参与锁竞争。


6. RC 和 RR 的"真正区别"

如果写锁释放时机一样,那差异在哪里?

核心就两条:

  1. 快照读(普通 SELECT)生成 Read View 的策略不同
  • RC:每条语句都可能生成新的 Read View
  • RR:事务内一致性读通常复用同一个 Read View
  1. 范围锁策略不同
  • RR 更积极使用 next-key lock(记录锁 + 间隙锁)抑制范围幻读
  • RC 下间隙锁行为更弱(一般场景不如 RR 强)

一句话记忆:

  • 写冲突层面:RC 和 RR 很像
  • 读一致性与防幻读层面:RC 和 RR 差异明显

7. MVCC:为什么普通查询不一定阻塞写

MVCC(多版本并发控制)让"读"和"写"在很多场景下能并行:

  • 行记录有版本信息(可理解为 trx_id + undo 版本链)
  • 读取时根据 Read View 判断哪个版本对当前事务可见

关键结论:

  • 普通 SELECT(快照读)多数情况下不加行锁
  • UPDATE / DELETE / SELECT ... FOR UPDATE 属于当前读,会加锁

这就是"有时不阻塞、有时阻塞"的根本原因。


8. 多并发案例

下面用同一张表演示:

sql 复制代码
CREATE TABLE account (
  id INT PRIMARY KEY,
  balance INT NOT NULL
) ENGINE=InnoDB;

INSERT INTO account(id, balance) VALUES (1,100), (2,100)
ON DUPLICATE KEY UPDATE balance=VALUES(balance);

案例 A:RR 下,T1 先 A+10 再 B-10,T2 执行 B+0

事务定义:

  • T1:先改 id=1(A),再改 id=2(B)
  • T2:改 id=2(B+0)

时序:

  1. T1: BEGIN
  2. T1: UPDATE id=1
  3. T1: UPDATE id=2
  4. T2: BEGIN
  5. T2: UPDATE id=2(阻塞)

结论:

  • T2 被阻塞直到 T1 提交/回滚
  • B+0 也会参与锁竞争

案例 B:RR 下交叉更新(经典死锁)

事务定义:

  • T1:A + 10,再 B - 10
  • T2:B + 10,再 A - 10

典型交错:

  1. T1 锁住 A
  2. T2 锁住 B
  3. T1 请求 B,等待
  4. T2 请求 A,等待
  5. 循环等待,死锁检测触发,MySQL 回滚其中一个事务

关键结论:

  • 先遇到的是死锁,不是"提交顺序影响结果"
  • 死锁分支下,提交顺序问题失去意义

案例 C:同一行三事务更新,为什么会像"按顺序覆盖"

初始 C=10,三个事务都改 C:

  • T1: C = C - 10
  • T2: C = C + 10
  • T3: C = C * 10

真实过程:

  • 三者不是并发同时改成功,而是排队抢同一把行锁
  • 前一个提交后,下一个才继续执行
  • 后一个基于"最新已提交值"计算

所以"按顺序覆盖"不是无锁并行,而是锁竞争后的串行化效果。

案例 D:RC 与 RR 在普通 SELECT 的差异

假设事务 X 中两次普通 SELECT 同一行:

  • RC:第二次可能看到别的事务刚提交的新值
  • RR:第二次通常仍看到旧快照值(可重复读)

案例 E:范围查询与幻读

RR 下锁定读:

sql 复制代码
SELECT * FROM account WHERE id BETWEEN 1 AND 10 FOR UPDATE;

在合适索引条件下,可能触发 next-key 锁,抑制该范围插入导致的幻读。


9. 对话中一个常见误区的纠正

误区说法:

  • "RC 下交叉更新不会死锁,RR 才会死锁"

更准确说法:

  • 死锁根因是加锁顺序冲突,不是隔离级别名字本身。
  • 在同样的锁顺序反转场景下,RC 也可能死锁。
  • RR 之所以经常被提到,是因为范围锁策略更强,锁冲突场景更多。

10. 如何在业务里降低死锁与事务冲突

  1. 固定更新顺序
  • 多资源更新统一顺序,例如统一按主键升序。
  1. 缩短事务
  • 减少事务中非必要逻辑,缩短持锁时间。
  1. 让 SQL 走索引
  • 避免锁范围扩大。
  1. 做好重试机制
  • 捕获 1213(死锁)和 1205(锁等待超时),幂等重试。
  1. 区分快照读与当前读
  • 普通 SELECT 与 SELECT ... FOR UPDATE 语义完全不同。

11. 可直接复现的双会话脚本

会话 1:

sql 复制代码
SET SESSION TRANSACTION ISOLATION LEVEL REPEATABLE READ;
BEGIN;
UPDATE account SET balance = balance + 10 WHERE id = 1;
-- 暂不提交

会话 2:

sql 复制代码
SET SESSION TRANSACTION ISOLATION LEVEL REPEATABLE READ;
BEGIN;
UPDATE account SET balance = balance + 10 WHERE id = 1;
-- 此处阻塞

会话 1:

sql 复制代码
COMMIT;

会话 2 将解除阻塞继续执行。

如果把两会话改成 A->B 与 B->A 顺序,就可快速复现死锁。


12. 一句话总复盘

  • RC 和 RR 在"写锁释放时机"上基本一致,都是事务结束才释放。
  • 两者真正区别在"快照读可见性规则 + 防幻读锁策略"。
  • 并发更新看上去像"提交顺序决定结果",本质是锁队列导致的串行化执行。
相关推荐
_376271531 小时前
怎样查询不同表的字段差异 information_schema结构对比
jvm·数据库·python
YL200404261 小时前
MySQL-进阶篇-存储引擎
数据库·mysql
weixin_444012931 小时前
宝塔面板如何实现网站重定向_配置301永久跳转与域名更换
jvm·数据库·python
m0_733565461 小时前
CSS如何高效命名样式类_掌握BEM规范提升语义化程度
jvm·数据库·python
lzh200409191 小时前
MySQL零基础入门:从建库到增删改查
数据库·mysql
woxihuan1234561 小时前
CSS如何引入自适应图标_利用svg外链配合css控制颜色
jvm·数据库·python
2401_880071401 小时前
如何正确合并多个 Word 文档(.docx)并保留格式与分页
jvm·数据库·python
瀚高PG实验室1 小时前
瀚高数据库V45及V6用户锁定后解锁步骤
运维·数据库·瀚高数据库
wang3zc1 小时前
MySQL行锁升级为表锁的原因是什么_分析非索引字段查询影响
jvm·数据库·python