1.2 并发控制
概述
并发控制是数据库管理系统中的核心问题。当多个用户同时访问数据库时,必须确保数据的一致性和完整性。MySQL 通过锁机制来实现并发控制,保证在多用户环境下数据操作的正确性。
为什么需要并发控制
并发访问带来的问题
当多个事务同时操作同一数据时,可能产生以下问题:
| 问题 | 说明 | 示例 |
|---|---|---|
| 脏读(Dirty Read) | 读取到其他事务未提交的数据 | 事务A修改但未提交,事务B读取到修改后的值 |
| 不可重复读(Non-repeatable Read) | 同一事务内多次读取结果不同 | 事务A读取后,事务B修改并提交,事务A再次读取结果不同 |
| 幻读(Phantom Read) | 同一事务内多次查询,结果集数量不同 | 事务A查询后,事务B插入新数据,事务A再次查询多出新行 |
并发控制的目标
- 数据一致性:确保数据在并发操作下保持正确
- 事务隔离性:事务之间相互隔离,互不干扰
- 最大化并发:在保证正确性的前提下,允许多个事务并行执行
1.2.1 读写锁
锁的基本概念
锁是数据库实现并发控制的基本机制。MySQL 中的锁主要分为两类:
锁类型
├── 共享锁(Shared Lock,S锁)/ 读锁(Read Lock)
└── 排他锁(Exclusive Lock,X锁)/ 写锁(Write Lock)共享锁(Shared Lock / S锁 / 读锁)
定义
共享锁允许多个事务同时读取同一资源,但阻止其他事务获取排他锁。
特性
| 特性 | 说明 |
|---|---|
| 并发读 | 多个事务可以同时持有共享锁 |
| 阻塞写 | 持有共享锁时,其他事务不能获取排他锁 |
| 不阻塞读 | 持有共享锁时,其他事务可以获取共享锁 |
使用场景
-- 显式加共享锁(InnoDB)
SELECT * FROM users WHERE id = 1 LOCK IN SHARE MODE;典型场景:
- 读取数据时防止数据被修改
- 实现可重复读
- 父子表关系中的外键检查
排他锁(Exclusive Lock / X锁 / 写锁)
定义
排他锁阻止其他事务同时读取或修改被锁定的资源。
特性
| 特性 | 说明 |
|---|---|
| 独占访问 | 同一时间只有一个事务能持有排他锁 |
| 阻塞读写 | 持有排他锁时,其他事务不能获取任何类型的锁 |
| 自动释放 | 事务提交或回滚后自动释放 |
使用场景
-- 自动加排他锁(DML操作)
UPDATE users SET name = '张三' WHERE id = 1;
DELETE FROM users WHERE id = 1;
-- 显式加排他锁
SELECT * FROM users WHERE id = 1 FOR UPDATE;锁的兼容性矩阵
| 共享锁(S) | 排他锁(X) | |
|---|---|---|
| 共享锁(S) | 兼容 ✓ | 不兼容 ✗ |
| 排他锁(X) | 不兼容 ✗ | 不兼容 ✗ |
锁的生命周期
事务开始
↓
请求锁(共享/排他)
↓
┌─────────────┐
│ 锁可用? │
└─────────────┘
↓ 是 ↓ 否
获取锁 等待锁释放
↓ ↓
执行操作 进入锁等待队列
↓ ↓
释放锁 ←─────── 超时/获取锁
↓
事务结束(提交/回滚)1.2.2 锁粒度
锁粒度的概念
锁粒度指的是锁定的数据范围大小。MySQL 支持多种锁粒度:
锁粒度(从粗到细)
├── 表级锁(Table Lock)
├── 页级锁(Page Lock)
└── 行级锁(Row Lock)表级锁(Table Lock)
定义
表级锁是最基本的锁策略,锁定整张表。
特点
| 特点 | 说明 |
|---|---|
| 开销小 | 加锁和释放锁的速度快 |
| 并发度低 | 锁定整张表,其他事务无法访问 |
| 无死锁 | 不会出现死锁问题 |
| 适合读多写少 | 读操作不会相互阻塞 |
MySQL 中的表级锁
1. 表共享读锁(Table Read Lock)
-- 加表级读锁
LOCK TABLES users READ;
-- 解锁
UNLOCK TABLES;特点:
- 多个会话可以同时获取 READ 锁
- 持有 READ 锁的会话只能读,不能写
- 其他会话可以读,写操作会阻塞
2. 表独占写锁(Table Write Lock)
-- 加表级写锁
LOCK TABLES users WRITE;
-- 解锁
UNLOCK TABLES;特点:
- 只有一个会话能获取 WRITE 锁
- 持有 WRITE 锁的会话可以读写
- 其他会话的读写操作都会阻塞
存储引擎支持
| 存储引擎 | 表级锁支持 |
|---|---|
| MyISAM | 支持(默认) |
| InnoDB | 支持(但主要使用行级锁) |
| Memory | 支持(默认) |
行级锁(Row Lock)
定义
行级锁只锁定需要修改的数据行,是粒度最细的锁。
特点
| 特点 | 说明 |
|---|---|
| 开销大 | 加锁和释放锁需要更多的资源 |
| 并发度高 | 只锁定必要的数据行 |
| 可能出现死锁 | 需要死锁检测和处理机制 |
| 适合写多读少 | 写操作不会阻塞其他行的读写 |
InnoDB 行级锁类型
1. 记录锁(Record Lock)
锁定索引中的一条记录:
-- 主键索引上的记录锁
SELECT * FROM users WHERE id = 1 FOR UPDATE;2. 间隙锁(Gap Lock)
锁定一个范围,但不包含记录本身:
-- 锁定 id 在 (1, 5) 之间的间隙
SELECT * FROM users WHERE id > 1 AND id < 5 FOR UPDATE;3. 临键锁(Next-Key Lock)
记录锁 + 间隙锁的组合,锁定记录及其前面的间隙:
数据:1, 5, 10, 15
Next-Key Lock 范围:
(-∞, 1], (1, 5], (5, 10], (10, 15], (15, +∞)4. 插入意向锁(Insert Intention Lock)
一种特殊的间隙锁,用于 INSERT 操作:
-- 事务A持有 (5, 10) 的间隙锁
-- 事务B可以获取 (5, 10) 的插入意向锁(插入不同位置)
INSERT INTO users VALUES (6, '张三');
INSERT INTO users VALUES (8, '李四');行级锁的存储
InnoDB 的行级锁是通过 索引 实现的:
┌─────────────────────────────────────┐
│ 表结构 │
│ CREATE TABLE users ( │
│ id INT PRIMARY KEY, │
│ name VARCHAR(50), │
│ INDEX idx_name (name) │
│ ); │
└─────────────────────────────────────┘
↓
┌─────────────────────────────────────┐
│ SELECT * FROM users WHERE id = 1 │
│ FOR UPDATE; │
└─────────────────────────────────────┘
↓
┌─────────────────────────────────────┐
│ 锁定主键索引中 id = 1 的记录 │
│ (如果 id = 1 不存在,锁定间隙) │
└─────────────────────────────────────┘页级锁(Page Lock)
定义
页级锁锁定数据库页(通常为 16KB),是表级锁和行级锁的折中方案。
特点
| 特点 | 说明 |
|---|---|
| 开销适中 | 比行级锁开销小,比表级锁开销大 |
| 并发度适中 | 锁定一页数据 |
| 可能出现死锁 | 需要死锁处理 |
存储引擎支持
| 存储引擎 | 页级锁支持 |
|---|---|
| BDB | 支持(已废弃) |
| InnoDB | 内部使用(16KB 数据页) |
锁粒度对比
| 锁粒度 | 开销 | 并发度 | 适用场景 | 死锁风险 |
|---|---|---|---|---|
| 表级锁 | 小 | 低 | 读多写少、数据量小 | 无 |
| 页级锁 | 中 | 中 | 中等数据量 | 有 |
| 行级锁 | 大 | 高 | 写多读少、数据量大 | 有 |
锁粒度的选择策略
1. 根据存储引擎选择
MyISAM/Memory → 表级锁
InnoDB → 行级锁(默认)2. 根据业务场景选择
| 场景 | 推荐锁粒度 | 原因 |
|---|---|---|
| 读多写少 | 表级锁 | 减少锁开销 |
| 写多读少 | 行级锁 | 提高并发度 |
| 大量数据 | 行级锁 | 减少锁竞争 |
| 少量数据 | 表级锁 | 简单高效 |
3. InnoDB 的锁升级
InnoDB 不会自动将行级锁升级为表级锁,但可以通过以下方式显式加表锁:
-- 显式加表级锁
LOCK TABLES users WRITE;
-- 或者设置事务隔离级别为 SERIALIZABLE
SET SESSION TRANSACTION ISOLATION LEVEL SERIALIZABLE;多版本并发控制(MVCC)
什么是 MVCC
多版本并发控制(Multi-Version Concurrency Control)是一种通过保存数据在不同时间点的多个版本来实现并发控制的技术。
MVCC 的工作原理
事务A(读取) 事务B(修改)
↓ ↓
读取数据版本V1 修改数据为V2
(创建ReadView) (创建新版本)
↓ ↓
继续读取V1 ←────────── 提交事务
(不受B影响) ↓
数据变为V2MVCC 的优势
- 读不阻塞写:读取操作不需要加锁
- 写不阻塞读:写入操作不影响读取旧版本
- 提高并发度:读写操作可以并行执行
InnoDB 的 MVCC 实现
InnoDB 通过以下机制实现 MVCC:
1. 隐藏列
每行记录包含两个隐藏列:
DB_TRX_ID:最后修改该行的事务IDDB_ROLL_PTR:回滚指针,指向 undo log
2. Undo Log
保存数据的历史版本,用于:
- 事务回滚
- 构建早期版本的数据
3. Read View
决定事务能看到哪个版本的数据:
Read View 包含:
- creator_trx_id:创建该视图的事务ID
- m_ids:活跃事务ID列表
- min_trx_id:最小活跃事务ID
- max_trx_id:下一个分配的事务IDMVCC 与隔离级别
| 隔离级别 | MVCC 行为 |
|---|---|
| READ UNCOMMITTED | 不生成 Read View,直接读取最新数据 |
| READ COMMITTED | 每次查询生成新的 Read View |
| REPEATABLE READ | 事务开始时生成 Read View,整个事务使用同一视图 |
| SERIALIZABLE | 不使用 MVCC,所有操作加锁 |
总结
核心概念回顾
读写锁
- 共享锁(S锁):允许多个事务同时读
- 排他锁(X锁):独占访问,阻塞其他所有操作
锁粒度
- 表级锁:开销小,并发度低
- 页级锁:折中方案
- 行级锁:开销大,并发度高
MVCC
- 通过多版本实现读写不阻塞
- 提高数据库并发性能
最佳实践
- 优先使用 InnoDB:支持行级锁和 MVCC
- 合理设计索引:行级锁通过索引实现
- 控制事务大小:减少锁持有时间
- 避免长事务:减少锁竞争和死锁风险
- 选择合适的隔离级别:平衡一致性和并发性
理解并发控制机制对于数据库性能优化和问题解决至关重要,是 MySQL 深入学习的基础。