事务ACID理解

数据库事务核心概念:ACID、隔离级别与MVCC

用账户余额和流水场景,理解事务的行为及不同情况下的数据影响

涵盖:ACID特性、MySQL隔离级别、MVCC机制、数据库锁

目录


场景设定

银行转账系统:

  • 账户表: id, balance(余额)
  • 流水表: id, from_account, to_account, amount, status

转账操作(A转100给B):

  1. A账户余额 -100
  2. B账户余额 +100
  3. 插入流水记录

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 读写阻塞 查报表时转账不卡顿
写写冲突 两个人同时改同一账户,排队执行
posted @ 2026-05-19 11:24  SeiunSky  阅读(16)  评论(0)    收藏  举报