事务ACID理解
数据库事务核心概念:ACID、隔离级别与MVCC
用账户余额和流水场景,理解事务的行为及不同情况下的数据影响
涵盖:ACID特性、MySQL隔离级别、MVCC机制、数据库锁
目录
- 场景设定
- 1. Atomicity(原子性)
- 2. Consistency(一致性)
- 3. Isolation(隔离性)
- 4. Durability(持久性)
- 5. MySQL 四种隔离级别
- 6. MVCC(多版本并发控制)
- 7. 数据库锁
- 总结:ACID与MVCC的关系
场景设定
银行转账系统:
- 账户表:
id,balance(余额) - 流水表:
id,from_account,to_account,amount,status
转账操作(A转100给B):
- A账户余额 -100
- B账户余额 +100
- 插入流水记录
1. Atomicity(原子性)
定义: 事务中的所有操作要么全部成功,要么全部失败回滚,不存在中间状态。
场景
转账过程:
1. A余额 1000 → 900 ✓
2. B余额 500 → 600 ✗ (系统崩溃)
| 情况 | 结果 |
|---|---|
| 没有原子性 | A少了100,B没收到,100元凭空消失 |
| 有原子性 | 检测到第2步失败,回滚第1步,A恢复1000,B保持500 |
核心: 原子性保证不会出现数据不一致的中间状态。
2. Consistency(一致性)
定义: 事务执行前后,数据库必须处于合法状态(满足所有约束和业务规则)。
一致性是目标,其他特性是手段。
一致性(数据正确合法)
↑ ↑ ↑
┌────┘ ┌────┘ ┌────┘
↓ ↓ ↓
原子性 隔离性 数据库约束
(过程) (并发) (规则定义)
两层含义
数据库层面(显式约束)
CREATE TABLE accounts (
balance DECIMAL(10,2) CHECK (balance >= 0)
);
场景: A余额50元,转账100给B
- 执行后A = -50,违反
CHECK (balance >= 0) - 事务回滚,报错"余额不足"
业务层面(隐式规则)
场景: A(1000) 转100给 B(500)
| 规则 | 说明 |
|---|---|
| 转账前 | 总资金 = 1000 + 500 = 1500 |
| 正确结果 | A(900) + B(600) = 1500 ✓ |
| 错误情况 | 只扣A没加B:A(900) + B(500) = 1400 ✗ |
核心: 一致性保证业务规则始终成立,数据始终合法。
3. Isolation(隔离性)
定义: 并发执行的事务之间互不影响,每个事务感觉像在独占数据库。
场景:并发转账
A同时给B和C各转100元:
┌─────────────────────────────────────────────────┐
│ 事务1(A→B) │ 事务2(A→C) │
├─────────────────────────────────────────────────┤
│ 读取A余额: 1000 │ │
│ │ 读取A余额: 1000 │
│ A = 1000 - 100 │ │
│ 写入A: 900 │ │
│ │ A = 1000 - 100 │
│ │ 写入A: 900 ← 覆盖! │
│ B = 500 + 100 │ │
│ 写入B: 600 │ │
│ │ C = 300 + 100 │
│ │ 写入C: 400 │
└─────────────────────────────────────────────────┘
结果:A=900, B=600, C=400
问题:A实际转出200,余额应为800,却是900!(丢失更新)
隔离性解决: 事务2等待事务1完成后,读取A=900,再执行扣款。
核心: 隔离性保证并发时数据正确,避免脏读、不可重复读、幻读。
4. Durability(持久性)
定义: 事务一旦提交,数据永久保存,即使系统故障也不会丢失。
场景
转账成功,用户收到"转账完成"提示
刚提交时:A=900, B=600(数据还在内存中)
↓
突然断电!
┌────────────────────────────────────────┐
│ 没有持久性 │
│ 数据丢失,A恢复1000 │
│ 用户以为转了,实际没转! │
├────────────────────────────────────────┤
│ 有持久性(WAL预写日志) │
│ 重启后通过日志恢复 │
│ A=900, B=600,交易有效 │
└────────────────────────────────────────┘
核心: 持久性保证已确认的数据永不丢失。
5. MySQL 四种隔离级别
MySQL InnoDB 默认使用 Repeatable Read(可重复读)。
级别对比
| 隔离级别 | 脏读 | 不可重复读 | 幻读 | 说明 |
|---|---|---|---|---|
| Read Uncommitted | ✗ 允许 | ✗ 允许 | ✗ 允许 | 基本不用,性能稍好但数据可能很脏 |
| Read Committed | ✓ 禁止 | ✗ 允许 | ✗ 允许 | Oracle/SQL Server默认,大多数场景够用 |
| Repeatable Read | ✓ 禁止 | ✓ 禁止 | ✓ 禁止* | MySQL默认,事务内多次读结果一致 |
| Serializable | ✓ 禁止 | ✓ 禁止 | ✓ 禁止 | 串行执行,性能最差,基本不用 |
*MySQL的RR通过MVCC+间隙锁,在InnoDB中幻读也基本被解决。
账户场景示例
-- 事务1:查A账户余额
START TRANSACTION;
SELECT balance FROM accounts WHERE id = 'A'; -- 读到1000
-- 事务2:更新A账户(同时执行)
START TRANSACTION;
UPDATE accounts SET balance = 900 WHERE id = 'A';
COMMIT;
-- 事务1:再次查A账户余额
SELECT balance FROM accounts WHERE id = 'A';
-- RC → 读到900(不可重复读)
-- RR → 读到1000(可重复读,快照版本)
设置隔离级别
-- 查看当前级别
SELECT @@transaction_isolation;
-- 设置会话级别
SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;
简单理解
| 级别 | 一句话 |
|---|---|
| RU | 别人改完还没提交,我就能看见 |
| RC | 别人提交了,我才能看见 |
| RR | 事务开始后,我看到的数据不变 |
| Serializable | 完全串行,一个完了下一个 |
6. MVCC(多版本并发控制)
定义: 数据库用来实现"快照读"的机制,不用加锁就能让不同事务看到不同版本的数据。
Multi-Version Concurrency Control
核心思想
不直接修改原数据,而是保存多个版本:
数据版本链
┌─────────┐
│ A=1000 │ ← 版本1(事务1启动时)
└────┬────┘
│
┌────▼────┐
│ A=900 │ ← 版本2(事务2修改后)
└────┬────┘
│
┌────▼────┐
│ A=800 │ ← 版本3(事务3修改后)
└─────────┘
每个事务根据启动时的时间点,找到自己能看到的版本
账户场景
事务1(启动早) 事务2(启动晚)
START TRANSACTION; START TRANSACTION;
-- 事务ID = 100 -- 事务ID = 200
SELECT balance FROM A; SELECT balance FROM A;
→ 看到版本1:A=1000 → 看到版本3:A=800
-- 事务2中间修改过A
怎么判断能看到哪个版本
每行数据隐藏两个字段:
| 字段 | 含义 |
|---|---|
DB_TRX_ID |
最后修改这行数据的事务ID |
DB_ROLL_PTR |
回滚指针,指向旧版本 |
判断逻辑:
读取时,从最新版本开始沿着版本链往回找:
- 找到第一个"已提交"且"对我可见"的版本
可见性规则(简化):
- 版本的事务ID < 我的事务ID → 可能可见
- 版本的事务ID > 我的事务ID → 不可见(比我晚开始的)
MVCC 解决的三个核心问题
| 问题 | 场景 | MVCC 怎么解决 |
|---|---|---|
| 读不阻塞写 | 查账时有人转账 | 查账读快照,转账写新版本,互不阻塞 |
| 写不阻塞读 | 转账时有人查账 | 转账生成新版本,查账继续读旧快照 |
| 读到的数据一致 | 事务内多次查询 | 始终读同一个快照,结果不变 |
实际业务场景
场景1:报表统计(最典型)
业务:生成月度财务报表,需要统计所有账户余额
没有MVCC:
- 统计10万条数据要10秒
- 这10秒内如果有转账,统计结果不准
- 只能凌晨没人用时跑报表
有MVCC:
- 事务启动时拍快照
- 统计过程中别人随便转账
- 结果始终准确
- 白天也能跑报表
场景2:备份数据库
业务:在线热备份,不停止服务
没有MVCC:
- 备份开始,锁全表
- 备份期间所有转账、查询都卡住
- 用户体验极差
有MVCC:
- 备份事务读快照
- 正常业务继续读写
- 备份的数据是某个时间点的完整快照
场景3:长事务查询
业务:复杂查询,涉及多表关联,执行时间较长
没有MVCC:
- 查询过程中数据被改,结果混乱
- 或者查询加锁,阻塞其他业务
有MVCC:
- 查询看到一致的数据快照
- 不阻塞其他事务
- 结果可预期
一句话总结
MVCC = 保存数据的历史版本 + 每个事务看自己的快照
读写互不阻塞,同时保证数据一致性。
7. 数据库锁
MVCC 处理读,锁处理写和写写冲突。
MySQL InnoDB 锁类型
┌─────────────────────────────────────────┐
│ MySQL InnoDB 锁 │
├─────────────────────────────────────────┤
│ │
│ 按粒度分: │
│ 行锁(Row Lock)← 最常用 │
│ 间隙锁(Gap Lock)← RR防幻读 │
│ 临键锁(Next-Key Lock)← 行+间隙 │
│ 表锁(Table Lock)← DDL用 │
│ │
│ 按功能分: │
│ 共享锁(S锁)← 读锁,可多个同时读 │
│ 排他锁(X锁)← 写锁,独占 │
│ │
└─────────────────────────────────────────┘
什么时候加锁
-- 场景1:普通SELECT(快照读)
SELECT balance FROM A WHERE id = 1;
-- 不加锁!用MVCC读快照
-- 场景2:当前读(手动加锁)
SELECT balance FROM A WHERE id = 1 FOR UPDATE;
-- 加排他锁(X锁),其他事务不能改这行
-- 场景3:UPDATE(自动加锁)
UPDATE A SET balance = 900 WHERE id = 1;
-- 自动对这行加X锁,防止别人同时改
账户场景:UPDATE 流程
┌─────────────────────────────────────────┐
│ 一个UPDATE流程 │
├─────────────────────────────────────────┤
│ │
│ 1. 找到这行数据 │
│ ↓ │
│ 2. 对这行加排他锁(X锁) │
│ → 别人现在不能改这行了 │
│ ↓ │
│ 3. 读取最新版本(当前读) │
│ → 不能用快照,要取最新值 │
│ ↓ │
│ 4. 执行修改,生成新版本 │
│ → 旧版本挂到版本链上 │
│ ↓ │
│ 5. 提交事务,释放锁 │
│ → 别人可以读了 │
│ │
└─────────────────────────────────────────┘
读操作类型对比
| 操作 | 读什么版本 | 是否加锁 | 说明 |
|---|---|---|---|
SELECT(普通) |
快照版本 | 不加锁 | 保证可重复读 |
SELECT ... FOR UPDATE |
最新版本 | 加X锁 | 当前读,阻塞其他写 |
SELECT ... LOCK IN SHARE MODE |
最新版本 | 加S锁 | 当前读,允许其他读 |
UPDATE/DELETE |
最新版本 | 加X锁 | 避免丢失更新 |
INSERT |
最新版本 | 加X锁 | 检查唯一约束 |
一句话总结
MVCC处理读,锁处理写。读读不阻塞靠MVCC,写写互斥靠锁。
总结:ACID与MVCC的关系
ACID 是什么
ACID 是事务的四个特性,定义了事务应该达到的效果:
| 特性 | 解决的问题 |
|---|---|
| Atomicity | 操作不完整 |
| Consistency | 数据不合法 |
| Isolation | 并发冲突 |
| Durability | 数据丢失 |
MVCC 是什么
MVCC 是实现隔离性的一种技术手段,专门解决并发读写问题:
┌─────────────────────────────────────────┐
│ 隔离性(Isolation) │
│ (目标:并发事务互不干扰) │
└─────────────────────────────────────────┘
↑
实现方式
│
┌─────────┴─────────┐
↓ ↓
锁机制 MVCC
(阻塞等待) (多版本快照)
关系总结
| ACID | MVCC | |
|---|---|---|
| 层级 | 概念/目标 | 技术实现 |
| 关系 | 定义事务该做什么 | 实现事务怎么做 |
| 具体 | I(隔离性)是目标 | MVCC是实现I的手段之一 |
一句话: ACID 是数据库事务的设计思想,MVCC 是实现这个思想的具体技术。
最终目标
无论系统出现什么故障(崩溃、断电、并发冲突),数据库都能保持数据的正确性和可靠性。
| 特性 | 解决的问题 | 账户场景体现 |
|---|---|---|
| Atomicity | 操作不完整 | 转账不能只扣款不收款 |
| Consistency | 数据不合法 | 余额不能为负,总资金守恒 |
| Isolation | 并发冲突 | 同时转账不会算错余额 |
| Durability | 数据丢失 | 转账成功后断电不丢数据 |
| MVCC | 读写阻塞 | 查报表时转账不卡顿 |
| 锁 | 写写冲突 | 两个人同时改同一账户,排队执行 |

浙公网安备 33010602011771号