数据库:并发控制基本概念

数据库并发控制详解

本文详细介绍数据库并发问题与并发控制的基本概念,包括并发问题、事务管理、封锁协议和死锁处理等核心技术。


目录

  1. [第一章 并发控制概述](#第一章 并发控制概述)
  2. [第二章 并发操作带来的问题](#第二章 并发操作带来的问题)
  3. [第三章 事务的基本概念](#第三章 事务的基本概念)
  4. [第四章 封锁协议概述](#第四章 封锁协议概述)
  5. [第五章 死锁问题概述](#第五章 死锁问题概述)
  6. [第六章 并发控制技术总览](#第六章 并发控制技术总览)
  7. [第七章 面试与考试重点](#第七章 面试与考试重点)
  8. 附录

第一章 并发控制概述

1.1 什么是数据库并发控制

数据库并发控制(Concurrency Control) 是指当多个用户或程序同时访问和操作数据库时,通过一定的机制来协调这些操作,确保数据的一致性和正确性。

复制代码
┌─────────────────────────────────────────────────────────────────┐
│                    并发控制的本质                                │
├─────────────────────────────────────────────────────────────────┤
│                                                                 │
│  核心思想:让多个操作"看起来"像是串行执行的                     │
│                                                                 │
│  生活类比:                                                     │
│  • 多人同时在银行ATM取款                                        │
│  • 每个人都要确保账户余额不会出错                               │
│  • 不能同时取款导致余额变成负数                                 │
│                                                                 │
│  技术实现:                                                     │
│  • 通过锁、时间戳等机制                                         │
│  • 控制事务的执行顺序                                           │
│  • 保证数据的正确性                                             │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘

并发 vs 并行:

复制代码
并发(Concurrency):多个任务交替执行
┌───────────────────────────────────┐
│ 时间轴                            │
├───────────────────────────────────┤
│ T1: ──█──  ──█──  ──█──           │
│ T2:   ──█──  ──█──  ──█──         │
│ T3:     ──█──  ──█──  ──█──       │
└───────────────────────────────────┘
单核CPU,快速切换,看起来同时执行

并行(Parallelism):多个任务真正同时执行
┌───────────────────────────────────┐
│ 时间轴                            │
├───────────────────────────────────┤
│ T1: ─────────────█                │
│ T2: ─────────────█                │
│ T3: ─────────────█                │
└───────────────────────────────────┘
多核CPU,真正的同时运行

数据库中的并发场景:

场景 示例 并发操作数
低并发 小型企业ERP 10-100
中并发 电商网站 1000-10000
高并发 抢购秒杀 10万+
超高并发 双十一 百万+

1.2 为什么需要并发控制

问题的根源:资源共享

在单用户环境下,数据库操作是串行的,不会产生冲突。但在多用户环境下,如果不加控制,会导致数据混乱。

典型问题示例:银行转账

复制代码
场景:账户A余额1000元,两个人同时取款500元

没有并发控制的情况:

时刻    用户1                用户2                账户A余额
T1      读取余额(1000)       -                    1000
T2      检查余额够           -                    1000
T3      -                   读取余额(1000)        1000
T4      -                   检查余额够            1000
T5      扣款500             -                    500
T6      -                   扣款500              0
T7      写回余额500         -                    500
T8      -                   写回余额500          500

问题:应该剩余0元,实际剩余500元!(丢失了一次修改)

有并发控制的情况:

复制代码
时刻    用户1                用户2                账户A余额
T1      读取余额(1000)       -                    1000
T2      加锁                 -                    1000
T3      检查余额够           -                    1000
T4      -                   尝试读取(等待...)    1000
T5      扣款500             -                    500
T6      写回余额500         -                    500
T7      释放锁               -                    500
T8      -                   获得锁,读取(500)    500
T9      -                   检查余额够            500
T10     -                   扣款500              0
T11     -                   写回余额0            0
T12     -                   释放锁                0

结果:正确!剩余0元

为什么必须进行并发控制:

复制代码
┌─────────────────────────────────────────────────────────────────┐
│                    并发控制的必要性                              │
├─────────────────────────────────────────────────────────────────┤
│                                                                 │
│  1. 保证数据正确性                                              │
│     • 避免数据丢失                                              │
│     • 避免数据不一致                                            │
│                                                                 │
│  2. 满足业务需求                                                │
│     • 多用户同时使用系统                                        │
│     • 提高系统吞吐量                                            │
│                                                                 │
│  3. 符合ACID要求                                                │
│     • 隔离性(Isolation)必须有并发控制支撑                     │
│     • 一致性(Consistency)需要并发控制保障                     │
│                                                                 │
│  4. 提升用户体验                                                │
│     • 减少等待时间                                              │
│     • 提高响应速度                                              │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘

1.3 并发控制的目标

并发控制要在正确性性能之间找到平衡。

复制代码
┌─────────────────────────────────────────────────────────────────┐
│                    并发控制的三大目标                            │
├─────────────────────────────────────────────────────────────────┤
│                                                                 │
│  1. 保证数据一致性(Consistency)                               │
│     • 并发操作后,数据库仍处于一致状态                          │
│     • 不违反完整性约束                                          │
│     • 示例:转账前后总金额不变                                  │
│                                                                 │
│  2. 保证事务隔离性(Isolation)                                 │
│     • 一个事务的执行不受其他事务干扰                            │
│     • 多个事务并发执行的结果等同于某种串行执行结果              │
│     • 示例:A转账时,B看不到中间状态                            │
│                                                                 │
│  3. 提高系统并发度(Concurrency)                               │
│     • 尽可能多的事务同时执行                                    │
│     • 减少等待时间                                              │
│     • 提高资源利用率                                            │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘

并发控制的权衡:

复制代码
严格控制(强一致性)         vs         宽松控制(高性能)
        ↓                                      ↓
  • 数据绝对正确                          • 吞吐量大
  • 并发度低                              • 可能有短暂不一致
  • 性能较差                              • 适合读多写少场景
  
  适用场景:                              适用场景:
  • 银行系统                              • 社交网络点赞数
  • 库存管理                              • 新闻网站浏览量
  • 订单系统                              • 日志统计

                     平衡点
                       ↓
            根据业务需求选择合适的隔离级别

可串行化(Serializability):

并发控制的理论基础是可串行化。

复制代码
┌─────────────────────────────────────────────────────────────────┐
│                    可串行化调度                                  │
├─────────────────────────────────────────────────────────────────┤
│                                                                 │
│  定义:                                                         │
│  多个事务并发执行的结果,与这些事务按某种顺序串行执行的结果相同 │
│                                                                 │
│  示例:                                                         │
│  事务T1: A=A+100                                                │
│  事务T2: A=A*2                                                  │
│  初始A=50                                                       │
│                                                                 │
│  串行调度1:T1→T2                                               │
│    A=50+100=150 → A=150*2=300  ✓                                │
│                                                                 │
│  串行调度2:T2→T1                                               │
│    A=50*2=100 → A=100+100=200  ✓                                │
│                                                                 │
│  可串行化的并发调度:                                           │
│    结果必须是300或200之一                                       │
│                                                                 │
│  不可串行化的调度:                                             │
│    结果是其他值(如250),则不可接受 ✗                          │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘

1.4 并发控制的核心挑战

复制代码
┌─────────────────────────────────────────────────────────────────┐
│                    并发控制面临的挑战                            │
├─────────────────────────────────────────────────────────────────┤
│                                                                 │
│  1. 性能与正确性的矛盾                                          │
│     • 锁越严格,正确性越高,但性能越差                          │
│     • 锁越宽松,性能越好,但可能不一致                          │
│                                                                 │
│  2. 死锁问题                                                    │
│     • 多个事务互相等待对方释放锁                                │
│     • 导致系统hang住                                            │
│     • 需要死锁检测和解除机制                                    │
│                                                                 │
│  3. 活锁和饥饿                                                  │
│     • 事务一直得不到执行机会                                    │
│     • 优先级机制可能导致低优先级事务饿死                        │
│                                                                 │
│  4. 开销问题                                                    │
│     • 锁管理开销(内存、CPU)                                   │
│     • 死锁检测开销                                              │
│     • 事务回滚开销                                              │
│                                                                 │
│  5. 分布式环境                                                  │
│     • 多机器间的协调更复杂                                      │
│     • 网络延迟                                                  │
│     • 时钟同步问题                                              │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘

并发控制技术演进:

复制代码
时间线:

1970s
  ↓
基于锁的并发控制(Locking)
  • 最经典的方法
  • 两段锁协议(2PL)
  
1980s
  ↓
基于时间戳的并发控制(Timestamp)
  • 无锁机制
  • 通过时间戳判断冲突
  
1980s
  ↓
乐观并发控制(Optimistic CC)
  • 假设冲突少
  • 提交时才检查冲突
  
1990s-至今
  ↓
多版本并发控制(MVCC)
  • 为每个事务提供数据快照
  • 读不阻塞写,写不阻塞读
  • 现代数据库主流方案
  • 代表:PostgreSQL, MySQL InnoDB, Oracle

并发控制的核心问题:

问题 描述 解决方法
丢失修改 一个事务的修改被另一个覆盖 加写锁
脏读 读到未提交的数据 读已提交隔离级别
不可重复读 同一事务内多次读取结果不同 可重复读隔离级别
幻读 范围查询时,其他事务插入新数据 串行化隔离级别或间隙锁

这些问题将在第二章详细讲解。


第二章 并发操作带来的问题

在没有适当并发控制的情况下,多个事务同时操作数据库会产生以下四类经典问题。

2.1 丢失修改(Lost Update)

2.1.1 问题描述

丢失修改是指两个或多个事务读取同一数据并进行修改,后提交的事务覆盖了先提交的事务的修改,导致先提交的修改丢失。

复制代码
┌─────────────────────────────────────────────────────────────────┐
│                    丢失修改示意图                                │
├─────────────────────────────────────────────────────────────────┤
│                                                                 │
│  T1: 读取(X=100) → 修改(X=X-50) → 写回(X=50)                    │
│  T2:       读取(X=100) → 修改(X=X+30) → 写回(X=130)             │
│                                                                 │
│  时间线:                                                       │
│  ────────────────────────────────────────────────────────>       │
│  T1: ───R(X=100)────────W(X=50)──────                          │
│  T2: ──────────R(X=100)─────────W(X=130)                       │
│                                                                 │
│  结果:X=130(T1的修改-50丢失了!)                             │
│  正确结果应该是:X=100-50+30=80                                 │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘

严重性:⭐⭐⭐⭐⭐(最严重)

这是最严重的并发问题,会导致数据彻底错误。

2.1.2 示例场景

场景1:库存扣减

sql 复制代码
-- 初始库存:product_stock = 100

-- 事务T1:订单A购买50件
BEGIN;
SELECT stock FROM product WHERE id=1;  -- 读取到100
-- 计算:100-50=50
UPDATE product SET stock=50 WHERE id=1;
COMMIT;

-- 事务T2:订单B购买30件(同时执行)
BEGIN;
SELECT stock FROM product WHERE id=1;  -- 也读取到100
-- 计算:100-30=70
UPDATE product SET stock=70 WHERE id=1;  -- 覆盖了T1的修改
COMMIT;

-- 结果:库存为70,但实际卖出了80件!库存错误!

场景2:账户余额

复制代码
场景:账户余额1000元,存款100元和取款200元同时操作

时间    T1(存款)            T2(取款)            余额
────────────────────────────────────────────────────
T1      读取余额(1000)       -                  1000
T2      -                   读取余额(1000)      1000
T3      余额+100=1100       -                  1000
T4      -                   余额-200=800       1000
T5      写回(1100)          -                  1100
T6      -                   写回(800)          800

结果:余额800元
正确结果:1000+100-200=900元
丢失了存款操作!

场景3:文章点赞数

复制代码
初始点赞数:1000

用户A点赞:                    用户B点赞:
1. 读取点赞数=1000            1. 读取点赞数=1000
2. 计算:1000+1=1001          2. 计算:1000+1=1001
3. 写回1001                   3. 写回1001

结果:点赞数=1001(两个赞只记录了一个)
2.1.3 产生原因
复制代码
┌─────────────────────────────────────────────────────────────────┐
│                    丢失修改的根本原因                            │
├─────────────────────────────────────────────────────────────────┤
│                                                                 │
│  1. 读-改-写模式(Read-Modify-Write)                           │
│     • 先读取数据                                                │
│     • 在内存中修改                                              │
│     • 最后写回数据库                                            │
│     • 中间没有保护机制                                          │
│                                                                 │
│  2. 无锁保护                                                    │
│     • 读取时没有加锁                                            │
│     • 其他事务可以同时读取                                      │
│     • 基于过时数据进行修改                                      │
│                                                                 │
│  3. 覆盖写入                                                    │
│     • 后提交的事务覆盖先提交的                                  │
│     • 没有检查数据是否已被修改                                  │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘

解决方案:

sql 复制代码
-- 方案1:使用排他锁
BEGIN;
SELECT stock FROM product WHERE id=1 FOR UPDATE;  -- 加排他锁
UPDATE product SET stock = stock - 50 WHERE id=1;
COMMIT;

-- 方案2:使用乐观锁(版本号)
BEGIN;
SELECT stock, version FROM product WHERE id=1;  -- stock=100, version=1
UPDATE product 
SET stock = 50, version = 2
WHERE id=1 AND version=1;  -- 只有版本号匹配才更新
COMMIT;

-- 方案3:原子操作
UPDATE product SET stock = stock - 50 WHERE id=1;  -- 直接在SQL中计算

2.2 不可重复读(Non-Repeatable Read)

2.2.1 问题描述

不可重复读是指在同一事务中,多次读取同一数据,但得到的结果不同。原因是在两次读取之间,另一个事务修改了该数据并提交。

复制代码
┌─────────────────────────────────────────────────────────────────┐
│                    不可重复读示意图                              │
├─────────────────────────────────────────────────────────────────┤
│                                                                 │
│  T1: 第一次读(X=100) → ... → 第二次读(X=200) ?!                 │
│  T2:            修改X=200并提交                                 │
│                                                                 │
│  时间线:                                                       │
│  ────────────────────────────────────────────────────────>       │
│  T1: ───R(X=100)────────────────R(X=200)────                   │
│  T2: ──────────W(X=200),COMMIT──────                           │
│                                                                 │
│  问题:T1在同一事务中两次读取X,结果不一样!                    │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘

严重性:⭐⭐⭐

导致事务内部数据不一致,影响业务逻辑判断。

2.2.2 示例场景

场景1:银行转账验证

sql 复制代码
-- 事务T1:检查并转账
BEGIN;
-- 第一次查询账户余额
SELECT balance FROM account WHERE id=1;  -- 余额1000元
-- 判断:余额足够,可以转账500元

-- ... 此时T2介入 ...

-- 第二次查询账户余额(再次确认)
SELECT balance FROM account WHERE id=1;  -- 余额变成200元!

COMMIT;

-- 事务T2:其他地方扣款(同时执行)
BEGIN;
UPDATE account SET balance=200 WHERE id=1;  -- 扣款800元
COMMIT;

-- 问题:T1第一次看到1000元,决定转账500元
--       但第二次看到200元,逻辑混乱!

场景2:统计分析

sql 复制代码
-- 事务T1:计算平均工资
BEGIN;
-- 第一次读取
SELECT AVG(salary) FROM employee;  -- 结果:5000元

-- ... 业务处理 ...

-- 第二次读取(验证)
SELECT AVG(salary) FROM employee;  -- 结果:8000元(?!)

COMMIT;

-- 事务T2:给员工加薪(同时执行)
BEGIN;
UPDATE employee SET salary = salary * 1.5;
COMMIT;

-- 问题:同一报表中两个数据不一致

场景3:订单状态检查

复制代码
时间    T1(查询订单状态)              T2(修改订单状态)
─────────────────────────────────────────────────────────
T1      第一次查询:status=待支付     -
T2      验证状态...                   -
T3      -                            修改为"已支付"并提交
T4      第二次查询:status=已支付     -
T5      ??状态变化了               -

问题:T1事务内部,同一订单状态前后不一致
2.2.3 产生原因
复制代码
┌─────────────────────────────────────────────────────────────────┐
│                    不可重复读的原因                              │
├─────────────────────────────────────────────────────────────────┤
│                                                                 │
│  1. 读取时未加锁                                                │
│     • SELECT语句默认不加锁                                      │
│     • 读完后立即释放共享锁                                      │
│                                                                 │
│  2. 其他事务可以修改                                            │
│     • 读取期间,其他事务可以UPDATE                              │
│     • 其他事务提交后,数据已改变                                │
│                                                                 │
│  3. 隔离级别不够                                                │
│     • READ COMMITTED级别只保证读到已提交数据                    │
│     • 但不保证多次读取结果相同                                  │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘

解决方案:

sql 复制代码
-- 方案1:使用REPEATABLE READ隔离级别
SET TRANSACTION ISOLATION LEVEL REPEATABLE READ;
BEGIN;
SELECT balance FROM account WHERE id=1;  -- 100元
-- 其他事务修改也看不到
SELECT balance FROM account WHERE id=1;  -- 仍然是100元
COMMIT;

-- 方案2:使用共享锁
BEGIN;
SELECT balance FROM account WHERE id=1 LOCK IN SHARE MODE;
-- 持有共享锁,其他事务不能修改
SELECT balance FROM account WHERE id=1;  -- 结果一致
COMMIT;

-- 方案3:使用排他锁
BEGIN;
SELECT balance FROM account WHERE id=1 FOR UPDATE;
-- 持有排他锁,完全锁定数据
SELECT balance FROM account WHERE id=1;
COMMIT;

2.3 脏读(Dirty Read)

2.3.1 问题描述

脏读 是指一个事务读取了另一个未提交事务修改的数据。如果该事务回滚,则读取的数据就是无效的"脏数据"。

复制代码
┌─────────────────────────────────────────────────────────────────┐
│                    脏读示意图                                    │
├─────────────────────────────────────────────────────────────────┤
│                                                                 │
│  T1: 修改X=200(未提交)                                        │
│  T2:        读取X=200(脏数据)→ 基于脏数据做决策               │
│  T1:                  ROLLBACK(回滚!)                         │
│                                                                 │
│  时间线:                                                       │
│  ────────────────────────────────────────────────────────>       │
│  T1: ───W(X=200)────────────ROLLBACK──                         │
│  T2: ──────────R(X=200)────────────(使用了无效数据)             │
│                                                                 │
│  问题:T2读到的X=200根本不存在!T1已回滚!                      │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘

严重性:⭐⭐⭐⭐

读取到从未真正存在过的数据,导致严重的业务错误。

2.3.2 示例场景

场景1:转账操作

sql 复制代码
-- 账户A余额:1000元
-- 账户B余额:500元

-- 事务T1:从A转账500元到B(但会失败回滚)
BEGIN;
UPDATE account SET balance=500 WHERE id='A';     -- A减少500
UPDATE account SET balance=1000 WHERE id='B';    -- B增加500
-- 此时A=500, B=1000
-- ... 发生错误,准备回滚 ...

-- 事务T2:查询B的余额(同时执行)
BEGIN;
SELECT balance FROM account WHERE id='B';  -- 读到1000(脏数据!)
-- 认为B有1000元,允许B消费900元
UPDATE account SET balance=100 WHERE id='B';
COMMIT;

-- 事务T1:回滚
ROLLBACK;  -- A恢复到1000, B恢复到500

-- 结果:B账户余额变成负数!
-- T2以为B有1000元,扣了900元,剩100元
-- 但T1回滚后,B实际只有500元,扣900元后应该是-400元!

场景2:库存判断

复制代码
时间    T1(补货,但会回滚)          T2(检查库存)
────────────────────────────────────────────────────
T1      库存=10                     -
T2      UPDATE stock=110           -
T3      (补货100件,未提交)         -
T4      -                          读取stock=110
T5      -                          判断:库存充足
T6      -                          允许下单100件
T7      ROLLBACK                   -
T8      库存恢复到10                -

问题:实际只有10件库存,却下单了100件!

场景3:审批流程

sql 复制代码
-- 事务T1:审批文档(但审批人反悔)
BEGIN;
UPDATE document SET status='已审批' WHERE id=1;
-- 未提交

-- 事务T2:查看文档状态
SELECT status FROM document WHERE id=1;  -- 读到"已审批"(脏数据)
-- 基于"已审批"状态,执行后续流程...

-- 事务T1:反悔,撤销审批
ROLLBACK;

-- 结果:文档实际未审批,但后续流程已启动!
2.3.3 产生原因
复制代码
┌─────────────────────────────────────────────────────────────────┐
│                    脏读的原因                                    │
├─────────────────────────────────────────────────────────────────┤
│                                                                 │
│  1. 读取未提交数据                                              │
│     • 事务隔离级别设置为READ UNCOMMITTED                        │
│     • 读取时不检查数据是否已提交                                │
│                                                                 │
│  2. 无锁保护                                                    │
│     • 写操作未加排他锁                                          │
│     • 或读操作无视排他锁                                        │
│                                                                 │
│  3. 事务回滚                                                    │
│     • 写事务可能回滚                                            │
│     • 但读事务已经使用了数据                                    │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘

解决方案:

sql 复制代码
-- 方案1:使用READ COMMITTED或更高隔离级别
SET TRANSACTION ISOLATION LEVEL READ COMMITTED;
BEGIN;
SELECT balance FROM account WHERE id='B';  -- 只能读到已提交的数据
COMMIT;

-- 方案2:等待其他事务提交
-- 大多数数据库默认会等待写锁释放
BEGIN;
SELECT balance FROM account WHERE id='B';
-- 如果B正在被其他事务修改,会等待其提交或回滚
COMMIT;

2.4 幻读(Phantom Read)

2.4.1 问题描述

幻读 是指在同一事务中,两次执行相同的范围查询 ,得到的记录数量不同。原因是其他事务插入或删除了符合条件的新记录。

复制代码
┌─────────────────────────────────────────────────────────────────┐
│                    幻读示意图                                    │
├─────────────────────────────────────────────────────────────────┤
│                                                                 │
│  T1: 第一次范围查询(3条记录)→ 第二次范围查询(5条记录)?!    │
│  T2:            INSERT新记录(2条)                             │
│                                                                 │
│  时间线:                                                       │
│  ────────────────────────────────────────────────────────>       │
│  T1: ──查询(3条)──────────────查询(5条)────                    │
│  T2: ──────────INSERT 2条, COMMIT───                           │
│                                                                 │
│  问题:T1两次查询,结果集的记录数不同!像产生了"幻影"           │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘

严重性:⭐⭐

主要影响统计类查询,在某些场景下可以接受。

2.4.2 示例场景

场景1:工资统计

sql 复制代码
-- 部门员工工资表
-- 初始数据:3名员工,工资分别为5000, 6000, 7000

-- 事务T1:统计部门工资
BEGIN;
-- 第一次统计
SELECT COUNT(*) FROM employee WHERE dept='销售部';  -- 结果:3人
SELECT SUM(salary) FROM employee WHERE dept='销售部';  -- 总额:18000

-- ... 业务处理 ...

-- 第二次统计(验证)
SELECT COUNT(*) FROM employee WHERE dept='销售部';  -- 结果:5人!
SELECT SUM(salary) FROM employee WHERE dept='销售部';  -- 总额:28000!

COMMIT;

-- 事务T2:新员工入职(同时执行)
BEGIN;
INSERT INTO employee VALUES (4, '张三', '销售部', 5000);
INSERT INTO employee VALUES (5, '李四', '销售部', 5000);
COMMIT;

-- 问题:报表中统计数据前后矛盾

场景2:库存盘点

复制代码
时间    T1(盘点)                        T2(入库)
────────────────────────────────────────────────────────
T1      第一次查询:10种商品             -
T2      逐个记录...                     -
T3      -                              INSERT新商品(2种)并提交
T4      第二次查询:12种商品             -
T5      总数对不上!                    -

问题:盘点过程中"凭空出现"了新商品

场景3:订单总额计算

sql 复制代码
-- 事务T1:计算某天的订单总额
BEGIN;
-- 第一次查询订单数量
SELECT COUNT(*) FROM orders 
WHERE order_date='2024-01-01';  -- 100个订单

-- 计算总金额
SELECT SUM(amount) FROM orders 
WHERE order_date='2024-01-01';  -- 10000元

-- ... 生成报表 ...

-- 再次验证
SELECT COUNT(*) FROM orders 
WHERE order_date='2024-01-01';  -- 102个订单!

COMMIT;

-- 事务T2:补录订单(同时执行)
BEGIN;
INSERT INTO orders VALUES (101, '2024-01-01', 100);
INSERT INTO orders VALUES (102, '2024-01-01', 50);
COMMIT;

-- 问题:报表显示"100个订单,总额10000元"
--       但实际已经有102个订单
2.4.3 产生原因
复制代码
┌─────────────────────────────────────────────────────────────────┐
│                    幻读的原因                                    │
├─────────────────────────────────────────────────────────────────┤
│                                                                 │
│  1. 行锁无法锁定"不存在的行"                                    │
│     • 普通锁只能锁定已存在的记录                                │
│     • 无法阻止其他事务INSERT新记录                              │
│                                                                 │
│  2. 范围查询的特性                                              │
│     • WHERE条件是范围(如salary>5000)                          │
│     • 新插入的记录可能满足范围条件                              │
│                                                                 │
│  3. 隔离级别不够                                                │
│     • REPEATABLE READ级别不能完全防止幻读                       │
│     • 需要SERIALIZABLE级别                                      │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘

解决方案:

sql 复制代码
-- 方案1:使用SERIALIZABLE隔离级别
SET TRANSACTION ISOLATION LEVEL SERIALIZABLE;
BEGIN;
SELECT COUNT(*) FROM employee WHERE dept='销售部';
-- 其他事务无法INSERT符合条件的新记录
SELECT COUNT(*) FROM employee WHERE dept='销售部';  -- 结果一致
COMMIT;

-- 方案2:使用间隙锁(Gap Lock)- MySQL InnoDB
BEGIN;
SELECT * FROM employee WHERE dept='销售部' FOR UPDATE;
-- 锁定现有记录 + 锁定"间隙",阻止INSERT
SELECT * FROM employee WHERE dept='销售部';
COMMIT;

-- 方案3:锁表(不推荐,性能差)
BEGIN;
LOCK TABLE employee WRITE;
SELECT COUNT(*) FROM employee WHERE dept='销售部';
UNLOCK TABLES;
COMMIT;

2.5 并发问题对比总结

复制代码
┌─────────────────────────────────────────────────────────────────┐
│              四大并发问题对比                                    │
├──────────┬────────────┬─────────────┬────────────┬──────────────┤
│ 问题类型 │ 影响的操作 │ 数据变化    │ 严重性     │ 典型场景     │
├──────────┼────────────┼─────────────┼────────────┼──────────────┤
│ 丢失修改 │ 写-写冲突  │ 修改被覆盖  │ ⭐⭐⭐⭐⭐  │ 库存扣减     │
│          │            │             │            │ 余额更新     │
├──────────┼────────────┼─────────────┼────────────┼──────────────┤
│ 脏读     │ 写-读冲突  │ 读到未提交  │ ⭐⭐⭐⭐    │ 转账审核     │
│          │            │ 数据        │            │ 状态检查     │
├──────────┼────────────┼─────────────┼────────────┼──────────────┤
│不可重复读│ 读-写冲突  │ 记录被修改  │ ⭐⭐⭐      │ 余额验证     │
│          │            │             │            │ 统计分析     │
├──────────┼────────────┼─────────────┼────────────┼──────────────┤
│ 幻读     │ 读-写冲突  │ 记录数量    │ ⭐⭐        │ 报表统计     │
│          │            │ 变化        │            │ 批量处理     │
└──────────┴────────────┴─────────────┴────────────┴──────────────┘

关键区别:

对比维度 不可重复读 幻读
变化内容 已有记录的值改变 记录总数改变
触发操作 UPDATE/DELETE INSERT
影响范围 单条记录 查询结果集
锁定方式 行锁可解决 需要间隙锁/表锁

问题关系图:

复制代码
并发问题严重性递减:

丢失修改(最严重)
    ↓
  脏读
    ↓
不可重复读
    ↓
幻读(最轻微)

解决难度递增:

丢失修改 → 简单行锁即可
脏读     → 读已提交即可
不可重复读 → 需要可重复读
幻读     → 需要串行化或间隙锁

各隔离级别能防止的问题:

隔离级别 丢失修改 脏读 不可重复读 幻读
READ UNCOMMITTED
READ COMMITTED
REPEATABLE READ ✗(MySQL可防)
SERIALIZABLE

注:MySQL InnoDB在REPEATABLE READ级别通过间隙锁可以防止幻读。


第三章 事务的基本概念

事务是并发控制的核心单元,是解决并发问题的基础。

3.1 什么是事务

事务(Transaction) 是数据库执行的最小工作单元,包含一组操作,这些操作要么全部成功,要么全部失败。

复制代码
┌─────────────────────────────────────────────────────────────────┐
│                    事务的本质                                    │
├─────────────────────────────────────────────────────────────────┤
│                                                                 │
│  定义:一个不可分割的工作单元                                   │
│                                                                 │
│  生活类比:去银行办理转账业务                                   │
│  1. 从账户A扣款                                                 │
│  2. 向账户B加款                                                 │
│  • 两个操作必须都成功,或者都不执行                             │
│  • 不能只扣了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;  -- 回滚事务(失败)

事务的边界:

复制代码
┌──────────────────────────────────────────────────────┐
│             事务的生命周期                           │
├──────────────────────────────────────────────────────┤
│                                                      │
│  BEGIN TRANSACTION                                   │
│  ↓                                                   │
│  ┌──────────────────────────────┐                   │
│  │  Transaction 活动状态         │                   │
│  │  • 执行SQL语句                │                   │
│  │  • 读写数据                   │                   │
│  │  • 修改数据                   │                   │
│  └──────────────────────────────┘                   │
│  ↓                       ↓                           │
│  COMMIT               ROLLBACK                       │
│  (提交)             (回滚)                       │
│  ↓                       ↓                           │
│  持久化到磁盘          恢复到初始状态                │
│                                                      │
└──────────────────────────────────────────────────────┘

3.2 事务的ACID特性

ACID是事务必须满足的四个特性,是数据库正确性的基石。

复制代码
┌─────────────────────────────────────────────────────────────────┐
│                    ACID特性总览                                  │
├─────────────────────────────────────────────────────────────────┤
│                                                                 │
│  A - Atomicity    (原子性)  → 全有或全无                        │
│  C - Consistency  (一致性)  → 符合约束                          │
│  I - Isolation    (隔离性)  → 互不干扰                          │
│  D - Durability   (持久性)  → 永久保存                          │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘
3.2.1 原子性(Atomicity)

定义: 事务中的所有操作,要么全部完成,要么全部不完成,不会停留在中间某个状态。

复制代码
┌─────────────────────────────────────────────────────────────────┐
│                    原子性示意                                    │
├─────────────────────────────────────────────────────────────────┤
│                                                                 │
│  转账事务包含2个操作:                                          │
│  ┌──────┐       ┌──────┐                                       │
│  │ 扣A  │  +   │ 加B  │                                       │
│  └──────┘       └──────┘                                       │
│                                                                 │
│  正确结果:                                                     │
│  ✓ 两个操作都成功 → COMMIT                                      │
│  ✓ 任一操作失败   → ROLLBACK(全部撤销)                        │
│                                                                 │
│  不允许的情况:                                                 │
│  ✗ 只执行了扣A,没有加B                                         │
│  ✗ 只执行了加B,没有扣A                                         │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘

实现机制:

sql 复制代码
-- 示例:原子性保障
BEGIN;
UPDATE account SET balance = balance - 100 WHERE id=1;
-- 如果这里发生错误(如余额不足)
UPDATE account SET balance = balance + 100 WHERE id=2;
-- 整个事务会回滚,第一个UPDATE也会撤销
COMMIT;

-- 数据库通过日志实现原子性
-- • Undo Log:记录修改前的值,用于回滚
-- • 失败时,根据Undo Log恢复数据

通俗理解:

原子性就像购物下单,要么商品+运费一起支付成功,要么都不支付。不能只扣了商品费,运费没扣成功。

3.2.2 一致性(Consistency)

定义: 事务执行前后,数据库从一个一致性状态转换到另一个一致性状态,不违反任何完整性约束。

复制代码
┌─────────────────────────────────────────────────────────────────┐
│                    一致性示意                                    │
├─────────────────────────────────────────────────────────────────┤
│                                                                 │
│  约束:账户总金额保持不变                                       │
│                                                                 │
│  事务前:A=1000, B=500, 总额=1500                               │
│  事务:A转账200给B                                              │
│  事务后:A=800,  B=700, 总额=1500 ✓                             │
│                                                                 │
│  其他一致性约束:                                               │
│  • 余额不能为负数                                               │
│  • 外键约束(订单必须关联有效用户)                             │
│  • 唯一性约束(用户名不重复)                                   │
│  • 业务规则(未成年人不能购买酒类)                             │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘

一致性示例:

sql 复制代码
-- 一致性约束:库存不能为负
CREATE TABLE product (
    id INT PRIMARY KEY,
    stock INT CHECK (stock >= 0)  -- 约束
);

-- 事务违反一致性会被拒绝
BEGIN;
UPDATE product SET stock = stock - 100 WHERE id=1;
-- 如果更新后stock变成负数,事务会失败
COMMIT;

-- 数据库会自动检查约束
-- ERROR: new row violates check constraint "product_stock_check"

通俗理解:

一致性就像天平,转账前后两边重量要相等。A账户减少的钱 = B账户增加的钱。

3.2.3 隔离性(Isolation)

定义: 多个事务并发执行时,一个事务的执行不应受到其他事务的干扰。

复制代码
┌─────────────────────────────────────────────────────────────────┐
│                    隔离性示意                                    │
├─────────────────────────────────────────────────────────────────┤
│                                                                 │
│  理想状态:完全隔离(串行化)                                   │
│  ┌─────────┐                                                    │
│  │ 事务T1  │ → 完成                                             │
│  └─────────┘                                                    │
│           ┌─────────┐                                           │
│           │ 事务T2  │ → 完成                                     │
│           └─────────┘                                           │
│                                                                 │
│  实际情况:并发执行但互不干扰                                   │
│  ┌─────────┐                                                    │
│  │ 事务T1  │ ─┬─ 看起来像是独立执行                             │
│  └─────────┘  │                                                 │
│  ┌─────────┐  │                                                 │
│  │ 事务T2  │ ─┴─ 虽然交叉执行,但感知不到对方                   │
│  └─────────┘                                                    │
│                                                                 │
│  这就是并发控制的核心!                                         │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘

隔离性级别:

不同级别的隔离性,性能和安全性不同(详见3.5节)。

通俗理解:

隔离性就像ATM机取款,虽然很多人在不同ATM同时操作同一账户,但每个人感觉就像只有自己在操作。

3.2.4 持久性(Durability)

定义: 一旦事务提交,其对数据库的修改就是永久性的,即使系统故障也不会丢失。

复制代码
┌─────────────────────────────────────────────────────────────────┐
│                    持久性示意                                    │
├─────────────────────────────────────────────────────────────────┤
│                                                                 │
│  COMMIT                                                         │
│  ↓                                                              │
│  数据写入磁盘                                                   │
│  ↓                                                              │
│  即使发生:                                                     │
│  • 断电                                                         │
│  • 系统崩溃                                                     │
│  • 磁盘故障(有备份)                                           │
│  ↓                                                              │
│  数据依然存在!                                                 │
│                                                                 │
│  实现机制:                                                     │
│  • Redo Log(重做日志)                                         │
│  • 先写日志,再写数据(WAL: Write-Ahead Logging)               │
│  • 崩溃恢复时,根据Redo Log重做已提交事务                       │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘

持久性示例:

sql 复制代码
BEGIN;
INSERT INTO orders VALUES (1001, 'product_A', 100);
COMMIT;  -- 提交后,数据永久保存

-- 即使此时:
-- 1. 数据库崩溃
-- 2. 服务器重启
-- 3. 断电
-- 订单1001的数据依然存在

-- 数据库通过日志保证持久性
-- • 提交时先写Redo Log
-- • 日志持久化后,事务才算成功
-- • 崩溃恢复时,重放Redo Log

通俗理解:

持久性就像银行存款凭证,一旦存款成功,即使银行停电、电脑坏了,你的钱也不会丢失(有记录可查)。

ACID之间的关系:

复制代码
┌─────────────────────────────────────────────────────────────────┐
│                    ACID关系图                                    │
├─────────────────────────────────────────────────────────────────┤
│                                                                 │
│            原子性(A)                                            │
│                ↓                                                │
│            保证操作完整性                                       │
│                                                                 │
│            一致性(C) ← 最终目标                                 │
│                ↑                                                │
│            由AID共同保证                                        │
│                                                                 │
│            隔离性(I)                                            │
│                ↓                                                │
│            解决并发问题                                         │
│                                                                 │
│            持久性(D)                                            │
│                ↓                                                │
│            防止数据丢失                                         │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘

3.3 事务的状态

事务在执行过程中会经历不同的状态。

复制代码
┌─────────────────────────────────────────────────────────────────┐
│                    事务状态转换图                                │
├─────────────────────────────────────────────────────────────────┤
│                                                                 │
│         BEGIN                                                   │
│          ↓                                                      │
│     ┌─────────┐                                                │
│     │ 活动状态 │ ← 正在执行SQL语句                              │
│     │(Active) │                                                │
│     └─────────┘                                                │
│          ↓                                                      │
│     最后一条SQL执行完                                           │
│          ↓                                                      │
│     ┌──────────┐                                               │
│     │ 部分提交  │ ← 已执行完,但未写入磁盘                       │
│     │(Partially│                                               │
│     │Committed)│                                               │
│     └──────────┘                                               │
│          ↓                    ↓                                │
│      写入成功                写入失败                           │
│          ↓                    ↓                                │
│     ┌─────────┐          ┌─────────┐                          │
│     │ 提交状态 │          │ 失败状态 │                          │
│     │(Committ-│          │(Failed) │                          │
│     │ed)      │          └─────────┘                          │
│     └─────────┘                ↓                               │
│                          执行回滚                               │
│                                ↓                               │
│                           ┌─────────┐                          │
│                           │ 中止状态 │                          │
│                           │(Aborted)│                          │
│                           └─────────┘                          │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘

状态说明:

状态 说明 可以进行的操作
Active 初始状态,事务正在执行 继续执行SQL
Partially Committed 最后一条语句执行完毕 等待写入磁盘
Committed 事务成功完成 无(已结束)
Failed 无法正常执行 准备回滚
Aborted 事务回滚,恢复到初始状态 可重启事务或放弃

3.4 事务的操作

基本操作命令:

sql 复制代码
-- 1. 开始事务
BEGIN;
-- 或
START TRANSACTION;
-- 或 (隐式开始)
-- 大多数数据库在执行第一条SQL时自动开始事务

-- 2. 提交事务
COMMIT;
-- 使所有修改永久生效

-- 3. 回滚事务
ROLLBACK;
-- 撤销所有修改

-- 4. 保存点(Savepoint)
SAVEPOINT sp1;  -- 设置保存点
-- ... 一些操作 ...
ROLLBACK TO sp1;  -- 回滚到保存点(部分回滚)
RELEASE SAVEPOINT sp1;  -- 删除保存点

事务操作示例:

sql 复制代码
-- 示例1:完整的转账事务
BEGIN;
  UPDATE account SET balance = balance - 100 WHERE id=1;
  UPDATE account SET balance = balance + 100 WHERE id=2;
COMMIT;

-- 示例2:带错误处理的事务
BEGIN;
  UPDATE inventory SET stock = stock - 10 WHERE product_id=100;
  
  -- 检查库存是否足够
  SELECT stock FROM inventory WHERE product_id=100;
  -- 如果stock < 0,则回滚
  IF (stock < 0) THEN
    ROLLBACK;
  ELSE
    COMMIT;
  END IF;

-- 示例3:使用保存点
BEGIN;
  INSERT INTO orders VALUES (1, 'A', 100);  -- 订单主记录
  SAVEPOINT sp1;
  
  INSERT INTO order_items VALUES (1, 1, 10);  -- 订单明细1
  INSERT INTO order_items VALUES (1, 2, 20);  -- 订单明细2
  
  -- 如果明细有错误,可以只回滚明细
  ROLLBACK TO sp1;
  -- 重新插入正确的明细
  INSERT INTO order_items VALUES (1, 1, 15);
  
COMMIT;

3.5 事务隔离级别

为什么需要隔离级别?

复制代码
性能 ←──────────────────────────→ 安全性
     低隔离    中隔离    高隔离
      ↓         ↓         ↓
   更快但     平衡      更安全但
   可能不一致          可能更慢

SQL标准定义了4个隔离级别,从低到高依次是:

3.5.1 读未提交(Read Uncommitted)

特点: 最低级别,允许读取未提交的数据。

复制代码
┌─────────────────────────────────────────────────────────────────┐
│                READ UNCOMMITTED                                 │
├─────────────────────────────────────────────────────────────────┤
│                                                                 │
│  特性:                                                         │
│  • 事务可以读取其他事务未提交的数据                             │
│  • 几乎没有隔离性                                               │
│  • 性能最好,但最不安全                                         │
│                                                                 │
│  可能出现的问题:                                               │
│  ✗ 脏读   - 会出现                                              │
│  ✗ 不可重复读 - 会出现                                          │
│  ✗ 幻读   - 会出现                                              │
│  ✗ 丢失修改 - 会出现                                            │
│                                                                 │
│  使用场景:                                                     │
│  • 几乎不使用(太不安全)                                       │
│  • 仅用于对一致性要求极低的统计                                 │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘
sql 复制代码
-- 设置隔离级别
SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;

BEGIN;
SELECT * FROM account WHERE id=1;  
-- 可能读到其他事务未提交的数据(脏读)
COMMIT;
3.5.2 读已提交(Read Committed)

特点: 只能读取已提交的数据,避免脏读。

复制代码
┌─────────────────────────────────────────────────────────────────┐
│                READ COMMITTED                                   │
├─────────────────────────────────────────────────────────────────┤
│                                                                 │
│  特性:                                                         │
│  • 只能读取已提交的数据                                         │
│  • 避免了脏读                                                   │
│  • Oracle和SQL Server的默认级别                                │
│                                                                 │
│  可能出现的问题:                                               │
│  ✓ 脏读   - 不会出现                                            │
│  ✗ 不可重复读 - 会出现                                          │
│  ✗ 幻读   - 会出现                                              │
│                                                                 │
│  使用场景:                                                     │
│  • 大多数OLTP系统的默认选择                                     │
│  • 适合读多写少的应用                                           │
│  • 余额查询、订单查询等                                         │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘
sql 复制代码
SET TRANSACTION ISOLATION LEVEL READ COMMITTED;

BEGIN;
SELECT balance FROM account WHERE id=1;  -- 读取:1000元
-- 此时其他事务修改并提交:balance=500
SELECT balance FROM account WHERE id=1;  -- 读取:500元(不可重复读)
COMMIT;
3.5.3 可重复读(Repeatable Read)

特点: 同一事务中多次读取结果一致。

复制代码
┌─────────────────────────────────────────────────────────────────┐
│                REPEATABLE READ                                  │
├─────────────────────────────────────────────────────────────────┤
│                                                                 │
│  特性:                                                         │
│  • 同一事务内多次读取同一数据,结果相同                         │
│  • 通过锁定读取的行实现                                         │
│  • MySQL InnoDB的默认级别                                       │
│                                                                 │
│  可能出现的问题:                                               │
│  ✓ 脏读   - 不会出现                                            │
│  ✓ 不可重复读 - 不会出现                                        │
│  ✗ 幻读   - 可能出现(MySQL InnoDB通过间隙锁避免)              │
│                                                                 │
│  使用场景:                                                     │
│  • 需要一致性读的报表                                           │
│  • 批量处理                                                     │
│  • 统计分析                                                     │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘
sql 复制代码
SET TRANSACTION ISOLATION LEVEL REPEATABLE READ;

BEGIN;
SELECT balance FROM account WHERE id=1;  -- 读取:1000元
-- 其他事务修改并提交:balance=500
SELECT balance FROM account WHERE id=1;  -- 仍然读取:1000元(可重复读)
COMMIT;
3.5.4 串行化(Serializable)

特点: 最高隔离级别,完全隔离,如同串行执行。

复制代码
┌─────────────────────────────────────────────────────────────────┐
│                SERIALIZABLE                                     │
├─────────────────────────────────────────────────────────────────┤
│                                                                 │
│  特性:                                                         │
│  • 最严格的隔离级别                                             │
│  • 事务串行执行(看起来像)                                     │
│  • 锁定整个范围,防止插入                                       │
│                                                                 │
│  可能出现的问题:                                               │
│  ✓ 脏读   - 不会出现                                            │
│  ✓ 不可重复读 - 不会出现                                        │
│  ✓ 幻读   - 不会出现                                            │
│                                                                 │
│  代价:                                                         │
│  • 性能最差                                                     │
│  • 并发度最低                                                   │
│  • 容易产生锁等待和死锁                                         │
│                                                                 │
│  使用场景:                                                     │
│  • 金融交易                                                     │
│  • 关键业务逻辑                                                 │
│  • 对一致性要求极高的场景                                       │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘
sql 复制代码
SET TRANSACTION ISOLATION LEVEL SERIALIZABLE;

BEGIN;
SELECT COUNT(*) FROM orders WHERE date='2024-01-01';  -- 100条
-- 其他事务无法插入新订单
SELECT COUNT(*) FROM orders WHERE date='2024-01-01';  -- 仍然100条
COMMIT;
3.5.5 隔离级别对比

详细对比表:

隔离级别 脏读 不可重复读 幻读 实现方式 性能 使用场景
READ UNCOMMITTED 无锁 ⭐⭐⭐⭐⭐ 几乎不用
READ COMMITTED 读加锁,读完释放 ⭐⭐⭐⭐ 默认选择
REPEATABLE READ 事务期间持有锁 ⭐⭐⭐ 报表统计
SERIALIZABLE 范围锁 ⭐⭐ 金融系统

注:△ 表示MySQL InnoDB可以通过间隙锁防止

性能vs安全性权衡:

复制代码
隔离级别    并发性能    数据安全    推荐场景
────────────────────────────────────────────
READ        ⭐⭐⭐⭐⭐    ⭐          测试环境
UNCOMMITTED

READ        ⭐⭐⭐⭐     ⭐⭐⭐      大多数Web应用
COMMITTED                          (Oracle默认)

REPEATABLE  ⭐⭐⭐      ⭐⭐⭐⭐    报表、银行查询
READ                               (MySQL默认)

SERIALIZABLE ⭐⭐       ⭐⭐⭐⭐⭐  关键金融业务

如何选择隔离级别:

复制代码
┌─────────────────────────────────────────────────────────────────┐
│                隔离级别选择指南                                  │
├─────────────────────────────────────────────────────────────────┤
│                                                                 │
│  1. 普通Web应用                                                 │
│     → READ COMMITTED                                            │
│     • 性能好,基本够用                                          │
│     • Oracle/PostgreSQL默认                                     │
│                                                                 │
│  2. 报表统计                                                    │
│     → REPEATABLE READ                                           │
│     • 保证数据一致性                                            │
│     • MySQL默认                                                 │
│                                                                 │
│  3. 金融交易                                                    │
│     → SERIALIZABLE                                              │
│     • 绝对安全                                                  │
│     • 牺牲性能换正确性                                          │
│                                                                 │
│  4. 高并发、对一致性要求不严格                                  │
│     → READ UNCOMMITTED                                          │
│     • 实时数据展示(如在线人数)                                │
│     • 非关键业务                                                │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘

主流数据库默认隔离级别:

数据库 默认隔离级别
MySQL InnoDB REPEATABLE READ
Oracle READ COMMITTED
SQL Server READ COMMITTED
PostgreSQL READ COMMITTED

第四章 封锁协议概述

封锁是实现并发控制最基本和最重要的技术之一。

4.1 什么是封锁

封锁(Locking) 是事务在访问数据前,先对数据加锁,阻止其他事务同时访问,从而避免并发冲突。

复制代码
┌─────────────────────────────────────────────────────────────────┐
│                    封锁的本质                                    │
├─────────────────────────────────────────────────────────────────┤
│                                                                 │
│  通俗理解:图书馆借书                                           │
│                                                                 │
│  1. 你要看一本书                                                │
│  2. 先从书架上拿下来(加锁)                                    │
│  3. 看书期间,别人无法同时看这本书                              │
│  4. 看完后放回书架(解锁)                                      │
│  5. 其他人才能借阅                                              │
│                                                                 │
│  数据库也是同样的道理!                                         │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘

基本封锁流程:

复制代码
┌──────────────────────────────────────────┐
│           封锁操作流程                   │
├──────────────────────────────────────────┤
│                                          │
│  事务开始                                │
│    ↓                                     │
│  加锁(Lock)                            │
│    ↓                                     │
│  访问数据(读/写)                       │
│    ↓                                     │
│  解锁(Unlock)                          │
│    ↓                                     │
│  其他事务可以访问                        │
│                                          │
└──────────────────────────────────────────┘

4.2 锁的类型

数据库中主要有两种基本锁类型:

4.2.1 排他锁(X锁)

排他锁(Exclusive Lock, X-Lock) 也叫写锁,当事务对数据加排他锁时,其他事务既不能读也不能写。

复制代码
┌─────────────────────────────────────────────────────────────────┐
│                    排他锁(X锁)                                 │
├─────────────────────────────────────────────────────────────────┤
│                                                                 │
│  特点:                                                         │
│  • 独占访问                                                     │
│  • 其他事务不能读,也不能写                                     │
│  • 保护写操作                                                   │
│                                                                 │
│  使用场景:                                                     │
│  • UPDATE语句                                                   │
│  • DELETE语句                                                   │
│  • INSERT语句                                                   │
│  • SELECT ... FOR UPDATE                                        │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘

示例:

sql 复制代码
-- 事务T1:加排他锁
BEGIN;
SELECT * FROM account WHERE id=1 FOR UPDATE;  -- 加X锁
UPDATE account SET balance=500 WHERE id=1;
COMMIT;  -- 释放锁

-- 事务T2:等待T1释放锁
BEGIN;
SELECT * FROM account WHERE id=1;  -- 等待...
4.2.2 共享锁(S锁)

共享锁(Shared Lock, S-Lock) 也叫读锁,当事务对数据加共享锁时,其他事务可以读但不能写。

复制代码
┌─────────────────────────────────────────────────────────────────┐
│                    共享锁(S锁)                                 │
├─────────────────────────────────────────────────────────────────┤
│                                                                 │
│  特点:                                                         │
│  • 多个事务可以同时持有共享锁                                   │
│  • 其他事务可以读,但不能写                                     │
│  • 保护读操作                                                   │
│                                                                 │
│  使用场景:                                                     │
│  • SELECT语句(某些隔离级别)                                   │
│  • SELECT ... LOCK IN SHARE MODE                                │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘

示例:

sql 复制代码
-- 事务T1和T2:同时加共享锁(可以)
BEGIN;  -- T1
SELECT * FROM account WHERE id=1 LOCK IN SHARE MODE;  -- 加S锁

BEGIN;  -- T2
SELECT * FROM account WHERE id=1 LOCK IN SHARE MODE;  -- 也可以加S锁

-- 事务T3:尝试加排他锁(阻塞)
BEGIN;
UPDATE account SET balance=500 WHERE id=1;  -- 等待T1和T2释放S锁
4.2.3 锁的兼容矩阵

锁的兼容性表:

复制代码
┌─────────────────────────────────────────┐
│       锁的兼容矩阵                      │
├──────────┬─────────────┬───────────────┤
│          │ 请求S锁     │ 请求X锁       │
├──────────┼─────────────┼───────────────┤
│ 已有S锁  │ ✓ 兼容      │ ✗ 不兼容      │
├──────────┼─────────────┼───────────────┤
│ 已有X锁  │ ✗ 不兼容    │ ✗ 不兼容      │
└──────────┴─────────────┴───────────────┘

✓ = 可以授予锁(兼容)
✗ = 必须等待(不兼容)

简记口诀:

复制代码
读读共享(S + S = ✓)
读写互斥(S + X = ✗)
写写互斥(X + X = ✗)
写读互斥(X + S = ✗)

4.3 封锁协议

封锁协议规定了何时加锁、何时解锁,不同协议解决不同的并发问题。

4.3.1 一级封锁协议
复制代码
┌─────────────────────────────────────────────────────────────────┐
│                一级封锁协议                                      │
├─────────────────────────────────────────────────────────────────┤
│                                                                 │
│  规则:                                                         │
│  • 事务修改数据前,必须加X锁                                    │
│  • 事务结束后释放X锁                                            │
│                                                                 │
│  能解决的问题:                                                 │
│  ✓ 丢失修改                                                     │
│                                                                 │
│  不能解决的问题:                                               │
│  ✗ 脏读                                                         │
│  ✗ 不可重复读                                                   │
│  ✗ 幻读                                                         │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘

示例:

sql 复制代码
-- 一级封锁协议
BEGIN;
LOCK-X(A);  -- 修改前加X锁
UPDATE account SET balance=500 WHERE id=A;
-- 读操作不需要加锁
SELECT * FROM account WHERE id=B;  -- 可能读到脏数据
COMMIT;  -- 释放所有锁
4.3.2 二级封锁协议
复制代码
┌─────────────────────────────────────────────────────────────────┐
│                二级封锁协议                                      │
├─────────────────────────────────────────────────────────────────┤
│                                                                 │
│  规则:                                                         │
│  • 一级封锁协议的基础上                                         │
│  • 读数据前加S锁                                                │
│  • 读完后立即释放S锁(不必等到事务结束)                        │
│                                                                 │
│  能解决的问题:                                                 │
│  ✓ 丢失修改                                                     │
│  ✓ 脏读                                                         │
│                                                                 │
│  不能解决的问题:                                               │
│  ✗ 不可重复读                                                   │
│  ✗ 幻读                                                         │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘

示例:

sql 复制代码
-- 二级封锁协议
BEGIN;
LOCK-S(A);  -- 读前加S锁
SELECT * FROM account WHERE id=A;  -- balance=1000
UNLOCK-S(A);  -- 读完立即释放
-- ...
LOCK-S(A);  -- 再次读取
SELECT * FROM account WHERE id=A;  -- 可能变成balance=500(不可重复读)
UNLOCK-S(A);
COMMIT;
4.3.3 三级封锁协议
复制代码
┌─────────────────────────────────────────────────────────────────┐
│                三级封锁协议                                      │
├─────────────────────────────────────────────────────────────────┤
│                                                                 │
│  规则:                                                         │
│  • 一级封锁协议的基础上                                         │
│  • 读数据前加S锁                                                │
│  • 事务结束后才释放S锁(持有到COMMIT)                          │
│                                                                 │
│  能解决的问题:                                                 │
│  ✓ 丢失修改                                                     │
│  ✓ 脏读                                                         │
│  ✓ 不可重复读                                                   │
│                                                                 │
│  不能解决的问题:                                               │
│  ✗ 幻读(需要更高级的锁机制)                                   │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘

示例:

sql 复制代码
-- 三级封锁协议
BEGIN;
LOCK-S(A);  -- 读前加S锁
SELECT * FROM account WHERE id=A;  -- balance=1000
-- 保持S锁不释放
-- ...
SELECT * FROM account WHERE id=A;  -- 仍然是balance=1000(可重复读)
COMMIT;  -- 事务结束才释放S锁
4.3.4 封锁协议对比
协议 写操作 读操作 解决的问题 对应隔离级别
一级 X锁到事务末 不加锁 丢失修改 -
二级 X锁到事务末 S锁读完即释放 丢失修改、脏读 READ COMMITTED
三级 X锁到事务末 S锁到事务末 丢失修改、脏读、不可重复读 REPEATABLE READ

4.4 两段锁协议(2PL)

4.4.1 两段锁协议原理

两段锁协议(Two-Phase Locking, 2PL) 是保证事务可串行化的重要协议。

复制代码
┌─────────────────────────────────────────────────────────────────┐
│                两段锁协议(2PL)                                 │
├─────────────────────────────────────────────────────────────────┤
│                                                                 │
│  两个阶段:                                                     │
│                                                                 │
│  1. 扩展阶段(Growing Phase)                                   │
│     • 事务可以获得锁                                            │
│     • 但不能释放锁                                              │
│                                                                 │
│  2. 收缩阶段(Shrinking Phase)                                 │
│     • 事务可以释放锁                                            │
│     • 但不能再获得新锁                                          │
│                                                                 │
│  关键点:一旦开始释放锁,就不能再加新锁!                       │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘

时间线示意:

复制代码
事务执行时间线
────────────────────────────────────────────────────>

扩展阶段          收缩阶段
├─────────────┼───────────────┤

加锁A           释放A
  加锁B           释放B
    加锁C           释放C

  获取锁 →     ← 释放锁

为什么2PL能保证可串行化?

复制代码
因为:
• 扩展阶段不释放锁 → 确保持有所有需要的资源
• 收缩阶段不加锁   → 避免与其他事务交叉
• 结果:事务之间有明确的顺序关系
4.4.2 两段锁协议的分类

1. 基本两段锁(Basic 2PL)

复制代码
• 遵循扩展-收缩规则
• 但可能导致级联回滚

2. 严格两段锁(Strict 2PL)

复制代码
• 所有X锁必须保持到事务COMMIT/ROLLBACK
• 避免级联回滚
• 大多数数据库采用这种方式

3. 强两段锁(Rigorous 2PL)

复制代码
• 所有锁(S锁和X锁)都保持到事务结束
• 最严格,性能最差

4.5 锁的粒度

锁的粒度是指锁定数据的范围大小。

4.5.1 行级锁
复制代码
┌─────────────────────────────────────────────────────────────────┐
│                    行级锁(Row Lock)                            │
├─────────────────────────────────────────────────────────────────┤
│                                                                 │
│  特点:                                                         │
│  • 锁定单个数据行                                               │
│  • 粒度最小                                                     │
│  • 并发度最高                                                   │
│  • 锁开销大(每行都需要锁)                                     │
│                                                                 │
│  使用场景:                                                     │
│  • OLTP系统                                                     │
│  • 高并发环境                                                   │
│  • 冲突少的场景                                                 │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘
sql 复制代码
-- 行级锁示例
UPDATE account SET balance=500 WHERE id=1;  -- 只锁定id=1的行
4.5.2 表级锁
复制代码
┌─────────────────────────────────────────────────────────────────┐
│                    表级锁(Table Lock)                          │
├─────────────────────────────────────────────────────────────────┤
│                                                                 │
│  特点:                                                         │
│  • 锁定整张表                                                   │
│  • 粒度最大                                                     │
│  • 并发度最低                                                   │
│  • 锁开销小(一个锁)                                           │
│                                                                 │
│  使用场景:                                                     │
│  • 批量操作(如导入数据)                                       │
│  • DDL操作(ALTER TABLE)                                       │
│  • 全表扫描                                                     │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘
sql 复制代码
-- 表级锁示例
LOCK TABLE account WRITE;  -- 锁定整张表
UPDATE account SET balance=balance*1.1;  -- 全表更新
UNLOCK TABLES;
4.5.3 页级锁
复制代码
┌─────────────────────────────────────────────────────────────────┐
│                    页级锁(Page Lock)                           │
├─────────────────────────────────────────────────────────────────┤
│                                                                 │
│  特点:                                                         │
│  • 锁定数据页(通常8KB)                                        │
│  • 粒度介于行锁和表锁之间                                       │
│  • 折中方案                                                     │
│                                                                 │
│  使用场景:                                                     │
│  • BerkeleyDB                                                   │
│  • SQL Server部分场景                                           │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘

粒度对比:

粒度 并发性 锁开销 使用场景
行级锁 ⭐⭐⭐⭐⭐ OLTP、高并发
页级锁 ⭐⭐⭐ 折中方案
表级锁 批量操作、DDL
4.5.4 意向锁

意向锁(Intention Lock) 是为了提高加表级锁的效率而引入的。

复制代码
┌─────────────────────────────────────────────────────────────────┐
│                    意向锁的作用                                  │
├─────────────────────────────────────────────────────────────────┤
│                                                                 │
│  问题:                                                         │
│  • 想给表加X锁                                                  │
│  • 需要检查表中所有行是否已被锁定                               │
│  • 如果表很大,检查非常耗时                                     │
│                                                                 │
│  解决:                                                         │
│  • 加行锁前,先在表上加意向锁                                   │
│  • 加表锁时,只需检查表的意向锁                                 │
│  • 大大提高效率!                                               │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘

意向锁类型:

复制代码
IS锁(Intention Shared)    - 意向共享锁
IX锁(Intention Exclusive) - 意向排他锁

示例:

sql 复制代码
-- 加行级X锁的完整流程
1. 在表上加IX锁
2. 在行上加X锁
3. 执行UPDATE
4. 释放锁

-- 加表级X锁时
1. 检查表的意向锁
2. 如果有IX锁,说明有行被锁定,等待
3. 否则加表级X锁

第五章 死锁问题概述

死锁是并发控制中最棘手的问题之一。

5.1 什么是死锁

死锁(Deadlock) 是指两个或多个事务互相等待对方释放锁,导致所有事务都无法继续执行的状态。

复制代码
┌─────────────────────────────────────────────────────────────────┐
│                    死锁示意图                                    │
├─────────────────────────────────────────────────────────────────┤
│                                                                 │
│  经典死锁场景:                                                 │
│                                                                 │
│  T1: 持有A的锁 → 等待B的锁                                      │
│         ↑              ↓                                        │
│         |              |                                        │
│         └──────┬───────┘                                        │
│                ×  互相等待(死锁!)                            │
│         ┌──────┴───────┐                                        │
│         |              |                                        │
│         ↓              ↑                                        │
│  T2: 持有B的锁 → 等待A的锁                                      │
│                                                                 │
│  结果:两个事务都卡住,永远无法继续!                           │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘

通俗理解:

死锁就像两辆车在窄路相遇,互不相让,谁也过不去。

死锁示例:

sql 复制代码
-- 事务T1
BEGIN;
UPDATE account SET balance=500 WHERE id=1;  -- 锁定账户1
-- 等待一会儿...
UPDATE account SET balance=600 WHERE id=2;  -- 尝试锁定账户2(等待T2)
COMMIT;

-- 事务T2(同时执行)
BEGIN;
UPDATE account SET balance=700 WHERE id=2;  -- 锁定账户2
-- 等待一会儿...
UPDATE account SET balance=800 WHERE id=1;  -- 尝试锁定账户1(等待T1)
COMMIT;

-- 结果:T1等T2释放账户2,T2等T1释放账户1,死锁!

5.2 死锁产生的条件

死锁产生需要同时满足以下四个条件(Coffman条件):

复制代码
┌─────────────────────────────────────────────────────────────────┐
│                死锁的四个必要条件                                │
├─────────────────────────────────────────────────────────────────┤
│                                                                 │
│  1. 互斥(Mutual Exclusion)                                    │
│     • 资源不能被共享                                            │
│     • 一次只能由一个事务使用                                    │
│                                                                 │
│  2. 持有并等待(Hold and Wait)                                 │
│     • 事务已持有至少一个资源                                    │
│     • 又在等待获取其他资源                                      │
│                                                                 │
│  3. 不可剥夺(No Preemption)                                   │
│     • 资源不能被强制抢占                                        │
│     • 只能由持有者主动释放                                      │
│                                                                 │
│  4. 循环等待(Circular Wait)                                   │
│     • 存在事务等待环路                                          │
│     • T1→T2→T3→...→T1                                           │
│                                                                 │
│  破坏任一条件,即可预防死锁!                                   │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘

5.3 死锁的预防

预防死锁就是破坏死锁产生的四个条件之一。

5.3.1 一次性封锁法

原理: 事务开始时一次性申请所有需要的锁,破坏"持有并等待"条件。

复制代码
┌─────────────────────────────────────────────────────────────────┐
│                一次性封锁法                                      │
├─────────────────────────────────────────────────────────────────┤
│                                                                 │
│  做法:                                                         │
│  1. 事务开始前,申请所有需要的锁                                │
│  2. 全部获得才开始执行                                          │
│  3. 执行完毕后一次性释放                                        │
│                                                                 │
│  优点:                                                         │
│  ✓ 完全避免死锁                                                 │
│                                                                 │
│  缺点:                                                         │
│  ✗ 降低并发度(资源利用率低)                                   │
│  ✗ 难以预知所有需要的资源                                       │
│  ✗ 可能导致饥饿                                                 │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘
sql 复制代码
-- 一次性封锁示例
BEGIN;
-- 一次性申请所有锁
SELECT * FROM account WHERE id IN (1,2,3) FOR UPDATE;
-- 执行所有操作
UPDATE account SET balance=balance-100 WHERE id=1;
UPDATE account SET balance=balance+50 WHERE id=2;
UPDATE account SET balance=balance+50 WHERE id=3;
COMMIT;
5.3.2 顺序封锁法

原理: 所有事务按照相同的顺序申请锁,破坏"循环等待"条件。

复制代码
┌─────────────────────────────────────────────────────────────────┐
│                顺序封锁法                                        │
├─────────────────────────────────────────────────────────────────┤
│                                                                 │
│  做法:                                                         │
│  1. 给所有数据对象编号(如主键)                                │
│  2. 所有事务按照编号顺序申请锁                                  │
│  3. 保证不会出现环路等待                                        │
│                                                                 │
│  示例:                                                         │
│  • 总是先锁id小的,再锁id大的                                   │
│  • T1: 锁1 → 锁2                                                │
│  • T2: 锁1 → 锁2(都先锁1,不会死锁)                           │
│                                                                 │
│  优点:                                                         │
│  ✓ 有效预防死锁                                                 │
│  ✓ 并发度较高                                                   │
│                                                                 │
│  缺点:                                                         │
│  ✗ 需要应用层遵守规则                                           │
│  ✗ 难以在复杂系统中维护                                         │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘
sql 复制代码
-- 顺序封锁示例
-- 错误做法(可能死锁):
BEGIN;
UPDATE account SET balance=balance-100 WHERE id=2;  -- 先锁2
UPDATE account SET balance=balance+100 WHERE id=1;  -- 再锁1
COMMIT;

-- 正确做法(按ID顺序):
BEGIN;
UPDATE account SET balance=balance+100 WHERE id=1;  -- 先锁1(小的)
UPDATE account SET balance=balance-100 WHERE id=2;  -- 再锁2(大的)
COMMIT;

5.4 死锁的检测

如果无法预防死锁,就需要检测死锁并解除。

5.4.1 等待图法

原理: 构建事务等待图,检测是否有环路。

复制代码
┌─────────────────────────────────────────────────────────────────┐
│                等待图(Wait-For Graph)                          │
├─────────────────────────────────────────────────────────────────┤
│                                                                 │
│  图的构建:                                                     │
│  • 节点:事务                                                   │
│  • 边:  T1→T2 表示T1等待T2释放锁                               │
│                                                                 │
│  死锁检测:                                                     │
│  • 如果图中有环,则存在死锁                                     │
│  • 定期运行检测算法                                             │
│                                                                 │
│  示例:                                                         │
│                                                                 │
│      T1 ──→ T2                                                  │
│      ↑       ↓                                                  │
│      └───────┘                                                  │
│    有环!存在死锁                                               │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘

检测算法:

复制代码
1. 构建等待图
2. 使用DFS/BFS检测环
3. 如果发现环,确认死锁
4. 选择牺牲者(victim)回滚
5.4.2 超时机制

原理: 如果事务等待超过一定时间,认为发生死锁。

复制代码
┌─────────────────────────────────────────────────────────────────┐
│                超时检测法                                        │
├─────────────────────────────────────────────────────────────────┤
│                                                                 │
│  机制:                                                         │
│  • 设置锁等待超时时间(如30秒)                                 │
│  • 超时后自动回滚事务                                           │
│  • 简单但不精确                                                 │
│                                                                 │
│  优点:                                                         │
│  ✓ 实现简单                                                     │
│  ✓ 开销小                                                       │
│                                                                 │
│  缺点:                                                         │
│  ✗ 可能误判(不是死锁只是慢)                                   │
│  ✗ 超时时间难以设置                                             │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘
sql 复制代码
-- MySQL设置锁等待超时
SET innodb_lock_wait_timeout = 50;  -- 50秒

BEGIN;
UPDATE account SET balance=500 WHERE id=1;
-- 如果等待超过50秒,自动回滚

5.5 死锁的解除

检测到死锁后,需要选择一个事务回滚。

复制代码
┌─────────────────────────────────────────────────────────────────┐
│                死锁解除策略                                      │
├─────────────────────────────────────────────────────────────────┤
│                                                                 │
│  1. 选择牺牲者(Victim Selection)                             │
│                                                                 │
│  选择标准:                                                     │
│  • 执行时间最短的(浪费小)                                     │
│  • 修改数据最少的(回滚成本低)                                 │
│  • 优先级最低的                                                 │
│  • 已回滚次数最少的(避免饥饿)                                 │
│                                                                 │
│  2. 回滚级别                                                    │
│                                                                 │
│  • 完全回滚:撤销事务的所有操作                                 │
│  • 部分回滚:只回滚到打破死锁为止                               │
│                                                                 │
│  3. 重启事务                                                    │
│                                                                 │
│  • 自动重试被回滚的事务                                         │
│  • 设置最大重试次数                                             │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘

数据库的死锁处理:

复制代码
MySQL:
• 自动检测死锁
• 选择回滚代价小的事务
• 返回错误:ERROR 1213: Deadlock found

PostgreSQL:
• 超时机制
• 返回错误:ERROR: deadlock detected

Oracle:
• 自动检测死锁
• 回滚其中一个事务
• 返回:ORA-00060: deadlock detected

5.6 活锁与饥饿

除了死锁,还有两个相关问题:

活锁(Livelock):

复制代码
┌─────────────────────────────────────────────────────────────────┐
│                    活锁                                          │
├─────────────────────────────────────────────────────────────────┤
│                                                                 │
│  定义:                                                         │
│  • 事务不断重试,但始终无法完成                                 │
│  • 系统看起来在运行,实际没有进展                               │
│                                                                 │
│  类比:                                                         │
│  • 两个人在走廊相遇                                             │
│  • 都想让对方先过                                               │
│  • 结果同时左右移动,谁也过不去                                 │
│                                                                 │
│  解决:                                                         │
│  • 引入随机延迟                                                 │
│  • 设置优先级                                                   │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘

饥饿(Starvation):

复制代码
┌─────────────────────────────────────────────────────────────────┐
│                    饥饿                                          │
├─────────────────────────────────────────────────────────────────┤
│                                                                 │
│  定义:                                                         │
│  • 某个事务一直得不到所需资源                                   │
│  • 永远无法执行                                                 │
│                                                                 │
│  原因:                                                         │
│  • 优先级调度不公平                                             │
│  • 高优先级事务不断插队                                         │
│  • 低优先级事务一直等待                                         │
│                                                                 │
│  解决:                                                         │
│  • 先来先服务(FCFS)                                           │
│  • 老化(Aging):等待时间越长,优先级越高                      │
│  • 公平调度算法                                                 │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘

三者对比:

问题 特征 表现 解决难度
死锁 互相等待,完全卡住 事务挂起 容易检测和解除
活锁 不断重试,无进展 CPU忙但无效 较难检测
饥饿 永远得不到资源 事务一直等待 需要公平调度

第六章 并发控制技术总览

数据库采用多种并发控制技术,各有优缺点。

6.1 基于封锁的并发控制

基于封锁的并发控制是最传统和最广泛使用的技术。

复制代码
┌─────────────────────────────────────────────────────────────────┐
│                基于封锁的并发控制                                │
├─────────────────────────────────────────────────────────────────┤
│                                                                 │
│  核心思想:                                                      │
│  • 访问数据前先加锁                                             │
│  • 其他事务必须等待                                             │
│  • 访问完毕后释放锁                                             │
│                                                                 │
│  优点:                                                          │
│  ✓ 实现简单                                                     │
│  ✓ 效果可靠                                                     │
│  ✓ 广泛应用                                                     │
│                                                                 │
│  缺点:                                                          │
│  ✗ 可能死锁                                                     │
│  ✗ 锁开销大                                                     │
│  ✗ 并发度受限                                                   │
│                                                                 │
│  使用场景:                                                      │
│  • 所有主流数据库                                               │
│  • 写多读少的场景                                               │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘

6.2 基于时间戳的并发控制

6.2.1 时间戳原理

**时间戳并发控制(Timestamp-based CC)**使用时间戳来确定事务顺序。

复制代码
┌─────────────────────────────────────────────────────────────────┐
│                时间戳并发控制                                    │
├─────────────────────────────────────────────────────────────────┤
│                                                                 │
│  核心思想:                                                      │
│  • 每个事务有唯一的时间戳TS(T)                                  │
│  • 每个数据项记录:                                              │
│    - 最后读的时间戳 Read-TS(X)                                  │
│    - 最后写的时间戳 Write-TS(X)                                 │
│  • 根据时间戳顺序决定是否允许操作                               │
│                                                                 │
│  读规则:                                                        │
│  • 如果TS(T) < Write-TS(X)  → 拒绝(读过时数据)                  │
│  • 否则允许读,并更新Read-TS(X)                                  │
│                                                                 │
│  写规则:                                                        │
│  • 如果TS(T) < Read-TS(X)   → 拒绝(覆盖新数据)                  │
│  • 如果TS(T) < Write-TS(X)  → 拒绝(覆盖新数据)                  │
│  • 否则允许写,并更新Write-TS(X)                                 │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘

优缺点:

复制代码
优点:
✓ 无死锁(不用锁)
✓ 读事务不会被阻塞

缺点:
✗ 冲突时需要回滚
✗ 时间戳管理开销
✗ 级联回滚问题
6.2.2 时间戳排序协议

基本时间戳排序(Basic TO):

复制代码
按照事务时间戳顺序执行,保证可串行化

6.3 乐观并发控制

6.3.1 乐观控制原理

**乐观并发控制(Optimistic CC)**假设冲突很少,提交时才检查。

复制代码
┌─────────────────────────────────────────────────────────────────┐
│                乐观并发控制                                      │
├─────────────────────────────────────────────────────────────────┤
│                                                                 │
│  哲学思想:                                                      │
│  • 假设:冲突很少发生                                            │
│  • 策略:先执行,提交时检查冲突                                   │
│  • 如果有冲突,再回滚                                            │
│                                                                 │
│  类比:                                                          │
│  • 悲观锁:出门前先锁门(加锁)                                    │
│  • 乐观锁:回家时检查门锁(提交时检查)                            │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘
6.3.2 三阶段协议
复制代码
┌─────────────────────────────────────────────────────────────────┐
│            乐观并发控制三阶段                                    │
├─────────────────────────────────────────────────────────────────┤
│                                                                 │
│  1. 读阶段(Read Phase)                                          │
│     • 事务读取数据到私有工作区                                  │
│     • 所有修改也在私有区进行                                    │
│     • 不影响其他事务                                            │
│                                                                 │
│  2. 验证阶段(Validation Phase)                                  │
│     • 检查是否与其他事务冲突                                    │
│     • 读集、写集检查                                            │
│     • 有冲突则回滚                                              │
│                                                                 │
│  3. 写阶段(Write Phase)                                         │
│     • 验证通过后                                                │
│     • 将私有工作区的修改写入数据库                              │
│     • 成为永久性修改                                            │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘

优缺点:

复制代码
优点:
✓ 读阶段不加锁,性能好
✓ 无死锁
✓ 适合读多写少的场景

缺点:
✗ 验证和回滚开销
✗ 写冲突多时性能差
✗ 饥饿问题

应用场景:

复制代码
• 缓存更新(如version字段)
• 文档编辑系统
• 源代码版本控制

6.4 多版本并发控制(MVCC)

6.4.1 MVCC原理

**多版本并发控制(Multi-Version Concurrency Control)**是现代数据库的主流技术。

复制代码
┌─────────────────────────────────────────────────────────────────┐
│                    MVCC核心思想                                  │
├─────────────────────────────────────────────────────────────────┤
│                                                                 │
│  基本原理:                                                      │
│  • 为每个数据保存多个版本                                       │
│  • 每个版本有时间戳或事务ID                                     │
│  • 读事务看到一致的快照                                         │
│  • 写事务创建新版本                                             │
│                                                                 │
│  关键优势:                                                      │
│  • 读不阻塞写                                                   │
│  • 写不阻塞读                                                   │
│  • 极大提高并发性                                               │
│                                                                 │
│  示例:                                                          │
│                                                                 │
│  数据X的版本链:                                                 │
│  X₁(TS=100) → X₂(TS=200) → X₃(TS=300)                           │
│                                                                 │
│  事务T(TS=250):                                                 │
│  • 读X → 看到X₂版本(TS=200)                                     │
│  • 看不到X₃(TS=300 > 250)                                       │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘

MVCC的实现:

复制代码
┌─────────────────────────────────────────────────────────────────┐
│                MVCC实现机制                                      │
├─────────────────────────────────────────────────────────────────┤
│                                                                 │
│  1. 版本存储                                                    │
│     • 在记录中保存多个版本                                      │
│     • 或在Undo Log中保存历史版本                                │
│                                                                 │
│  2. 版本选择                                                    │
│     • 根据事务的可见性规则                                      │
│     • 选择合适的版本读取                                        │
│                                                                 │
│  3. 垃圾回收                                                    │
│     • 定期清理不再需要的旧版本                                  │
│     • 释放存储空间                                              │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘
6.4.2 MVCC的优势
复制代码
┌─────────────────────────────────────────────────────────────────┐
│                    MVCC优势                                      │
├─────────────────────────────────────────────────────────────────┤
│                                                                 │
│  1. 高并发性                                                    │
│     ✓ 读写不互斥                                                │
│     ✓ 多个读事务并发执行                                        │
│     ✓ 极大提升性能                                              │
│                                                                 │
│  2. 一致性快照                                                  │
│     ✓ 事务看到一致的数据视图                                    │
│     ✓ 自然支持可重复读                                          │
│     ✓ 无需长时间持有锁                                          │
│                                                                 │
│  3. 无读锁                                                      │
│     ✓ 读操作几乎不加锁                                          │
│     ✓ 减少锁竞争                                                │
│     ✓ 降低死锁概率                                              │
│                                                                 │
│  4. 支持时间旅行                                                │
│     ✓ 可以查询历史数据                                          │
│     ✓ 闪回查询功能                                              │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘

代价:

复制代码
✗ 存储开销(保存多版本)
✗ 垃圾回收开销
✗ 写操作需要创建新版本
6.4.3 MVCC在主流数据库中的应用

PostgreSQL:

复制代码
• 每行数据有xmin和xmax字段
• xmin:创建该版本的事务ID
• xmax:删除该版本的事务ID
• 根据事务ID判断可见性

MySQL InnoDB:

复制代码
• Undo Log保存历史版本
• 每行有DBTRX_ID(事务ID)和DBROLL_PTR(回滚指针)
• Read View判断版本可见性
• 结合Next-Key Lock防止幻读

Oracle:

复制代码
• Undo表空间保存旧版本
• SCN(System Change Number)标识版本
• 支持闪回查询(Flashback Query)

6.5 并发控制技术对比

技术 原理 优点 缺点 代表数据库
基于锁 访问前加锁 简单可靠 可能死锁,性能差 所有数据库
时间戳 时间戳排序 无死锁 回滚多 部分学术系统
乐观控制 提交时检查 读快 冲突多时性能差 应用层实现
MVCC 多版本 读写不冲突 存储开销大 PostgreSQL,MySQL,Oracle

选择建议:

复制代码
┌─────────────────────────────────────────────────────────────────┐
│                技术选择指南                                      │
├─────────────────────────────────────────────────────────────────┤
│                                                                 │
│  读多写少:                                                      │
│  → MVCC或乐观控制                                               │
│                                                                 │
│  写多读少:                                                      │
│  → 基于锁                                                       │
│                                                                 │
│  极高并发:                                                      │
│  → MVCC                                                         │
│                                                                 │
│  简单系统:                                                      │
│  → 基于锁                                                       │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘

第七章 面试与考试重点

7.1 理论知识要点

必须掌握的核心概念:

复制代码
1. 四大并发问题
   • 丢失修改 ⭐⭐⭐⭐⭐
   • 脏读 ⭐⭐⭐⭐
   • 不可重复读 ⭐⭐⭐
   • 幻读 ⭐⭐⭐

2. 事务ACID特性
   • 原子性 → Undo Log
   • 一致性 → 约束检查
   • 隔离性 → 并发控制
   • 持久性 → Redo Log

3. 隔离级别
   • 读未提交 → 几乎不用
   • 读已提交 → Oracle默认
   • 可重复读 → MySQL默认
   • 串行化 → 最严格

4. 封锁协议
   • 一级封锁协议 → 防丢失修改
   • 二级封锁协议 → 防脏读
   • 三级封锁协议 → 防不可重复读
   • 两段锁协议 → 保证可串行化

5. 死锁
   • 四个必要条件
   • 预防方法
   • 检测方法
   • 解除方法

7.2 经典面试题

问题1:解释四大并发问题及解决方法

复制代码
答题要点:
1. 每个问题给出定义
2. 举一个生活化的例子
3. 说明严重性
4. 给出解决方案(隔离级别或锁)

示例:
脏读:事务读取了另一个未提交事务的数据。
例如:银行转账未提交,但ATM显示了余额增加,如果转账回滚就出问题。
解决:使用READ COMMITTED或更高隔离级别。

问题2:讲解事务隔离级别

复制代码
答题框架:
1. 四个级别从低到高
2. 每个级别能解决什么问题
3. 性能vs安全性权衡
4. 主流数据库默认级别

关键点:
• READ UNCOMMITTED → 允许所有问题
• READ COMMITTED → 防脏读
• REPEATABLE READ → 防脏读+不可重复读
• SERIALIZABLE → 防所有问题

问题3:什么是两段锁协议?为什么能保证可串行化?

复制代码
答题要点:
1. 定义两阶段:扩展阶段和收缩阶段
2. 扩展阶段只能加锁不能解锁
3. 收缩阶段只能解锁不能加锁
4. 保证可串行化原因:
   • 事务持有所有需要的锁
   • 不会与其他事务交叉
   • 形成严格的执行顺序

问题4:死锁的四个必要条件及预防方法

复制代码
答题框架:
1. 四个条件:
   • 互斥
   • 持有并等待
   • 不可剥夺
   • 循环等待

2. 预防方法(破坏其中一个):
   • 一次性封锁 → 破坏持有并等待
   • 顺序封锁 → 破坏循环等待
   • 抢占 → 破坏不可剥夺

3. 检测与解除:
   • 等待图法
   • 超时机制
   • 选择牺牲者回滚

问题5:解释MVCC及其优势

复制代码
答题要点:
1. 定义:多版本并发控制
2. 原理:为数据维护多个版本
3. 优势:
   • 读不阻塞写
   • 写不阻塞读
   • 极大提高并发性
4. 应用:PostgreSQL、MySQL InnoDB、Oracle
5. 代价:存储开销、垃圾回收

7.3 实践应用题

题目1:设计转账事务

sql 复制代码
-- 问:如何设计一个安全的转账事务?

-- 答案:
BEGIN TRANSACTION;
-- 1. 加排他锁防止并发修改
SELECT balance FROM account 
WHERE id IN (1, 2) FOR UPDATE;

-- 2. 检查余额
IF (SELECT balance FROM account WHERE id=1) < 100 THEN
    ROLLBACK;
    RETURN 'insufficient balance';
END IF;

-- 3. 执行转账
UPDATE account SET balance = balance - 100 WHERE id=1;
UPDATE account SET balance = balance + 100 WHERE id=2;

COMMIT;

-- 关键点:
-- • FOR UPDATE加锁
-- • 检查余额
-- • 按ID顺序锁定(防死锁)
-- • 使用事务保证原子性

题目2:避免死锁的订单系统

sql 复制代码
-- 问:订单系统如何避免死锁?

-- 错误做法:
BEGIN;
UPDATE inventory SET stock=stock-10 WHERE product_id=100;
UPDATE orders SET status='paid' WHERE order_id=1;
COMMIT;

-- 正确做法:
BEGIN;
-- 1. 按照固定顺序锁定资源
-- 2. 先锁ID小的表,再锁ID大的表
-- 3. 同一张表内先锁ID小的行

-- 方案1:顺序封锁
SELECT * FROM inventory 
WHERE product_id IN (100,200) 
ORDER BY product_id  -- 按ID排序!
FOR UPDATE;

-- 方案2:一次性封锁
SELECT * FROM inventory WHERE product_id IN (100,200) FOR UPDATE;
SELECT * FROM orders WHERE order_id IN (1,2) FOR UPDATE;
-- 然后执行更新...

COMMIT;

题目3:选择合适的隔离级别

复制代码
场景1:银行转账
→ 使用SERIALIZABLE
理由:金融数据,必须绝对准确

场景2:电商库存查询
→ 使用READ COMMITTED
理由:允许不可重复读,性能优先

场景3:报表统计
→ 使用REPEATABLE READ
理由:需要一致性快照,防止数据前后矛盾

场景4:在线人数统计
→ 使用READ UNCOMMITTED
理由:实时性重要,容忍一定误差

7.4 易混淆概念辨析

1. 不可重复读 vs 幻读

对比维度 不可重复读 幻读
变化内容 已有记录的值 记录总数
触发操作 UPDATE/DELETE INSERT
影响范围 单条记录 查询结果集
解决方法 行锁 间隙锁/表锁

2. 封锁协议 vs 隔离级别

复制代码
封锁协议:
• 理论概念
• 描述加锁解锁规则
• 一级、二级、三级、两段锁

隔离级别:
• 实际配置
• 用户可设置
• READ UNCOMMITTED、READ COMMITTED等

关系:
封锁协议是隔离级别的实现机制

3. 悲观锁 vs 乐观锁

复制代码
悲观锁:
• 假设冲突会发生
• 先加锁再操作
• 数据库的锁机制
• 适合写多的场景

乐观锁:
• 假设冲突很少
• 先操作后检查
• 版本号机制
• 适合读多写少

4. 共享锁 vs 排他锁

复制代码
共享锁(S锁):
• 多个事务可同时持有
• 读读兼容
• 读写互斥

排他锁(X锁):
• 只能一个事务持有
• 与任何锁都互斥
• 保护写操作

记忆:
读读共享(S+S=✓)
其他都互斥

核心口诀:

复制代码
并发问题四兄弟:丢修脏读不重幻
隔离级别分四档:未提已提可重串
ACID四特性:原一隔持
封锁三兄弟:一二三级各不同
死锁四条件:互持不循环
MVCC是王道:读写不冲突

附录

附录A:并发控制相关SQL

设置隔离级别:

sql 复制代码
-- MySQL
SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;
SET GLOBAL TRANSACTION ISOLATION LEVEL REPEATABLE READ;

-- PostgreSQL
SET TRANSACTION ISOLATION LEVEL SERIALIZABLE;

-- SQL Server
SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;

加锁语法:

sql 复制代码
-- 共享锁
SELECT * FROM account WHERE id=1 LOCK IN SHARE MODE;  -- MySQL
SELECT * FROM account WHERE id=1 FOR SHARE;  -- PostgreSQL

-- 排他锁
SELECT * FROM account WHERE id=1 FOR UPDATE;  -- 通用

-- 表级锁
LOCK TABLE account IN SHARE MODE;  -- Oracle/PostgreSQL
LOCK TABLES account WRITE;  -- MySQL

死锁检测:

sql 复制代码
-- MySQL查看死锁日志
SHOW ENGINE INNODB STATUS;

-- PostgreSQL查看锁等待
SELECT * FROM pg_locks WHERE NOT granted;

-- 查看当前事务
SELECT * FROM information_schema.innodb_trx;

附录B:主流数据库并发控制机制

MySQL InnoDB:

复制代码
并发控制:
• MVCC + 锁
• 默认隔离级别:REPEATABLE READ
• 支持行锁、表锁、间隙锁
• Next-Key Lock防止幻读

锁类型:
• 共享锁(S)
• 排他锁(X)
• 意向锁(IS、IX)
• 记录锁(Record Lock)
• 间隙锁(Gap Lock)
• Next-Key Lock(Record + Gap)

MVCC实现:
• Undo Log保存历史版本
• Read View判断可见性
• 每行有隐藏字段:DBTRX_ID、DBROLL_PTR

PostgreSQL:

复制代码
并发控制:
• MVCC
• 默认隔离级别:READ COMMITTED
• 支持行锁、表锁、Advisory Lock

MVCC实现:
• 每行有xmin、xmax字段
• 事务ID判断可见性
• VACUUM回收旧版本

特色:
• SSI(Serializable Snapshot Isolation)
• 真正的无死锁串行化

Oracle:

复制代码
并发控制:
• MVCC
• 默认隔离级别:READ COMMITTED
• 多粒度锁

MVCC实现:
• Undo表空间
• SCN(System Change Number)
• 支持闪回查询

特色:
• 读不加锁
• 极高的并发性能

SQL Server:

复制代码
并发控制:
• 锁 + 可选MVCC
• 默认隔离级别:READ COMMITTED
• 支持行版本控制

锁类型:
• 共享锁、排他锁、更新锁
• 意向锁、架构锁、批量更新锁

特色:
• 可配置快照隔离
• 自动死锁检测

附录C:推荐学习资源

经典书籍:

复制代码
1. 《数据库系统概念》(Database System Concepts)
   • Abraham Silberschatz等著
   • 权威教材,理论全面

2. 《数据库系统实现》(Database System Implementation)
   • Hector Garcia-Molina等著
   • 深入实现细节

3. 《高性能MySQL》
   • Baron Schwartz等著
   • InnoDB锁和MVCC讲解详细

4. 《PostgreSQL技术内幕》
   • MVCC实现原理

在线资源:

复制代码
• MySQL官方文档 - InnoDB锁章节
• PostgreSQL官方文档 - 并发控制
• 数据库内核月报(阿里云)
• CMU 15-445数据库系统课程

附录D:术语表

中文术语 英文术语 缩写 说明
并发控制 Concurrency Control CC 协调并发事务访问
原子性 Atomicity A ACID之A
一致性 Consistency C ACID之C
隔离性 Isolation I ACID之I
持久性 Durability D ACID之D
排他锁 Exclusive Lock X锁 写锁
共享锁 Shared Lock S锁 读锁
两段锁 Two-Phase Locking 2PL 保证可串行化
死锁 Deadlock - 互相等待
活锁 Livelock - 不断重试无进展
饥饿 Starvation - 永远得不到资源
多版本并发控制 Multi-Version CC MVCC 现代主流技术
乐观并发控制 Optimistic CC OCC 提交时检查
时间戳排序 Timestamp Ordering TO 无锁技术
可串行化 Serializability - 最高隔离级别
脏读 Dirty Read - 读未提交数据
不可重复读 Non-Repeatable Read - 多次读结果不同
幻读 Phantom Read - 插入导致记录数变化
丢失修改 Lost Update - 修改被覆盖

注: 本文是并发控制的总览文章,具体技术细节(封锁协议、事务管理、死锁处理)将在后续独立文章中详细讲解。

复制代码
相关推荐
银发控、2 小时前
数据库隔离级别与三个问题(脏读、不可重复读、幻读)
数据库·面试
爱可生开源社区2 小时前
MySQL 性能优化:真正重要的变量
数据库·mysql
茶杯梦轩2 小时前
从零起步学习并发编程 || 第九章:Future 类详解及CompletableFuture 类在项目实战中的应用
服务器·后端·面试
ZeroNews内网穿透2 小时前
谷歌封杀OpenClaw背后:本地部署或是出路
运维·服务器·数据库·安全
~远在太平洋~2 小时前
Linux 基础命令
linux·服务器·数据库
小马爱打代码2 小时前
MySQL性能优化核心:InnoDB Buffer Pool 详解
数据库·mysql·性能优化
2501_946205523 小时前
晶圆机器人双臂怎么选型?适配2-12寸晶圆的末端效应器有哪些?
服务器·网络·机器人
linux kernel3 小时前
第七部分:高级IO
服务器·网络
田井中律.3 小时前
服务器部署问题汇总(ubuntu24.04.3)
运维·服务器