数据库:事务管理详解

数据库事务管理详解

深入理解数据库事务的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(两阶段提交):

  1. 准备阶段: 协调者询问所有参与者
  2. 提交阶段: 全部同意后提交

3PC(三阶段提交):

  1. CanCommit
  2. PreCommit
  3. DoCommit

TCC模式:

  • Try:尝试执行
  • Confirm:确认提交
  • Cancel:取消回滚

7.4 事务性能优化

优化策略:

  1. 减少事务范围
  2. 使用批量提交
  3. 选择合适隔离级别
  4. 避免长事务
  5. 使用连接池
  6. 合理设置锁超时

附录

附录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: 数据库会自动回滚,应用层需要重试逻辑。


注: 本文详细讲解数据库事务管理的核心概念和实战应用。

相关推荐
kangzerun2 小时前
SQLiteManager:一个优雅的Qt SQLite数据库操作类
数据库·qt·sqlite
troublea2 小时前
ThinkPHP6快速入门指南
数据库·mysql·缓存
数据知道2 小时前
MongoDB 元素查询运算符:使用 `$exists` 检查字段是否存在及处理缺失字段
数据库·mongodb
科技D人生2 小时前
PostgreSQL学习总结(17)—— PostgreSQL 插件大全:25款核心扩展解锁数据库全能力
数据库·postgresql·pgsql 插件·postgresql插件大全
志栋智能2 小时前
安全超自动化:从被动防御到主动响应的革命
运维·网络·数据库·人工智能·安全·web安全·自动化
数据知道2 小时前
MongoDB 批量写操作:`bulkWrite()` 在数据迁移与清洗中的高性能应用
数据库·mongodb
June`2 小时前
Redis缓存深度解析:20%数据应对80%请求
数据库·redis
阿寻寻2 小时前
【数据库】sql的update语句怎么使用?
数据库·sql
数据知道2 小时前
MongoDB 数组更新操作符:`$push`、`$pull`、`$addToSet` 管理列表数据
数据库·mongodb