事务与并发控制:当多人同时操作数据库

事务与并发控制:当多人同时操作数据库

一句话总结:事务通过 ACID 特性保证操作的完整性;并发控制通过锁机制和多版本并发控制(MVCC)协调多个事务同时执行,防止丢失修改、读脏数据、不可重复读和幻读四大问题。


一、从一个生活场景说起

想象你和同事同时操作公司的财务数据库:

  • :从 A 账户转 1000 元到 B 账户
  • 同事:同时查询 A 账户的余额用于报表

如果没有事务和并发控制,会发生什么?

  • 你的转账做了一半(A 已扣款,B 还没加款),系统崩溃了------钱去哪了?
  • 同事读到了你"扣款后、加款前"的中间状态------报表数据是错的!
  • 两人同时修改同一条记录------一个人的修改被另一个人覆盖!

这些问题,正是事务与并发控制要解决的核心。


二、事务:要么全做,要么全不做

2.1 什么是事务?

事务(Transaction)是用户定义的一个数据库操作序列,这些操作要么全部执行,要么全部不执行,是一个不可分割的工作单位。

2.2 事务的 ACID 特性

特性 英文 含义 类比
原子性 Atomicity 事务中的操作要么全部成功,要么全部回滚 转账:扣款和加款必须同时完成
一致性 Consistency 事务执行前后,数据库从一个一致状态变到另一个一致状态 转账前后,总金额不变
隔离性 Isolation 多个事务并发执行时,互不干扰 你转账,同事查账,互不干扰
持久性 Durability 事务一旦提交,结果永久保存 转账成功后,即使断电,钱也不会变回来

2.3 SQL 中的事务控制

sql 复制代码
-- 开始事务
BEGIN TRANSACTION;  -- 或 START TRANSACTION;

-- 执行操作
UPDATE 账户 SET 余额 = 余额 - 1000 WHERE 账户号 = 'A';
UPDATE 账户 SET 余额 = 余额 + 1000 WHERE 账户号 = 'B';

-- 提交事务(永久保存)
COMMIT;

-- 或者:回滚事务(撤销所有操作)
-- ROLLBACK;

2.4 事务的状态

复制代码
              BEGIN
                │
                ▼
        ┌───────────────┐
        │   活动状态      │  ← 正在执行操作
        └───────┬───────┘
                │
        ┌───────┴───────┐
        ▼               ▼
   ┌─────────┐     ┌─────────┐
   │ 部分提交 │     │ 失败状态 │
   │ (COMMIT)│     │ (ROLLBACK)
   └────┬────┘     └────┬────┘
        │               │
        ▼               ▼
   ┌─────────┐     ┌─────────┐
   │ 提交状态 │     │ 中止状态 │
   │ (持久化)│     │ (回滚完成)
   └─────────┘     └─────────┘

三、并发操作带来的四大问题

当多个事务同时操作数据库时,如果不加控制,会出现以下问题:

3.1 丢失修改(Lost Update)

两个事务同时读取同一数据,然后都修改,后提交的事务覆盖了先提交的事务。

时间 事务 A 事务 B
T1 读取余额 = 1000
T2 读取余额 = 1000
T3 余额 = 1000 + 100 = 1100
T4 余额 = 1000 + 200 = 1200
T5 提交
T6 提交(覆盖了 A 的修改)

结果:A 的 +100 操作丢失了,最终余额是 1200 而非 1300。

3.2 读脏数据(Dirty Read)

一个事务读取了另一个事务尚未提交的数据,而那个事务后来回滚了。

时间 事务 A 事务 B
T1 修改余额为 1200(未提交)
T2 读取余额 = 1200(脏数据)
T3 回滚(余额恢复为 1000)
T4 基于 1200 做决策(错误!)

3.3 不可重复读(Non-repeatable Read)

同一事务内,两次读取同一数据,结果不一样(因为另一个事务在两次读取之间修改并提交了)。

时间 事务 A 事务 B
T1 读取余额 = 1000
T2 修改余额 = 800,提交
T3 再次读取余额 = 800(变了!)

不可重复读强调"同一事务内两次读取结果不一致"。

3.4 幻读(Phantom Read)

同一事务内,两次查询条件相同的数据集合,但第二次出现了新的行(另一个事务插入并提交)。

时间 事务 A 事务 B
T1 SELECT COUNT(*) FROM 学生 WHERE 系='计算机' → 100
T2 INSERT 一名计算机系学生,提交
T3 再次执行相同查询 → 101(幻行出现!)

幻读和不可重复读的区别:不可重复读是某行的值变了,幻读是行数变了。


四、事务隔离级别:在并发与性能之间权衡

SQL 标准定义了四种隔离级别,级别越高,并发问题越少,但性能越低。

隔离级别 丢失修改 脏读 不可重复读 幻读 实现复杂度
READ UNCOMMITTED
READ COMMITTED
REPEATABLE READ
SERIALIZABLE 最高

❌ = 不会发生,✅ = 可能发生

4.1 设置隔离级别

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

-- PostgreSQL
SET TRANSACTION ISOLATION LEVEL SERIALIZABLE;

-- SQL Server
SET TRANSACTION ISOLATION LEVEL READ COMMITTED;

4.2 各数据库默认隔离级别

数据库 默认隔离级别 说明
MySQL (InnoDB) REPEATABLE READ 默认解决不可重复读,但可能有幻读
PostgreSQL READ COMMITTED 默认解决脏读
Oracle READ COMMITTED
SQL Server READ COMMITTED

五、并发控制:锁机制

5.1 锁的基本类型

锁类型 英文 含义 兼容
共享锁 S Lock / Shared Lock 读锁,可以多人同时加 S 与 S 兼容,S 与 X 不兼容
排他锁 X Lock / Exclusive Lock 写锁,只能一人持有 X 与任何锁都不兼容

5.2 锁的粒度

粒度 范围 并发度 开销
行锁 一行
页锁 一页(通常 8KB)
表锁 整张表
数据库锁 整个库 最低 最小

InnoDB 默认使用行锁 ,并发度高;MyISAM 使用表锁,并发度低。

5.3 封锁协议

一级封锁协议
  • 事务修改数据前加 X 锁,直到事务结束才释放
  • 解决:丢失修改
二级封锁协议
  • 一级协议 + 读取数据前加 S 锁,读完后立即释放
  • 解决:丢失修改 + 读脏数据
三级封锁协议
  • 一级协议 + 读取数据前加 S 锁,直到事务结束才释放

  • 解决:丢失修改 + 读脏数据 + 不可重复读

    ┌────────────────────────────────────────┐
    │ 隔离级别 vs 封锁协议对应关系 │
    ├────────────────────────────────────────┤
    │ READ UNCOMMITTED → 无锁或最低级别 │
    │ READ COMMITTED → 二级封锁协议 │
    │ REPEATABLE READ → 三级封锁协议 │
    │ SERIALIZABLE → 三级封锁协议 + 锁全表│
    └────────────────────────────────────────┘

5.4 活锁与死锁

活锁(Livelock)

事务 A 申请锁,事务 B 也申请同一锁,但 B 优先级更高,A 一直等待,永远得不到执行。

解决:采用先来先服务(FIFO)策略,避免优先级导致的饥饿。

死锁(Deadlock)

两个事务互相等待对方持有的锁,形成循环等待。

时间 事务 A 事务 B
T1 加 X 锁 on 记录 1
T2 加 X 锁 on 记录 2
T3 请求 X 锁 on 记录 2(等待 B)
T4 请求 X 锁 on 记录 1(等待 A)
T5 死锁! 死锁!

死锁的解决策略

  • 预防:一次加锁法(事务开始时一次性加所有需要的锁)、顺序加锁法(按固定顺序加锁)
  • 检测与解除:设置超时时间,超时后自动回滚其中一个事务(牺牲者)
  • 等待图:DBMS 维护等待图,发现环就选择一个事务回滚

六、MVCC:多版本并发控制

6.1 什么是 MVCC?

MVCC(Multi-Version Concurrency Control)是不用锁也能实现并发控制的技术。核心思想:

每个数据项都有多个版本,读操作读取的是历史版本 ,写操作创建新版本,读和写互不阻塞。

6.2 MVCC 如何工作(以 InnoDB 为例)

InnoDB 的每行数据都有两个隐藏列:

  • DB_TRX_ID:创建该版本的事务 ID
  • DB_ROLL_PTR:回滚指针,指向该版本的上一个版本(形成版本链)

读操作:根据事务的 Read View(可见性判断规则),找到对该事务可见的最新版本。

写操作:不直接修改原数据,而是创建新版本的行,旧版本保留在 Undo 日志中。

6.3 MVCC 的优势

  • 读不阻塞写,写不阻塞读:读操作读取历史版本,不会等待写锁
  • 高并发性能:避免了大量锁等待
  • 实现快照读:REPEATABLE READ 隔离级别下,事务内两次读取结果一致

七、动手练习

练习 1:事务基础

在 MySQL 中执行以下操作,观察事务的行为:

sql 复制代码
START TRANSACTION;
SELECT 余额 FROM 账户 WHERE 账户号 = 'A';  -- 记录结果
UPDATE 账户 SET 余额 = 余额 - 100 WHERE 账户号 = 'A';
SELECT 余额 FROM 账户 WHERE 账户号 = 'A';  -- 查看变化
ROLLBACK;  -- 撤销
SELECT 余额 FROM 账户 WHERE 账户号 = 'A';  -- 验证是否恢复

练习 2:隔离级别实验

开启两个 MySQL 客户端窗口,分别模拟事务 A 和事务 B,测试不同隔离级别下的行为:

  • 在 READ UNCOMMITTED 下,A 修改未提交,B 能否读到?
  • 在 READ COMMITTED 下,A 修改未提交,B 能否读到?A 提交后呢?
  • 在 REPEATABLE READ 下,A 修改并提交,B 再次读,结果是否变化?

练习 3:死锁模拟

在两个会话中执行:

sql 复制代码
-- 会话 A
BEGIN;
UPDATE 账户 SET 余额 = 余额 - 100 WHERE 账户号 = 'A';
-- 暂停
UPDATE 账户 SET 余额 = 余额 - 100 WHERE 账户号 = 'B';  -- 等待 B 释放锁
COMMIT;

-- 会话 B
BEGIN;
UPDATE 账户 SET 余额 = 余额 - 100 WHERE 账户号 = 'B';
-- 暂停
UPDATE 账户 SET 余额 = 余额 - 100 WHERE 账户号 = 'A';  -- 等待 A 释放锁
COMMIT;

观察哪个事务被回滚,以及错误信息。


八、常见误区与避坑指南

误区 正确理解
"事务越多越好" 长事务占用锁时间长,降低并发性能,尽量缩小事务范围
"SERIALIZABLE 最安全, always 用它" 隔离级别越高性能越差,大多数场景 READ COMMITTED 或 REPEATABLE READ 足够
"InnoDB 的默认隔离级别不会幻读" REPEATABLE READ 下 InnoDB 通过 MVCC + 间隙锁解决了大部分幻读,但某些场景仍可能
"锁越粗越好" 粗粒度锁并发度低,细粒度锁并发度高但管理复杂
"ROLLBACK 很慢" ROLLBACK 利用 Undo 日志恢复,通常很快,不要因担心性能而不用

九、下篇预告

下一篇,我们将学习数据库恢复技术------当数据库遭遇故障(断电、崩溃、误操作)时,如何通过日志、检查点和备份把数据救回来。

相关推荐
howard20052 小时前
PostgreSQL起步
数据库·postgresql
秋田君3 小时前
QT_QT布局详解
开发语言·数据库·qt
可乐ea3 小时前
【Redis八股|第8篇】Redis 分布式锁原理与 Redisson 使用
数据库·redis·分布式·面试题·redis八股
琢森3 小时前
不用装Agent,SSH直连搞定服务器监控
服务器
snow@li3 小时前
深入理解内核与操作系统的关系
运维·服务器·microsoft
ClouGence4 小时前
Oracle 到 OceanBase 迁移方案横评:停机导出/导入 vs OMS vs CDC 工具
数据库·oracle
Meya11274 小时前
不同规模机房怎么选 U 位系统?8 柜小型机房、40 柜数据中心完整选型参考
服务器·网络·数据库
laoli_coding4 小时前
系统部署之部署方式
服务器
prettybug1254 小时前
OBS背景音乐_气氛实战指南
运维·服务器