数据库事务管理详解
深入理解数据库事务的ACID特性与并发控制
目录
第一章 事务基本概念
- 1.1 什么是事务
- 1.2 为什么需要事务
- 1.3 事务的应用场景
第二章 ACID特性详解
- 2.1 原子性(Atomicity)
- 2.2 一致性(Consistency)
- 2.3 隔离性(Isolation)
- 2.4 持久性(Durability)
- 2.5 ACID特性关系
第三章 事务的生命周期
- 3.1 事务状态
- 3.2 BEGIN TRANSACTION
- 3.3 COMMIT提交
- 3.4 ROLLBACK回滚
- 3.5 SAVEPOINT保存点
第四章 并发控制问题
- 4.1 丢失更新(Lost Update)
- 4.2 脏读(Dirty Read)
- 4.3 不可重复读(Non-Repeatable Read)
- 4.4 幻读(Phantom Read)
- 4.5 并发问题对比总结
第五章 事务隔离级别
- 5.1 READ UNCOMMITTED
- 5.2 READ COMMITTED
- 5.3 REPEATABLE READ
- 5.4 SERIALIZABLE
- 5.5 隔离级别选择指南
第六章 事务实现机制
- 6.1 日志机制(Undo/Redo Log)
- 6.2 锁机制
- 6.3 MVCC机制
- 6.4 检查点机制
第七章 实战案例
- 7.1 转账业务实现
- 7.2 订单系统事务
- 7.3 分布式事务简介
- 7.4 事务性能优化
附录
- 附录A:事务相关SQL命令
- 附录B:不同数据库事务对比
- 附录C:常见问题FAQ
第一章 事务基本概念
1.1 什么是事务
事务(Transaction)是数据库执行的最小工作单元,包含一组相关操作,这些操作要么全部成功,要么全部失败。
┌─────────────────────────────────────────────────────────────────┐
│ 事务的本质 │
├─────────────────────────────────────────────────────────────────┤
│ │
│ 定义:一个不可分割的工作单元 │
│ │
│ 生活类比:去银行办理转账业务 │
│ ┌────────────────────────────────────────┐ │
│ │ 步骤1:从账户A扣款1000元 │ │
│ │ 步骤2:向账户B加款1000元 │ │
│ └────────────────────────────────────────┘ │
│ │
│ 特点: │
│ • 两个步骤是一个整体 │
│ • 必须都成功,或者都不执行 │
│ • 不能只扣了A的钱,B却没收到 │
│ • All or Nothing(全有或全无) │
│ │
└─────────────────────────────────────────────────────────────────┘
事务示例:
sql
-- 转账事务
BEGIN TRANSACTION; -- 开始事务
-- 操作1:扣款
UPDATE account SET balance = balance - 1000
WHERE account_id = 'A';
-- 操作2:加款
UPDATE account SET balance = balance + 1000
WHERE account_id = 'B';
COMMIT; -- 提交事务(成功)
-- 或
-- ROLLBACK; -- 回滚事务(失败)
1.2 为什么需要事务
核心原因:
┌─────────────────────────────────────────────────────────────────┐
│ 事务解决的核心问题 │
├─────────────────────────────────────────────────────────────────┤
│ │
│ 1. 保证数据一致性 │
│ 问题:如果扣款成功但加款失败怎么办? │
│ 解决:事务保证要么都成功,要么都失败 │
│ │
│ 2. 应对系统故障 │
│ 问题:操作到一半系统崩溃怎么办? │
│ 解决:事务未提交的修改会全部撤销 │
│ │
│ 3. 隔离并发操作 │
│ 问题:多个用户同时操作同一数据怎么办? │
│ 解决:事务提供隔离机制,避免相互干扰 │
│ │
│ 4. 提供恢复能力 │
│ 问题:错误操作能否撤销? │
│ 解决:事务支持回滚,可以撤销错误 │
│ │
└─────────────────────────────────────────────────────────────────┘
没有事务的后果:
sql
-- 场景:转账操作没有事务保护
-- 步骤1:扣款成功
UPDATE account SET balance = balance - 1000 WHERE account_id = 'A';
-- 账户A余额:5000 → 4000 ✓
-- 此时发生:
-- ❌ 网络中断
-- ❌ 系统崩溃
-- ❌ SQL语法错误
-- 步骤2:加款失败(未执行)
UPDATE account SET balance = balance + 1000 WHERE account_id = 'B';
-- 账户B余额:3000 → 3000 (未变化)
-- 结果:灾难!
-- A账户少了1000元
-- B账户没有收到
-- 1000元凭空消失!
1.3 事务的应用场景
典型应用场景:
| 场景 | 操作 | 事务必要性 |
|---|---|---|
| 银行转账 | 扣款+加款 | ⭐⭐⭐⭐⭐ 必须 |
| 订单支付 | 扣库存+创建订单+扣款 | ⭐⭐⭐⭐⭐ 必须 |
| 用户注册 | 插入用户+发送邮件+创建初始数据 | ⭐⭐⭐⭐ 重要 |
| 商品秒杀 | 检查库存+扣库存+创建订单 | ⭐⭐⭐⭐⭐ 必须 |
| 删除用户 | 删除用户+删除关联数据 | ⭐⭐⭐⭐ 重要 |
| 批量导入 | 插入大量数据 | ⭐⭐⭐ 建议 |
场景详解:
sql
-- 场景1:电商订单支付
BEGIN;
-- 1. 扣减库存
UPDATE product SET stock = stock - 1
WHERE id = 100 AND stock > 0;
-- 2. 创建订单
INSERT INTO orders (user_id, product_id, amount)
VALUES (1001, 100, 299.00);
-- 3. 扣除账户余额
UPDATE account SET balance = balance - 299.00
WHERE user_id = 1001;
-- 4. 记录支付日志
INSERT INTO payment_log (order_id, amount, status)
VALUES (LAST_INSERT_ID(), 299.00, 'SUCCESS');
COMMIT; -- 全部成功才提交
-- 任何一步失败都会ROLLBACK,保证一致性
-- 场景2:用户注册
BEGIN;
-- 1. 创建用户账号
INSERT INTO users (username, password, email)
VALUES ('zhangsan', 'hash123', 'zhang@example.com');
SET @user_id = LAST_INSERT_ID();
-- 2. 创建用户资料
INSERT INTO user_profiles (user_id, nickname, avatar)
VALUES (@user_id, '张三', '/default.jpg');
-- 3. 分配初始积分
INSERT INTO user_points (user_id, points, reason)
VALUES (@user_id, 100, '注册赠送');
-- 4. 创建默认设置
INSERT INTO user_settings (user_id, theme, language)
VALUES (@user_id, 'light', 'zh-CN');
COMMIT;
第二章 ACID特性详解
ACID是事务必须满足的四个核心特性,是数据库正确性的基石。
┌─────────────────────────────────────────────────────────────────┐
│ ACID特性总览 │
├─────────────────────────────────────────────────────────────────┤
│ │
│ A - Atomicity 原子性 → 全有或全无 │
│ C - Consistency 一致性 → 符合约束 │
│ I - Isolation 隔离性 → 互不干扰 │
│ D - Durability 持久性 → 永久保存 │
│ │
│ 目标:保证数据库从一个一致性状态转换到另一个一致性状态 │
│ │
└─────────────────────────────────────────────────────────────────┘
2.1 原子性(Atomicity)
定义: 事务中的所有操作,要么全部完成,要么全部不完成,不会停留在中间某个状态。
┌─────────────────────────────────────────────────────────────────┐
│ 原子性详解 │
├─────────────────────────────────────────────────────────────────┤
│ │
│ 核心概念: │
│ • All or Nothing(要么全做,要么全不做) │
│ • 不允许部分成功 │
│ • 事务是最小执行单元 │
│ │
│ 类比: │
│ 就像化学反应: │
│ • 2H₂ + O₂ → 2H₂O │
│ • 要么反应完成,生成水 │
│ • 要么不反应,保持原状 │
│ • 不存在"半反应"状态 │
│ │
└─────────────────────────────────────────────────────────────────┘
原子性示例:
sql
-- 转账事务
BEGIN;
-- 操作1:扣款
UPDATE account SET balance = balance - 100 WHERE id = 1;
-- 执行成功 ✓
-- 操作2:加款(假设失败)
UPDATE account SET balance = balance + 100 WHERE id = 2;
-- 执行失败 ✗ (例如:账户2不存在)
ROLLBACK; -- 回滚
-- 结果:操作1也被撤销,账户1余额恢复原值
-- 保证原子性:全部失败
实现机制:
原子性通过Undo Log实现:
事务执行流程:
1. BEGIN - 开始事务
↓
2. 修改数据前,先记录旧值到Undo Log
account_id=1: balance 1000 → 900
【Undo Log记录:id=1, old_value=1000】
↓
3. 执行UPDATE
↓
4. 如果COMMIT:删除Undo Log
如果ROLLBACK:根据Undo Log恢复数据
2.2 一致性(Consistency)
定义: 事务执行前后,数据库从一个一致性状态转换到另一个一致性状态,不违反任何完整性约束。
┌─────────────────────────────────────────────────────────────────┐
│ 一致性详解 │
├─────────────────────────────────────────────────────────────────┤
│ │
│ 核心概念: │
│ • 数据库约束始终满足 │
│ • 业务规则始终成立 │
│ • 数据逻辑正确 │
│ │
│ 一致性约束包括: │
│ ✓ 主键约束(PRIMARY KEY) │
│ ✓ 外键约束(FOREIGN KEY) │
│ ✓ 唯一约束(UNIQUE) │
│ ✓ 非空约束(NOT NULL) │
│ ✓ 检查约束(CHECK) │
│ ✓ 业务规则(如:余额≥0) │
│ │
└─────────────────────────────────────────────────────────────────┘
一致性示例:
sql
-- 约束1:余额不能为负
CREATE TABLE account
(
id INT PRIMARY KEY,
balance DECIMAL(10,2) CHECK (balance >= 0) -- 检查约束
);
-- 转账事务
BEGIN;
-- 扣款
UPDATE account SET balance = balance - 1000 WHERE id = 1;
-- 如果导致balance < 0,违反约束,事务失败
-- 加款
UPDATE account SET balance = balance + 1000 WHERE id = 2;
COMMIT;
-- 只有满足所有约束才能提交
-- 示例2:总金额守恒(业务规则)
BEGIN;
-- 转账前总额
SELECT SUM(balance) FROM account; -- 结果:10000元
-- 执行转账
UPDATE account SET balance = balance - 500 WHERE id = 1;
UPDATE account SET balance = balance + 500 WHERE id = 2;
-- 转账后总额
SELECT SUM(balance) FROM account; -- 结果:仍然10000元
COMMIT;
-- 一致性:总金额守恒
2.3 隔离性(Isolation)
定义: 多个事务并发执行时,一个事务的执行不应影响其他事务的执行,每个事务都感觉自己在独占数据库。
┌─────────────────────────────────────────────────────────────────┐
│ 隔离性详解 │
├─────────────────────────────────────────────────────────────────┤
│ │
│ 核心概念: │
│ • 并发事务相互隔离 │
│ • 避免相互干扰 │
│ • 像串行执行一样 │
│ │
│ 类比: │
│ 就像考试: │
│ • 每个考生独立答题 │
│ • 互不影响 │
│ • 不能看别人答案 │
│ • 不能被别人干扰 │
│ │
│ 隔离级别(从低到高): │
│ 1. READ UNCOMMITTED - 读未提交 │
│ 2. READ COMMITTED - 读已提交 │
│ 3. REPEATABLE READ - 可重复读 │
│ 4. SERIALIZABLE - 串行化 │
│ │
└─────────────────────────────────────────────────────────────────┘
隔离性示例:
sql
-- 事务T1:查询余额
BEGIN;
SELECT balance FROM account WHERE id = 1; -- 读到:1000元
-- ... 业务处理5秒 ...
SELECT balance FROM account WHERE id = 1; -- 期望还是:1000元
COMMIT;
-- 事务T2:同时修改余额
BEGIN;
UPDATE account SET balance = balance - 100 WHERE id = 1;
COMMIT;
-- 根据隔离级别不同:
-- READ UNCOMMITTED: T1可能读到900(T2未提交的数据)
-- READ COMMITTED: T1第二次读可能是900(T2已提交)
-- REPEATABLE READ: T1两次读都是1000(可重复读)
-- SERIALIZABLE: T1和T2串行执行,完全隔离
2.4 持久性(Durability)
定义: 事务一旦提交,其对数据库的修改就是永久性的,即使系统崩溃也不会丢失。
┌─────────────────────────────────────────────────────────────────┐
│ 持久性详解 │
├─────────────────────────────────────────────────────────────────┤
│ │
│ 核心概念: │
│ • 提交即永久 │
│ • 崩溃可恢复 │
│ • 写入持久化存储 │
│ │
│ 类比: │
│ 就像合同签字: │
│ • 签字前:可以修改,撤销 │
│ • 签字后:具有法律效力,不可篡改 │
│ • 即使文件丢失,仍可从备份恢复 │
│ │
│ 保障措施: │
│ ✓ Redo Log(重做日志) │
│ ✓ 数据持久化到磁盘 │
│ ✓ Write-Ahead Logging(WAL) │
│ ✓ 定期检查点 │
│ │
└─────────────────────────────────────────────────────────────────┘
持久性示例:
sql
-- 转账事务
BEGIN;
UPDATE account SET balance = balance - 1000 WHERE id = 1;
UPDATE account SET balance = balance + 1000 WHERE id = 2;
COMMIT; -- ✓ 提交成功
-- ❌ 此时发生:
-- • 数据库服务器崩溃
-- • 操作系统重启
-- • 机房断电
-- ✓ 重启后:
-- 数据库通过Redo Log恢复
-- 已提交的修改仍然存在
-- balance被正确更新
实现机制:
持久性通过Redo Log实现:
提交流程:
1. 执行UPDATE
↓
2. 写Redo Log到磁盘(WAL原则)
【记录:将account_id=1的balance改为900】
↓
3. 返回COMMIT成功
↓
4. 异步刷新数据页到磁盘
崩溃恢复:
1. 数据库重启
↓
2. 读取Redo Log
↓
3. 重放已提交的事务
↓
4. 数据恢复完成
2.5 ACID特性关系
四大特性的关系:
┌─────────────────────────────────────────────────────────────────┐
│ ACID特性关系图 │
├─────────────────────────────────────────────────────────────────┤
│ │
│ 一致性(C) │
│ ↑ │
│ │ │
│ (最终目标) │
│ │ │
│ ┌────────────────┼────────────────┐ │
│ │ │ │ │
│ ↓ ↓ ↓ │
│ 原子性(A) 隔离性(I) 持久性(D) │
│ │ │ │ │
│ (手段) (手段) (手段) │
│ │ │ │ │
│ └────────────────┴────────────────┘ │
│ │ │
│ ↓ │
│ 数据库一致性 │
│ │
└─────────────────────────────────────────────────────────────────┘
特性对比:
| 特性 | 目标 | 实现机制 | 失败后果 |
|---|---|---|---|
| 原子性 | 操作完整性 | Undo Log | 部分修改 |
| 一致性 | 约束满足 | 约束检查+AID | 数据混乱 |
| 隔离性 | 并发正确性 | 锁/MVCC | 脏读/幻读 |
| 持久性 | 数据可靠性 | Redo Log | 数据丢失 |
相互依赖关系:
1. 一致性是目标
• 其他三个特性是实现一致性的手段
2. 原子性保证一致性
• 要么全做要么全不做
• 避免中间状态破坏一致性
3. 隔离性保证一致性
• 并发事务不相互干扰
• 避免并发破坏一致性
4. 持久性保证一致性
• 已提交的修改永久有效
• 避免崩溃破坏一致性
5. 没有隔离性,原子性无意义
• 单个事务原子,但并发仍可能冲突
6. 没有持久性,原子性和隔离性无意义
• 崩溃后数据丢失,前功尽弃
第三章 事务的生命周期
3.1 事务状态
事务在执行过程中会经历多个状态:
┌─────────────────────────────────────────────────────────────────┐
│ 事务状态转换图 │
├─────────────────────────────────────────────────────────────────┤
│ │
│ 开始 │
│ ↓ │
│ ┌──────────┐ │
│ │ 活动状态 │ (Active) │
│ │ 正在执行 │ │
│ └──────────┘ │
│ ↓ │
│ ┌───────────┴───────────┐ │
│ ↓ ↓ │
│ ┌──────────┐ ┌──────────┐ │
│ │ 部分提交 │ │ 失败状态 │ │
│ │(Partially │ │ (Failed) │ │
│ │Committed)│ └──────────┘ │
│ └──────────┘ ↓ │
│ ↓ ↓ │
│ ↓ ┌──────────┐ │
│ ↓ │ 中止状态 │ │
│ ↓ │(Aborted) │ │
│ ↓ └──────────┘ │
│ ┌──────────┐ │
│ │ 提交状态 │ │
│ │(Committed)│ │
│ └──────────┘ │
│ │
└─────────────────────────────────────────────────────────────────┘
状态说明:
| 状态 | 说明 | 特点 |
|---|---|---|
| 活动(Active) | 事务正在执行 | 可读写数据 |
| 部分提交 | 最后一条语句执行完 | 等待写入磁盘 |
| 提交(Committed) | 事务成功完成 | 修改永久生效 |
| 失败(Failed) | 事务无法继续执行 | 需要回滚 |
| 中止(Aborted) | 事务回滚完成 | 恢复到初始状态 |
3.2 BEGIN TRANSACTION
作用: 显式开启一个新事务,标记事务的起点。
sql
-- MySQL
START TRANSACTION; -- 或 BEGIN;
SET autocommit = 0; -- 关闭自动提交
-- PostgreSQL
BEGIN;
BEGIN TRANSACTION;
BEGIN TRANSACTION ISOLATION LEVEL READ COMMITTED;
-- SQL Server
BEGIN TRANSACTION;
BEGIN TRAN;
-- Oracle (默认就是事务模式)
SET TRANSACTION READ WRITE;
BEGIN选项:
sql
-- 指定隔离级别
BEGIN TRANSACTION ISOLATION LEVEL REPEATABLE READ;
-- 只读事务
BEGIN TRANSACTION READ ONLY;
-- 读写事务
BEGIN TRANSACTION READ WRITE;
-- 命名事务
BEGIN TRANSACTION transfer_money;
3.3 COMMIT提交
作用: 提交事务,使所有修改永久生效。
┌─────────────────────────────────────────────────────────────────┐
│ COMMIT执行流程 │
├─────────────────────────────────────────────────────────────────┤
│ │
│ 1. 执行COMMIT命令 │
│ ↓ │
│ 2. 将Redo Log写入磁盘(WAL) │
│ ↓ │
│ 3. 释放事务持有的锁 │
│ ↓ │
│ 4. 标记事务为已提交 │
│ ↓ │
│ 5. 返回成功 │
│ ↓ │
│ 6. 异步刷新数据页到磁盘 │
│ │
└─────────────────────────────────────────────────────────────────┘
COMMIT示例:
sql
-- 基本提交
BEGIN;
UPDATE account SET balance = balance - 100 WHERE id = 1;
UPDATE account SET balance = balance + 100 WHERE id = 2;
COMMIT; -- ✓ 提交成功
-- 带错误处理
BEGIN;
DECLARE EXIT HANDLER FOR SQLEXCEPTION
BEGIN
ROLLBACK;
SELECT 'Transaction rollback due to error';
END;
UPDATE account SET balance = balance - 100 WHERE id = 1;
UPDATE account SET balance = balance + 100 WHERE id = 2;
COMMIT;
3.4 ROLLBACK回滚
作用: 撤销事务的所有修改,恢复到事务开始前的状态。
┌─────────────────────────────────────────────────────────────────┐
│ ROLLBACK执行流程 │
├─────────────────────────────────────────────────────────────────┤
│ │
│ 1. 执行ROLLBACK命令 │
│ ↓ │
│ 2. 读取Undo Log │
│ ↓ │
│ 3. 根据Undo Log逐条撤销修改 │
│ ↓ │
│ 4. 释放事务持有的锁 │
│ ↓ │
│ 5. 删除Undo Log │
│ ↓ │
│ 6. 标记事务为已中止 │
│ │
└─────────────────────────────────────────────────────────────────┘
ROLLBACK示例:
sql
-- 主动回滚
BEGIN;
UPDATE account SET balance = balance - 100 WHERE id = 1;
-- 发现余额不足
IF (SELECT balance FROM account WHERE id = 1) < 0 THEN
ROLLBACK; -- 回滚
ELSE
UPDATE account SET balance = balance + 100 WHERE id = 2;
COMMIT;
END IF;
-- 错误自动回滚
BEGIN;
UPDATE account SET balance = balance - 1000 WHERE id = 1;
UPDATE account SET balance = balance + 1000 WHERE id = 999; -- id不存在
-- 错误发生,自动ROLLBACK
-- 超时回滚
BEGIN;
SET innodb_lock_wait_timeout = 5;
SELECT * FROM account WHERE id = 1 FOR UPDATE; -- 等待锁
-- 5秒后超时,自动ROLLBACK
3.5 SAVEPOINT保存点
作用: 在事务中设置保存点,可以部分回滚到指定保存点。
┌─────────────────────────────────────────────────────────────────┐
│ SAVEPOINT原理 │
├─────────────────────────────────────────────────────────────────┤
│ │
│ BEGIN │
│ ↓ │
│ 操作1: INSERT INTO t1 ... │
│ ↓ │
│ SAVEPOINT sp1 ← 保存点1 │
│ ↓ │
│ 操作2: UPDATE t2 ... │
│ ↓ │
│ SAVEPOINT sp2 ← 保存点2 │
│ ↓ │
│ 操作3: DELETE FROM t3 ... ← 发现错误 │
│ ↓ │
│ ROLLBACK TO sp2 ← 只回滚操作3 │
│ ↓ │
│ 操作4: DELETE FROM t3 WHERE ... ← 重新执行 │
│ ↓ │
│ COMMIT ← 提交操作1,2,4 │
│ │
└─────────────────────────────────────────────────────────────────┘
SAVEPOINT示例:
sql
-- 示例1:批量插入with回滚点
BEGIN;
-- 插入用户
INSERT INTO users (name, email) VALUES ('张三', 'zhang@example.com');
SAVEPOINT after_user;
-- 插入订单
INSERT INTO orders (user_id, amount) VALUES (1, 100.00);
SAVEPOINT after_order;
-- 插入支付记录(失败)
INSERT INTO payments (order_id, amount) VALUES (999, 100.00); -- 外键错误
-- 回滚到订单之后
ROLLBACK TO after_order;
-- 重新插入正确的支付记录
INSERT INTO payments (order_id, amount) VALUES (1, 100.00);
COMMIT; -- 用户和支付都成功
-- 示例2:嵌套保存点
BEGIN;
UPDATE account SET balance = balance - 1000 WHERE id = 1;
SAVEPOINT sp1;
UPDATE account SET balance = balance + 500 WHERE id = 2;
SAVEPOINT sp2;
UPDATE account SET balance = balance + 500 WHERE id = 3;
SAVEPOINT sp3;
-- 发现id=3余额超限
ROLLBACK TO sp2; -- 回滚id=3的修改
UPDATE account SET balance = balance + 1000 WHERE id = 4; -- 重新分配
COMMIT;
-- 示例3:释放保存点
BEGIN;
INSERT INTO log (message) VALUES ('step1');
SAVEPOINT sp1;
INSERT INTO log (message) VALUES ('step2');
RELEASE SAVEPOINT sp1; -- 释放保存点,不能再回滚到sp1
COMMIT;
第四章 并发控制问题
多个事务并发执行时,如果没有适当的并发控制,会产生以下四类经典问题。
4.1 丢失更新(Lost Update)
定义: 两个或多个事务读取同一数据并进行修改,后提交的事务覆盖了先提交的事务的修改。
严重性: ⭐⭐⭐⭐⭐ (最严重)
┌─────────────────────────────────────────────────────────────────┐
│ 丢失更新示意图 │
├─────────────────────────────────────────────────────────────────┤
│ │
│ 时间 事务T1 事务T2 数据库 │
│ ──── ───────────────────── ─────────────────── ────── │
│ │
│ t1 BEGIN stock=100 │
│ t2 READ stock (100) stock=100 │
│ t3 BEGIN stock=100 │
│ t4 READ stock (100) stock=100 │
│ t5 计算: 100-50=50 stock=100 │
│ t6 计算: 100-30=70 stock=100 │
│ t7 UPDATE stock=50 stock=50 │
│ t8 COMMIT stock=50 │
│ t9 UPDATE stock=70 stock=70 │
│ t10 COMMIT stock=70 │
│ │
│ 结果: T1的修改丢失! 实际卖出80件,库存却是70 │
│ │
└─────────────────────────────────────────────────────────────────┘
丢失更新示例:
sql
-- 场景:库存扣减
-- 初始: product.stock = 100
-- 事务T1: 订单A购买50件
BEGIN;
SELECT stock FROM product WHERE id=1; -- 读到 100
-- 业务处理...
UPDATE product SET stock=50 WHERE id=1; -- 100-50=50
COMMIT;
-- 事务T2: 订单B购买30件(同时执行)
BEGIN;
SELECT stock FROM product WHERE id=1; -- 也读到 100
-- 业务处理...
UPDATE product SET stock=70 WHERE id=1; -- 100-30=70,覆盖了T1!
COMMIT;
-- ❌ 结果: stock=70,但实际卖出80件!
解决方案:
sql
-- 方案1: 使用FOR UPDATE加排他锁
BEGIN;
SELECT stock FROM product WHERE id=1 FOR UPDATE; -- 加X锁
UPDATE product SET stock = stock - 50 WHERE id=1;
COMMIT;
-- 方案2: 使用原子操作
UPDATE product SET stock = stock - 50
WHERE id=1 AND stock >= 50;
-- 方案3: 乐观锁(版本号)
UPDATE product SET stock = stock - 50, version = version + 1
WHERE id=1 AND version = @old_version;
4.2 脏读(Dirty Read)
定义: 一个事务读取了另一个未提交事务修改的数据,如果该事务回滚,则读取的数据是无效的"脏数据"。
严重性: ⭐⭐⭐⭐
┌─────────────────────────────────────────────────────────────────┐
│ 脏读示意图 │
├─────────────────────────────────────────────────────────────────┤
│ │
│ 时间 事务T1 事务T2 数据库 │
│ ──── ───────────────────── ─────────────────── ────── │
│ │
│ t1 BEGIN balance=1000│
│ t2 UPDATE balance=500 balance=500 │
│ t3 BEGIN balance=500 │
│ t4 READ balance (500) balance=500 │
│ t5 允许消费400元 balance=500 │
│ t6 发现错误 balance=500 │
│ t7 ROLLBACK balance=1000│
│ t8 UPDATE balance=100 balance=100 │
│ t9 COMMIT balance=100 │
│ │
│ 结果: T2基于脏数据(500)做决策,实际余额是1000 │
│ 导致账户变成负数100! │
│ │
└─────────────────────────────────────────────────────────────────┘
脏读示例:
sql
-- 事务T1: 转账(会失败)
BEGIN;
UPDATE account SET balance=500 WHERE id='A'; -- A减少500
UPDATE account SET balance=1500 WHERE id='B'; -- B增加500
-- B账户现在是1500
-- 准备提交时发现问题...
-- 事务T2: 查询B的余额(同时执行)
SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED; -- 允许脏读
BEGIN;
SELECT balance FROM account WHERE id='B'; -- 读到 1500 (脏数据!)
-- 认为B有1500元,允许B消费1200元
UPDATE account SET balance=300 WHERE id='B';
COMMIT;
-- 事务T1: 回滚
ROLLBACK; -- A恢复1000, B恢复1000
-- ❌ 结果: B账户最终余额=-200 (负数!)
-- 因为T2基于脏数据1500做决策,实际B只有1000
解决方案:
sql
-- 使用READ COMMITTED或更高隔离级别
SET TRANSACTION ISOLATION LEVEL READ COMMITTED;
BEGIN;
SELECT balance FROM account WHERE id='B'; -- 只读已提交的数据
COMMIT;
4.3 不可重复读(Non-Repeatable Read)
定义: 在同一事务中,多次读取同一数据,但得到的结果不同,原因是其他事务修改了该数据并提交。
严重性: ⭐⭐⭐
┌─────────────────────────────────────────────────────────────────┐
│ 不可重复读示意图 │
├─────────────────────────────────────────────────────────────────┤
│ │
│ 时间 事务T1 事务T2 数据库 │
│ ──── ───────────────────── ─────────────────── ────── │
│ │
│ t1 BEGIN balance=1000│
│ t2 READ balance (1000) balance=1000│
│ t3 检查余额足够 balance=1000│
│ t4 BEGIN balance=1000│
│ t5 UPDATE balance=200 balance=200 │
│ t6 COMMIT balance=200 │
│ t7 READ balance (200) ← 变了! balance=200 │
│ t8 准备扣款500元 balance=200 │
│ t9 COMMIT(失败,余额不足) balance=200 │
│ │
│ 结果: T1两次读取结果不同,业务逻辑混乱 │
│ │
└─────────────────────────────────────────────────────────────────┘
不可重复读示例:
sql
-- 事务T1: 检查并转账
SET TRANSACTION ISOLATION LEVEL READ COMMITTED;
BEGIN;
-- 第一次查询
SELECT balance FROM account WHERE id=1; -- 结果: 1000元
IF balance >= 500 THEN
-- 余额足够,准备转账
-- ... 业务处理5秒 ...
END IF;
-- 事务T2: 其他地方扣款(同时执行)
BEGIN;
UPDATE account SET balance=200 WHERE id=1; -- 扣款800元
COMMIT;
-- 事务T1: 第二次查询(再次确认)
SELECT balance FROM account WHERE id=1; -- 结果: 200元!变了!
-- ❌ 业务逻辑混乱,之前判断余额够,现在不够了
UPDATE account SET balance=balance-500 WHERE id=1; -- 失败
ROLLBACK;
解决方案:
sql
-- 使用REPEATABLE READ隔离级别
SET TRANSACTION ISOLATION LEVEL REPEATABLE READ;
BEGIN;
SELECT balance FROM account WHERE id=1; -- 1000元
-- 其他事务修改并提交
SELECT balance FROM account WHERE id=1; -- 仍然是1000元(可重复读)
COMMIT;
4.4 幻读(Phantom Read)
定义: 在同一事务中,两次执行相同的范围查询 ,得到的记录数量不同,原因是其他事务插入或删除了符合条件的新记录。
严重性: ⭐⭐
┌─────────────────────────────────────────────────────────────────┐
│ 幻读示意图 │
├─────────────────────────────────────────────────────────────────┤
│ │
│ 时间 事务T1 事务T2 数据库 │
│ ──── ───────────────────── ─────────────────── ────── │
│ │
│ t1 BEGIN 3条记录 │
│ t2 SELECT COUNT(*) (3条) 3条记录 │
│ t3 SELECT SUM(salary) (15000) 3条记录 │
│ t4 BEGIN 3条记录 │
│ t5 INSERT 2条新员工 5条记录 │
│ t6 COMMIT 5条记录 │
│ t7 SELECT COUNT(*) (5条) ← 多了! 5条记录 │
│ t8 SELECT SUM(salary) (25000) 5条记录 │
│ t9 数据不一致 5条记录 │
│ │
│ 结果: T1前后查询记录数不同,出现"幻影"记录 │
│ │
└─────────────────────────────────────────────────────────────────┘
幻读示例:
sql
-- 事务T1: 统计部门工资
SET TRANSACTION ISOLATION LEVEL REPEATABLE READ;
BEGIN;
-- 第一次统计
SELECT COUNT(*) FROM employee WHERE dept='销售部'; -- 结果: 3人
SELECT SUM(salary) FROM employee WHERE dept='销售部'; -- 总额: 18000
-- 事务T2: 新员工入职(同时执行)
BEGIN;
INSERT INTO employee (name, dept, salary) VALUES ('张三', '销售部', 5000);
INSERT INTO employee (name, dept, salary) VALUES ('李四', '销售部', 5000);
COMMIT;
-- 事务T1: 第二次统计(验证)
SELECT COUNT(*) FROM employee WHERE dept='销售部'; -- 结果: 5人!多了!
SELECT SUM(salary) FROM employee WHERE dept='销售部'; -- 总额: 28000!
-- ❌ 幻读: 凭空出现了2条记录
COMMIT;
解决方案:
sql
-- 方案1: 使用SERIALIZABLE隔离级别
SET TRANSACTION ISOLATION LEVEL SERIALIZABLE;
BEGIN;
SELECT COUNT(*) FROM employee WHERE dept='销售部';
-- 其他事务无法INSERT新记录
SELECT COUNT(*) FROM employee WHERE dept='销售部'; -- 结果一致
COMMIT;
-- 方案2: MySQL使用Next-Key Lock(默认在REPEATABLE READ下)
-- InnoDB会锁定索引范围,防止插入
BEGIN;
SELECT * FROM employee WHERE dept='销售部' FOR UPDATE;
-- 锁定范围,防止INSERT
COMMIT;
4.5 并发问题对比总结
四大问题对比表:
| 问题 | 影响对象 | 触发操作 | 严重性 | 现象 |
|---|---|---|---|---|
| 丢失更新 | 单条记录的值 | UPDATE | ⭐⭐⭐⭐⭐ | 修改被覆盖 |
| 脏读 | 未提交的数据 | UPDATE+ROLLBACK | ⭐⭐⭐⭐ | 读到脏数据 |
| 不可重复读 | 已有记录的值 | UPDATE | ⭐⭐⭐ | 多次读不一致 |
| 幻读 | 记录总数 | INSERT/DELETE | ⭐⭐ | 记录数变化 |
关键区别:
不可重复读 vs 幻读:
┌─────────────────────────────────────────────────────────────┐
│ 维度 │ 不可重复读 │ 幻读 │
├─────────────────────────────────────────────────────────────┤
│ 变化内容 │ 已有记录的值改变 │ 记录总数改变 │
│ 触发操作 │ UPDATE/DELETE │ INSERT │
│ 影响范围 │ 单条记录 │ 查询结果集 │
│ 锁定方式 │ 行锁可解决 │ 需要间隙锁/表锁 │
│ 示例 │ 余额1000→200 │ 3条记录→5条记录 │
└─────────────────────────────────────────────────────────────┘
各隔离级别能防止的问题:
| 隔离级别 | 丢失更新 | 脏读 | 不可重复读 | 幻读 |
|---|---|---|---|---|
| READ UNCOMMITTED | ✗ | ✗ | ✗ | ✗ |
| READ COMMITTED | ✓ | ✓ | ✗ | ✗ |
| REPEATABLE READ | ✓ | ✓ | ✓ | ✗(MySQL可防) |
| SERIALIZABLE | ✓ | ✓ | ✓ | ✓ |
第五章 事务隔离级别
事务隔离级别定义了事务之间的隔离程度,级别从低到高,并发性递减,一致性递增。
5.1 READ UNCOMMITTED
定义: 最低的隔离级别,允许读取未提交的数据。
┌─────────────────────────────────────────────────────────────────┐
│ READ UNCOMMITTED(读未提交) │
├─────────────────────────────────────────────────────────────────┤
│ │
│ 特点: │
│ • 不加任何锁,性能最高 │
│ • 可能读到脏数据 │
│ • 几乎不用于生产环境 │
│ │
│ 能防止的问题: │
│ ❌ 丢失更新 ❌ 脏读 ❌ 不可重复读 ❌ 幻读 │
│ │
│ 使用场景: │
│ • 对数据准确性要求极低 │
│ • 仅用于统计分析(近似值) │
│ │
└─────────────────────────────────────────────────────────────────┘
示例:
sql
-- 设置隔离级别
SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;
BEGIN;
SELECT balance FROM account WHERE id=1; -- 可能读到未提交的数据
COMMIT;
5.2 READ COMMITTED
定义: 只能读取已提交的数据,避免脏读。
┌─────────────────────────────────────────────────────────────────┐
│ READ COMMITTED(读已提交) │
├─────────────────────────────────────────────────────────────────┤
│ │
│ 特点: │
│ • 读取时加S锁,读完立即释放 │
│ • 写入时加X锁,事务结束才释放 │
│ • Oracle/PostgreSQL默认级别 │
│ │
│ 能防止的问题: │
│ ✓ 丢失更新 ✓ 脏读 ❌ 不可重复读 ❌ 幻读 │
│ │
│ 使用场景: │
│ • 大多数OLTP系统 │
│ • 允许不可重复读的场景 │
│ │
└─────────────────────────────────────────────────────────────────┘
示例:
sql
SET TRANSACTION ISOLATION LEVEL READ COMMITTED;
BEGIN;
SELECT balance FROM account WHERE id=1; -- 1000元
-- 其他事务修改并提交
SELECT balance FROM account WHERE id=1; -- 可能是900元(不可重复读)
COMMIT;
5.3 REPEATABLE READ
定义: 同一事务中多次读取相同数据结果一致。
┌─────────────────────────────────────────────────────────────────┐
│ REPEATABLE READ(可重复读) │
├─────────────────────────────────────────────────────────────────┤
│ │
│ 特点: │
│ • 事务开始时创建快照 │
│ • 读取的数据加S锁,事务结束才释放 │
│ • MySQL InnoDB默认级别 │
│ │
│ 能防止的问题: │
│ ✓ 丢失更新 ✓ 脏读 ✓ 不可重复读 ❌ 幻读† │
│ (†MySQL通过Next-Key Lock可防止幻读) │
│ │
│ 使用场景: │
│ • 需要数据一致性的分析查询 │
│ • 需要可重复读的报表生成 │
│ │
└─────────────────────────────────────────────────────────────────┘
示例:
sql
SET TRANSACTION ISOLATION LEVEL REPEATABLE READ;
BEGIN;
SELECT balance FROM account WHERE id=1; -- 1000元
-- 其他事务修改并提交
SELECT balance FROM account WHERE id=1; -- 仍然是1000元(可重复读)
COMMIT;
5.4 SERIALIZABLE
定义: 最高的隔离级别,事务串行执行,完全隔离。
┌─────────────────────────────────────────────────────────────────┐
│ SERIALIZABLE(串行化) │
├─────────────────────────────────────────────────────────────────┤
│ │
│ 特点: │
│ • 读取加表级S锁,写入加表级X锁 │
│ • 完全消除并发,性能最低 │
│ • 数据一致性最强 │
│ │
│ 能防止的问题: │
│ ✓ 丢失更新 ✓ 脏读 ✓ 不可重复读 ✓ 幻读 │
│ │
│ 使用场景: │
│ • 金融交易系统 │
│ • 对一致性要求极高的关键业务 │
│ │
└─────────────────────────────────────────────────────────────────┘
示例:
sql
SET TRANSACTION ISOLATION LEVEL SERIALIZABLE;
BEGIN;
SELECT * FROM account WHERE dept='财务部'; -- 锁定整个表
-- 其他事务无法读写account表
COMMIT;
5.5 隔离级别选择指南
选择决策树:
是否需要高并发?
│
└────────┼────────┐
↓ ↓
是 否
↓ ↓
能容忍脏读? 需要极高一致性?
│ │
是│否 是│否
│ │
↓ ↓
RU RC/RR SERIALIZABLE
具体建议:
| 场景 | 推荐级别 | 理由 |
|---|---|---|
| 普通OLTP | READ COMMITTED | 平衡性能和一致性 |
| 报表查询 | REPEATABLE READ | 保证数据一致性 |
| 一般统计 | READ UNCOMMITTED | 允许误差,追求性能 |
| 金融交易 | SERIALIZABLE | 零容忍错误 |
| 库存系统 | REPEATABLE READ | 防止丢失更新 |
| 电商订单 | REPEATABLE READ | 防止幻读 |
性能对比:
性能 │██████████│ RU (最快)
│████████ │ RC
│██████ │ RR
│██ │ SERIALIZABLE (最慢)
└──────────┘
一致性 │██ │ RU (最弱)
│██████ │ RC
│████████ │ RR
│██████████│ SERIALIZABLE (最强)
└──────────┘
第六章 事务实现机制
6.1 日志机制(Undo/Redo Log)
Undo Log(回滚日志):
- 用途:实现原子性,支持事务回滚
- 记录:修改前的旧值
- 时机:修改数据前写入
Redo Log(重做日志):
- 用途:实现持久性,崩溃恢复
- 记录:修改后的新值
- 时机:COMMIT前写入磁盘(WAL)
6.2 锁机制
两种基本锁:
- S锁(共享锁): 读者之间不互斥
- X锁(排他锁): 独占访问
锁粒度:
- 行锁:高并发,开销大
- 表锁:低开销,低并发
6.3 MVCC机制
多版本并发控制(MVCC):
- 原理:为每个事务创建数据快照
- 优势:读不加锁,读写不冲突
- 实现:PostgreSQL/MySQL InnoDB
6.4 检查点机制
Checkpoint:
- 作用:定期将内存数据刷新到磁盘
- 目的:减少崩溃恢复时间
- 时机:周期性/日志满/手动触发
第七章 实战案例
7.1 转账业务实现
sql
-- 完整的转账事务
DELIMITER $$
CREATE PROCEDURE transfer_money(
IN from_account INT,
IN to_account INT,
IN amount DECIMAL(10,2)
)
BEGIN
DECLARE EXIT HANDLER FOR SQLEXCEPTION
BEGIN
ROLLBACK;
SELECT '转账失败' AS result;
END;
START TRANSACTION;
-- 1. 检查余额
IF (SELECT balance FROM account WHERE id=from_account FOR UPDATE) < amount THEN
SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = '余额不足';
END IF;
-- 2. 扣款
UPDATE account SET balance = balance - amount WHERE id = from_account;
-- 3. 加款
UPDATE account SET balance = balance + amount WHERE id = to_account;
-- 4. 记录日志
INSERT INTO transfer_log (from_id, to_id, amount, transfer_time)
VALUES (from_account, to_account, amount, NOW());
COMMIT;
SELECT '转账成功' AS result;
END$$
DELIMITER ;
7.2 订单系统事务
sql
-- 电商订单事务
BEGIN;
-- 1. 扣库存(乐观锁)
UPDATE product
SET stock = stock - 1, version = version + 1
WHERE id = 100 AND stock > 0 AND version = @old_version;
IF ROW_COUNT() = 0 THEN
ROLLBACK;
SELECT '库存不足' AS error;
END IF;
-- 2. 创建订单
INSERT INTO orders (user_id, product_id, amount, status)
VALUES (1001, 100, 299.00, 'PENDING');
SET @order_id = LAST_INSERT_ID();
-- 3. 创建支付记录
INSERT INTO payments (order_id, user_id, amount, status)
VALUES (@order_id, 1001, 299.00, 'SUCCESS');
COMMIT;
7.3 分布式事务简介
2PC(两阶段提交):
- 准备阶段: 协调者询问所有参与者
- 提交阶段: 全部同意后提交
3PC(三阶段提交):
- CanCommit
- PreCommit
- DoCommit
TCC模式:
- Try:尝试执行
- Confirm:确认提交
- Cancel:取消回滚
7.4 事务性能优化
优化策略:
- 减少事务范围
- 使用批量提交
- 选择合适隔离级别
- 避免长事务
- 使用连接池
- 合理设置锁超时
附录
附录A:事务相关SQL命令
MySQL:
sql
START TRANSACTION;
BEGIN;
COMMIT;
ROLLBACK;
SAVEPOINT sp_name;
ROLLBACK TO sp_name;
SET TRANSACTION ISOLATION LEVEL READ COMMITTED;
PostgreSQL:
sql
BEGIN;
COMMIT;
ROLLBACK;
SAVEPOINT sp_name;
SET TRANSACTION ISOLATION LEVEL SERIALIZABLE;
附录B:不同数据库事务对比
| 特性 | MySQL InnoDB | PostgreSQL | Oracle | SQL Server |
|---|---|---|---|---|
| 默认隔离级别 | RR | RC | RC | RC |
| MVCC | ✓ | ✓ | ✓ | ✓(行版本) |
| 行级锁 | ✓ | ✓ | ✓ | ✓ |
| 自动提交 | 默认开 | 默认关 | 默认关 | 默认开 |
附录C:常见问题FAQ
Q1: 怎么选择隔离级别?
A: 根据业务对一致性和性能的要求。OLTP系统用RC,报表系统用RR。
Q2: MVCC和锁有什么区别?
A: MVCC通过多版本实现读写不冲突,锁通过阻塞实现互斥。
Q3: 长事务如何优化?
A: 拆分成小事务,使用批量处理,避免长时间持有锁。
Q4: 什么时候会发生死锁?
A: 多个事务相互等待对方持有的锁时。解决:设置超时,按顺序加锁。
Q5: 事务提交失败怎么办?
A: 数据库会自动回滚,应用层需要重试逻辑。
注: 本文详细讲解数据库事务管理的核心概念和实战应用。