事务
事务的概念
一个事务是由一条或者多条对数据库操作的SQL语句所组成的一个不可分割的单元,只有当事务中的所有操作都正常执行完了,整个事务才会被提交给数据库;如果有部分事务处理失败,那么事务就要回退到最初的状态,因此,事务要么全部执行成功,要么全部失败。所以记住事务的几个基本概念,如下:
1. 事务是一组SQL语句的执行,要么全部成功,要么全部失败,不能出现部分成功,部分失败的结果。保证事务执行的原子操作。
2. 事务的所有SQL语句全部执行成功,才能提交(commit)事务,把结果写回磁盘上。
3. 事务执行过程中,有的SQL出现错误,那么事务必须要回滚(rollback)到最初的状态
ACID特性
- 事务的原子性(Atomic): 事务是一个不可分割的整体,事务必须具有原子特性,及当数据修改时,要么全执行,要么全不执行, 即不允许事务部分的完成。
- 事务的一致性(Consistency): 一个事务执行之前和执行之后,数据库数据必须保持一致性状态。数据库的一致性状态必须由用户来负 责,由并发控制机制实现。就拿网上购物来说,你只有让商品出库,又让商品进入顾客的购物车才能构 成一个完整的事务。
- 事务的隔离性(Isolation): 当两个或者多个事务并发执行时,为了保证数据的安全性,将一个事物内部的操作与其它事务的操作隔 离起来,不被其它正在执行的事务所看到,使得并发执行的各个事务之间不能互相影响。
- 事务的持久性(Durability): 事务完成(commit)以后,DBMS保证它对数据库中的数据的修改是永久性的,即使数据库因为故障出 错,也应该能够恢复数据!
事务并发问题
- 脏读:事务A读取事务B未提交的数据,B回滚后A读到无效数据。
- 不可重复读:事务A多次读取同一数据,期间事务B修改导致A前后结果不一致。
- 幻读:事务A查询范围内数据,事务B新增或删除记录导致A结果集变化。
隔离级别及解决方案
隔离级别 | 脏读 | 不可重复读 | 幻读 | 实现方式 |
---|---|---|---|---|
READ UNCOMMITTED | ❌ | ❌ | ❌ | 无锁 |
READ COMMITTED | ✅ | ❌ | ❌ | 每次查询生成新快照(MVCC) |
REPEATABLE READ(默认) | ✅ | ✅ | ❌ | 事务首次查询生成快照(MVCC) |
SERIALIZABLE | ✅ | ✅ | ✅ | 锁机制(Next-Key Lock) |
-- 设置隔离级别示例
SET SESSION TRANSACTION ISOLATION LEVEL REPEATABLE READ;
InnoDB默认隔离级别是REPEATABLE READ可重复读
在“可重复读“级别,实际上可以解决部分的幻读问题,但是不能防止update更新产生的虚读问题,要禁止虚读产生,还是需要设置串行化隔离级别。
隔离级别底层实现
已提交读:每次执行语句的时候都重新生成一次快照(Read View),每次`select`查询时。这样就解决了脏读
可重复读:同一个事务开始的时候生成一个当前事务全局性的快照(Read View),第一次`select`查询时。
串行化隔离级别下怎么解决幻读问题: next-key lock:行锁+间隙锁
锁的粒度差异
表级锁
锁定整张表,适合数据量小或全表操作场景。资源消耗低,但会限制其他事务对表的访问。
行级锁
仅锁定特定行,适合高并发场景。资源消耗较高,需注意死锁问题。
-- 表级锁示例(MyISAM引擎)
LOCK TABLES orders READ; -- 共享锁
UNLOCK TABLES;
-- 行级锁示例(InnoDB引擎)
SELECT * FROM orders WHERE id = 1 FOR UPDATE; -- 排它锁
共享锁与排它锁
共享锁(S锁)
允许并发读取,阻止其他事务获取排它锁。适用于查询为主场景。
排它锁(X锁)
独占数据访问权,阻止其他事务获取任何锁。适用于数据修改操作。
-- 显式加锁示例
SELECT * FROM products WHERE stock > 0 LOCK IN SHARE MODE; -- 共享锁
UPDATE products SET stock = stock -1 WHERE id = 101; -- 自动加排它锁
锁的兼容性矩阵
当前锁 \ 请求锁 | S锁 | X锁 |
---|---|---|
S锁 | 兼容 | 冲突 |
X锁 | 冲突 | 冲突 |
-- 查看锁等待情况(MySQL)
SHOW ENGINE INNODB STATUS;
间隙锁
间隙锁(Gap Lock)是InnoDB引擎在特定隔离级别(如串行化)下使用的一种锁机制,主要用于防止幻读现象。以下从多个维度分析其工作原理和应用场景。
间隙锁触发条件
使用范围条件而非相等条件检索数据时,InnoDB会为符合条件的数据记录及不存在的“间隙”加锁。例如:
SELECT * FROM user WHERE userid > 100 FOR UPDATE;
此语句会锁定所有userid > 100
的记录,以及userid
值大于101的未存在区间(如102, 103等)。
防止幻读的机制
间隙锁的核心目标是满足串行化隔离级别的要求。若不使用间隙锁,其他事务插入符合范围条件的新记录(如userid=102
),当前事务再次执行相同查询会读到新插入的行,导致幻读。
辅助索引的间隙锁
当过滤条件为辅助索引时,间隙锁会锁定索引值的间隙范围。例如:
SELECT * FROM user WHERE age > 20 FOR UPDATE;
假设age
的索引值为10,13,14,15,20,21,22,则锁定的间隙包括:
- 15-20
- 20-21
- 21-22
- 22以上 此时插入
age=15
或age=16
会被阻塞,但age=14
允许插入。
全表扫描与表锁
若查询条件无法有效使用索引(如age > 14
但优化器选择全表扫描),InnoDB会退化为表级锁,导致所有插入操作被阻塞。
等值查询与辅助索引
主键等值查询因唯一性无需间隙锁,但辅助索引允许重复,需间隙锁防止幻读。例如:
- 事务A执行:
SELECT * FROM user WHERE age = 20 FOR UPDATE;
- 事务B尝试插入:
此操作会被阻塞,因间隙锁锁定了INSERT INTO user(name, age, sex) VALUES ('ddd', 18, 'M');
age=20
附近的区间。
代码示例:间隙锁验证
可通过以下实验验证间隙锁行为:
-- 会话1:开启事务并锁定范围
START TRANSACTION;
SELECT * FROM user WHERE age BETWEEN 15 AND 20 FOR UPDATE;
-- 会话2:尝试插入被阻塞
INSERT INTO user(age) VALUES (16); -- 阻塞
INSERT INTO user(age) VALUES (14); -- 成功
关键总结
- 作用范围:锁定索引记录间的“间隙”,阻止其他事务插入。
- 隔离级别:主要应用于串行化隔离级别。
- 索引影响:是否使用间隙锁取决于查询能否有效利用索引。
- 性能权衡:过度使用间隙锁可能导致并发性能下降。
undo log回滚日志
`undo log`:回滚日志,保存了事务发生之前的数据的一个版本,用于事务执行时的回滚操作,同时也是实现多版本并发控制(MVCC)下读操作的关键技术。
`DB_TRX_ID`:事务ID
`DB_ROLL_PTR`:回滚指针
MVCC
已提交读和可重复读 ---> 底层实现原理MVCC ---> 并发方式:快照读 ---> 底层依赖undo log回滚日志
InnoDB提供了两个读取操作:锁定读(S X)和非锁定读(MVCC的快照读)
MVCC是多版本并发控制(Multi-Version Concurrency Control,简称MVCC),是MySQL中基于乐观锁理论实现隔离级别的方式,用于实现已提交读和可重复读隔离级别的实现,也经常称为多版本数据库。MVCC机制会生成一个数据请求时间点的一致性数据快照 (Snapshot), 并用这个快照来提供一定级别 (语句级或事务级) 的一致性读取。从用户的角度来看,好象是数据库可以提供同一数据的多个版本(系统版本号和事务版本号)。
MVCC多版本并发控制中,读操作可以分为两类:
1. 快照读(snapshot read):读的是记录的可见版本,不用加锁。如`select`
2. 当前读(current read):读取的是记录的最新版本,并且当前读返回的记录。如`insert,delete,update,select...lock in sharemode/for update`
MVCC:每一行记录实际上有多个版本,每个版本的记录除了数据本身之外,增加了其它字段
`DB_TRX_ID`:记录当前事务ID
`DB_ROLL_PTR`:指向undo log日志上数据的指针
已提交读:每次执行语句的时候都重新生成一次快照(Read View),每次`select`查询时。这样就解决了脏读
可重复读:同一个事务开始的时候生成一个当前事务全局性的快照(Read View),第一次`select`查询时。
快照内容读取原则:
1. 版本未提交无法读取生成快照
2. 版本已提交,但是在快照创建后提交的,无法读取
3. 版本已提交,但是在快照创建前提交的,可以读取
4. 当前事务内自己的更新,可以读到
总结
MVCC(多版本并发控制)= 让读写互不干扰的黑科技
核心思想
数据库同时保存同一数据的多个版本(像时光机一样),读操作和写操作可以各干各的,互不等待。
举个栗子
- 场景: 你在淘宝看商品库存(读操作),同时商家正在修改库存(写操作)。
- 传统方式: 商家改库存时会上锁,你必须等他改完才能看到库存,体验很差。
- MVCC 方式: 1. 商家修改库存前,数据库自动保存旧库存(如 100 件)到 `undo log`(版本链)。
2. 商家修改为 99 件(当前版本),但你读的是旧版本 100 件(通过 `Read View` 判断可见性)。
3. 你和商家的操作互不影响,并发执行!
关键机制
1. 版本链(undo log):
- 每次修改数据时,旧值被保存到 `undo log`,形成类似 `100 → 99 → 98` 的链条。
- 每个版本带一个 **事务 ID**(如 `trx_id=100` 生成版本 100)。
2. 读视图(Read View):
- 你(事务)查询时,数据库生成一个 “时间窗口”(Read View),只让你看到窗口内已完成的修改。
- 规则:
- 若商家事务未提交,你看不到 99 件,只能读旧版本 100 件;
- 若商家已提交,且你的窗口包含该提交,你才能读到 99 件。
3. 读写不冲突:
- **读操作**:通过 `Read View` 直接从版本链取可见版本,无需锁。
- **写操作**:直接修改当前版本,加锁防止其他写操作,但不影响读。
隔离级别差异
- 读已提交(RC):
每次读都生成新窗口,能看到最新已提交的修改(如商家提交后,你立即看到 99 件)。
- 可重复读(RR,MySQL 默认):
事务内窗口固定,多次读同一数据结果一致(如你始终看到 100 件,直到事务结束)。
MVCC 让数据库像 “平行时空” 一样,写操作创建新版本,读操作按自己的时间线取数据,读写互不干扰,并发性能暴增