一、事务(Transaction)
本质
- 并发控制的基本单元,用户定义的操作序列,具有原子性(要么全做,要么全不做)。
目的
- 将数据库从一致性状态转换为另一种一致性状态,确保系统状态完整正确。
组成
- 可由单条 SQL 语句(如
INSERT
)或一组复杂 SQL 语句组成。
特征
- 提交时确保所有修改持久化或全部回滚;
- 是访问和更新数据库数据项的独立执行单元。
1. 事务控制语句(MySQL InnoDB)
-- 关闭自动提交(手动管理事务)
SET autocommit = 0;
-- 显式开启事务
START TRANSACTION; 或 BEGIN;
-- 提交事务(持久化修改)
COMMIT;
-- 回滚事务(撤销未提交修改)
ROLLBACK;
-- 创建保存点
SAVEPOINT savepoint_name;
-- 回滚到保存点
ROLLBACK TO SAVEPOINT savepoint_name;
-- 删除保存点
RELEASE SAVEPOINT savepoint_name;
2. ACID 特性
1. 原子性(Atomicity)
- 定义:事务操作要么全执行(提交),要么全不执行(回滚),不可分割。
- 实现:通过 undo log 记录操作逆过程,回滚时执行逆运算。
2. 隔离性(Isolation)
- 定义:控制事务间的相互影响程度,应对脏读、不可重复读、幻读问题。
- 实现:
- MVCC(多版本并发控制):通过行版本快照实现一致性非锁定读,提升读性能;
- 锁机制:处理并发写操作,支持表锁、页锁、行锁(InnoDB 默认为行锁)。
3. 持久性(Durability)
- 定义:事务提交后,修改永久保存(即使宕机)。
- 实现:通过 redo log 记录物理修改(如页偏移、数据),故障时通过日志恢复。
4. 一致性(Consistency)
- 定义:事务前后数据库满足完整性约束(如唯一键、外键)。
- 实现:依赖原子性、隔离性、持久性共同保证。
3. 隔离级别(ISO/ANSI 标准)
隔离级别 | 描述 | 锁机制 | MySQL 默认 |
---|---|---|---|
READ UNCOMMITTED | 读未提交(可能读到脏数据),写加排他锁,读不加锁。 | 写锁(提交 / 回滚释放) | 否 |
READ COMMITTED | 读已提交(RC),通过 MVCC 读取最新已提交快照,避免脏读。 | MVCC + 写锁 | 否 |
REPEATABLE READ | 可重复读(RR),通过 MVCC 读取事务开始时的快照,避免不可重复读。 | MVCC + 写锁 | 是 |
SERIALIZABLE | 可串行化,读加共享锁,强制事务串行执行,完全避免并发问题。 | 读写锁(串行化) | 否 |
1. READ UNCOMMITTED(读未提交)
问题:可能出现脏读(Dirty Read)
- 描述:事务可以读取其他未提交事务的数据变更,数据可能是临时的、未确认的。
- 锁机制:
- 写操作加排他锁(X 锁),阻止其他事务写,但不阻止读。
- 读操作不加锁,直接读取最新数据(可能未提交)。
- MySQL 默认:否
- 示例场景:银行转账中的脏读
- 用户 A发起转账 100 元给用户 B,事务未提交:
START TRANSACTION; UPDATE accounts SET balance = balance - 100 WHERE id = 'A'; -- 未提交
- 用户 B此时查询余额(隔离级别为读未提交):
SELECT balance FROM accounts WHERE id = 'B'; -- 假设此时读取到临时增加的100元(脏数据)
- 若用户 A的事务回滚(
ROLLBACK
),用户 B 读到的 100 元会消失,导致数据不一致。- 适用场景:几乎不使用,因为脏读风险高,仅适用于允许临时脏数据的极弱一致性场景(如日志统计)。
2. READ COMMITTED(读已提交,RC)
解决:避免脏读,但可能出现不可重复读(Non-Repeatable Read)
- 描述:事务只能读取其他已提交事务的数据变更,每次读取时获取最新已提交的快照。
- 锁机制:
- 写操作加排他锁(X 锁),提交后释放锁。
- 读操作通过 MVCC(多版本并发控制) 读取历史快照,不加锁(一致性非锁定读)。
- MySQL 默认:否(但 Oracle 默认是 RC)
- 示例场景:订单状态更新的不可重复读
- 事务 1(用户查询订单状态):
START TRANSACTION; SELECT status FROM orders WHERE id = 1; -- 第一次查询,状态为"待支付"
- 事务 2(商家修改订单状态)(隔离级别 RC,已提交):
UPDATE orders SET status = '已支付' WHERE id = 1; -- 提交事务
- 事务 1 再次查询:
SELECT status FROM orders WHERE id = 1; -- 第二次查询,状态变为"已支付"(前后结果不一致,不可重复读)
- 适用场景:大多数 OLTP 系统(如电商、票务),平衡一致性和性能,避免脏读。
3. REPEATABLE READ(可重复读,RR)
解决:避免不可重复读,但可能出现幻读(Phantom Read)
- 描述:事务在启动时获取初始快照,整个事务期间读取的数据始终与初始快照一致,即使其他事务已提交新数据。
- 锁机制:
- 写操作加排他锁(X 锁),提交后释放锁。
- 读操作通过 MVCC 读取事务开始时的快照,不加锁(一致性非锁定读)。
- MySQL 特殊处理:通过 间隙锁(Gap Lock) 避免幻读(默认配置下)。
- MySQL 默认:是
- 示例场景:库存扣减的可重复读
- 库存表初始数据:
goods_id=1
,stock=10
- 事务 1(用户 A 查询库存并下单):
START TRANSACTION; SELECT stock FROM goods WHERE goods_id = 1; -- 第一次查询,stock=10 -- 模拟业务处理(未提交)
- 事务 2(用户 B 购买并提交):
UPDATE goods SET stock = stock - 1 WHERE goods_id = 1; -- 提交后,stock=9
- 事务 1 再次查询:
SELECT stock FROM goods WHERE goods_id = 1; -- 第二次查询,stock仍为10(与初始快照一致,可重复读)
- 幻读示例:若事务 1 查询
stock < 20
的所有商品,事务 2 插入一条stock=15
的新记录,事务 1 再次查询会看到新记录(幻读),但 MySQL 通过间隙锁默认避免此问题。- 适用场景:需要事务内数据一致性的场景(如金融交易、库存扣减),MySQL 默认级别,性能与一致性平衡较好。
4. SERIALIZABLE(可串行化)
解决:完全避免脏读、不可重复读、幻读,强制串行执行
- 描述:事务通过 ** 共享锁(S 锁)和排他锁(X 锁)** 强制串行执行,读加 S 锁,写加 X 锁,读写互斥。
- 锁机制:
- 读操作加共享锁(S 锁),写操作加排他锁(X 锁),两者互斥(读时不能写,写时不能读)。
- 所有事务按顺序执行,相当于单线程处理。
- MySQL 默认:否
- 示例场景:火车票抢购的强一致性
- 剩余票数:
train_id=1
,tickets=1
- 事务 1(用户 A 查询并购票):
START TRANSACTION; SELECT tickets FROM train WHERE train_id = 1 LOCK IN SHARE MODE; -- 加S锁,读取tickets=1 -- 模拟业务处理 UPDATE train SET tickets = tickets - 1 WHERE train_id = 1; -- 升级为X锁,提交后释放锁 COMMIT;
- 事务 2(用户 B 同时购票):
START TRANSACTION; SELECT tickets FROM train WHERE train_id = 1 LOCK IN SHARE MODE; -- 阻塞,等待事务1释放锁
- 事务 1 提交后,事务 2 才能执行,避免超卖(强一致性)。
- 适用场景:极少数需要绝对一致性且不考虑性能的场景(如银行核心交易),性能最差,并发吞吐量极低。
总结对比
隔离级别 脏读 不可重复读 幻读 锁机制 并发性能 MySQL 默认 READ UNCOMMITTED 允许 允许 允许 写 X 锁,读无锁 最高 否 READ COMMITTED 禁止 允许 允许 MVCC + 写 X 锁 高 否 REPEATABLE READ 禁止 禁止 部分禁止 MVCC + 写 X 锁 + 间隙锁 中 是 SERIALIZABLE 禁止 禁止 禁止 读写锁(串行化) 最低 否 选择建议:
- 优先使用 REPEATABLE READ(MySQL 默认),平衡一致性和性能,适合大多数场景。
- 若需更高一致性,可通过业务层逻辑(如唯一索引、乐观锁)补充,而非直接使用 SERIALIZABLE。
设置隔离级别:
-- 设置全局隔离级别(需重启或新会话生效)
SET GLOBAL TRANSACTION ISOLATION LEVEL REPEATABLE READ;
SET @@global.tx_isolation = 'REPEATABLE READ';
-- 设置当前会话隔离级别(仅当前连接生效)
SET SESSION TRANSACTION ISOLATION LEVEL REPEATABLE READ;
SET @@session.tx_isolation = 'REPEATABLE READ';
-- 查看隔离级别
SELECT @@global.tx_isolation; -- 全局
SELECT @@session.tx_isolation; -- 当前会话
手动加锁语句:
SELECT ... LOCK IN SHARE MODE; -- 共享锁(S 锁),读锁
SELECT ... FOR UPDATE; -- 排他锁(X 锁),写锁
锁查看:
-- 查看 InnoDB 锁信息
SELECT * FROM information_schema.innodb_locks;
4. 锁
锁机制用于管理对共享资源的并发访问;用来实现事务的隔离级别;
4.1 锁类型
共享锁和排他锁都是行级锁;MySQL当中事务采用的是粒度锁;针对表(B+树)、页(B+树叶子 节点)、行(B+树叶子节点当中某一段记录行)三种粒度加锁; 意向共享锁和意向排他锁都是表级别的锁;
4.2 共享锁 (S)
事务读操作加的锁;对某一行加锁;
在 SERIALIZABLE 隔离级别下,默认帮读操作加共享锁;
在 REPEATABLE READ 隔离级别下,需手动加共享锁,可解决幻读问题;
在 READ COMMITTED 隔离级别下,没必要加共享锁,采用的是 MVCC;
在 READ UNCOMMITTED 隔离级别下,既没有加锁也没有使用 MVCC;
4.3 排它锁 (X)
事务删除或更新加的锁;对某一行加锁; 在4种隔离级别下,都添加了排他锁,事务提交或事务回滚后释放锁;
4.4 意向共享锁 (IS)
对一张表中某几行加的共享锁;
4.5 意向排它锁 (IX)
对一张表中某几行加的排他锁; 目的:为了告诉其他事务,此时这条表被一个事务在访问;作用:排除表级别读写锁 (全面扫描 加锁);
IX 锁(Intention Exclusive Lock)是表级别的意向锁,其核心作用是声明事务即将在表的某些行上施加排它锁(X 锁)。它是一种 “协调锁”,用于告知其他事务当前事务对表的操作意图,避免表锁与行锁的冲突。
- 作用范围:表级别(非行级别)。
- 设计目的:当事务需要对表中的某一行加行级 X 锁时,必须先在表级别加 IX 锁。这是为了快速判断是否有其他事务正在占用表级锁(如全表写锁),避免直接遍历所有行检查行锁的开销。
- 与其他锁的兼容关系:
- 与表级共享锁(IS 锁)兼容;
- 与表级排它锁(X 锁)互斥;
- 不影响行级锁的加锁逻辑(行级 X 锁需要先加表级 IX 锁)。
4.6 锁的兼容性
锁 | S | X | IS | IX | AI |
---|---|---|---|---|---|
S | 兼容 | 冲突 | 兼容 | 冲突 | 冲突 |
X | 冲突 | 冲突 | 冲突 | 冲突 | 冲突 |
IS | 兼容 | 冲突 | 兼容 | 兼容 | 兼容 |
IX | 冲突 | 冲突 | 兼容 | 兼容 | 兼容 |
AI | 冲突 | 冲突 | 兼容 | 兼容 | 冲突 |
由于 InnoDB 支持的是行级别的锁,意向锁并不会阻塞除了全表扫描以外的任何请求;
意向锁之间是互相兼容的;
IS 只对排他锁不兼容;
当想为某一行添加 S 锁,先自动为所在的页和表添加意向锁 IS,再为该行添加 S 锁;
当想为某一行添加 X 锁,先自动为所在的页和表添加意向锁 IX,再为该行添加 X 锁;
当事务试图读或写某一条记录时,会先在表上加上意向锁,然后才在要操作的记录上加读锁或写锁。这样判断表中是否有记录加锁就很简单了,只要看下表上是否有意向锁就行了。意向锁之间是不会产生冲突的,也不和 AUTO_INC 锁冲突,它只会阻塞表级读锁或表级写锁,另外,意向锁也不会和行锁冲突,行锁只会和行锁冲突。
5. 锁算法
Record Lock(记录锁)
- 定义:锁定单个行记录的锁。
- 示例:对某一行数据加锁,仅限制其他事务对该行的修改,不影响其他行。
Gap Lock(间隙锁)
- 定义:锁定一个范围,但不包含记录本身,用于 可重复读(REPEATABLE READ) 及以上隔离级别,防止幻读(其他事务插入新记录导致当前事务查询结果变化)。
- 条件:当
innodb_locks_unsafe_for_binlog = 0
时启用。- 示例:若索引列有值
10
、20
,间隙锁可能锁定(10, 20)
区间,阻止其他事务在该区间插入新值(如15
)。Next - Key Lock(临键锁)
- 定义:记录锁(Record Lock)与间隙锁(Gap Lock)的组合,既锁定记录本身,又锁定记录前的间隙(左开右闭区间)。
- 作用:在可重复读隔离级别下,彻底解决幻读问题,既防止修改已有记录,也防止插入新记录。
- 示例:锁定索引值
10
这一行,同时锁定(某个下限, 10]
的区间。Insert Intention Lock(插入意向锁)
- 定义:插入操作时产生的锁,用于表示事务插入数据的意向。
- 特点:多个事务向同一索引间隙插入数据时,插入意向锁之间相互兼容,不会互相阻塞。
- 示例:假设索引列有值
4
和7
,两个事务分别插入5
和6
,各自生成在(4, 7)
区间的插入意向锁,不会互相阻塞,因为插入行不冲突。AUTO - INC Lock (AI 锁)
- 定义:一种特殊的表级锁,用于
AUTO_INCREMENT
约束的插入操作。- 机制:早期采用表级锁,性能较差;从 MySQL 5.1.22 开始引入轻量级机制,提升高并发插入性能,仅在分配自增值时短暂锁定。
锁 | GAP (持有) | Insert Intention (持有) | Record (持有) | Next - key (持有) |
---|---|---|---|---|
GAP (请求) | 兼容 | 兼容 | 兼容 | 兼容 |
Insert Intention (请求) | 冲突 | 兼容 | 兼容 | 冲突 |
Record (请求) | 兼容 | 兼容 | 冲突 | 冲突 |
Next - key (请求) | 兼容 | 兼容 | 冲突 | 冲突 |
- 横向:已持有的锁类型(如
GAP
、Insert Intention
等)。 - 纵向:正在请求的锁类型。
- 规则:
- 若请求锁与已持锁在表格交叉处为 “兼容”,则可成功加锁;若为 “冲突”,则需等待。
- 示例:
- 请求
GAP
锁,已持有GAP
锁 → 兼容(可加锁)。 - 请求
Insert Intention
锁,已持有GAP
锁 → 冲突(需等待)。
- 请求
- 特殊说明:
- 一个事务获取插入意向锁,对其他事务无阻塞影响。
- 若其他事务已加
Gap Lock
或Next - Key Lock
,插入意向锁会被阻塞,因为前两者包含的范围可能与插入操作冲突。
6. MVCC
MVCC(Multi - Version Concurrency Control,多版本并发控制)是 InnoDB 存储引擎实现一致性非锁定读的关键机制,其核心是通过维护数据的多个版本,结合读视图(Read View)判断数据对当前事务的可见性,从而避免读操作被写操作阻塞,提升并发性能。以下是其原理的详细讲解:
一、MVCC 在不同隔离级别的应用
- Read Committed(读已提交):每次
SELECT
操作都会生成新的快照,读取的是被锁定行的最新一份快照数据。这意味着在同一个事务中多次读取同一条数据,可能因其他事务提交修改而出现结果不一致。- Repeatable Read(可重复读):在事务启动时生成一个快照,整个事务过程都使用该快照。这样能保证事务期间读到的数据始终是事务启动时的记录,避免不可重复读和幻读问题。
二、Read View(读视图)
- 定义与作用:Read View 是 MVCC 实现的核心数据结构,用于判断数据版本对当前事务的可见性。
- 创建时机:
- Read Committed:每次执行
SELECT
时创建新的 Read View。- Repeatable Read:事务启动时创建 Read View,整个事务期间复用。
- 构成:
m_ids
:创建 Read View 时,当前数据库中活跃事务(未提交)的事务 ID 列表。min_trx_id
:m_ids
中的最小事务 ID。max_trx_id
:创建 Read View 时,为下一个事务分配的 ID(不一定是m_ids
中的最大值)。creator_trx_id
:创建该 Read View 所在事务的 ID。三、聚簇索引隐藏列
每条聚簇索引记录都包含以下隐藏列:
trx_id
:最后一次修改该记录的事务 ID。roll_pointer
:回滚指针,指向undo log
中该记录的旧版本。通过此指针可找到修改前的数据,用于实现多版本控制。四、事务状态
- 已提交的事务:其修改对其他事务的可见性由 Read View 判断。
- 已启动未提交的事务:其修改对其他事务不可见(除非符合特定可见性规则)。
- 还没开始的事务:与当前 Read View 无直接关联。
五、数据可见性判断流程
- 若
trx_id < min_trx_id
:说明记录在 Read View 创建之前已提交,对当前事务可见。- 若
trx_id >= max_trx_id
:说明记录是在 Read View 创建之后生成的,对当前事务不可见。- 若
min_trx_id <= trx_id < max_trx_id
:
- 检查
trx_id
是否在m_ids
列表中。- 若在列表中,说明生成该版本的事务仍处于活跃状态,该版本对当前事务不可见。
- 若不在列表中,说明生成该版本的事务已提交,该版本对当前事务可见。
快照读和当前读的区别:
快照读:
- 操作:普通的
SELECT
语句(如select * from table where?
),不加锁。- 原理:通过 MVCC(多版本并发控制)读取历史版本数据,实现一致性非锁定读,无需等待锁,提升并发性能。
- 数据版本:依赖隔离级别,
Read Committed
每次读生成新快照(读最新已提交版本);Repeatable Read
事务启动时生成快照(读事务启动时的版本)。当前读:
- 操作:包括加锁的
SELECT
(如select... lock in share mode
共享锁、select... for update
排他锁),以及写操作(INSERT
、UPDATE
、DELETE
)。- 原理:读取最新版本数据,且会对数据加锁,确保数据的实时性与操作互斥,避免其他事务干扰。
- 数据版本:始终读取最新提交的数据版本。
示例
假设表中有一条记录
id = 1
,初始value = 'old'
,有事务 A 和事务 B:快照读示例(以
Read Committed
隔离级别为例):
- 事务 A 执行
select * from table where id = 1;
(快照读),读取到value = 'old'
。- 事务 B 执行
update table set value = 'new' where id = 1;
并提交,修改数据为new
。- 事务 A 再次执行
select * from table where id = 1;
(快照读),因Read Committed
重新生成快照,读取到value = 'new'
。若为
Repeatable Read
隔离级别,事务 A 启动时生成快照,两次快照读都会读到value = 'old'
(除非事务 A 自身修改数据)。当前读示例:
- 事务 A 执行
select * from table where id = 1 for update;
(当前读,加排他锁),读取到最新的value
(假设为old
)。- 事务 B 执行
update table set value = 'new' where id = 1;
(当前读,需获取排他锁),因事务 A 已加锁,事务 B 被阻塞,直到事务 A 提交或回滚释放锁。- 事务 A 提交后,事务 B 才能获取锁并执行更新,确保事务 A 读到的是最新且未被修改的数据(若有其他事务先修改,当前读会等待锁释放后读最新值)。
7. redolog 和 undolog
undo log(撤销日志)
- 定义与作用:undo log 是 InnoDB 存储引擎生成的日志,用于事务回滚及实现 MVCC(多版本并发控制),保证事务的原子性(操作要么全部成功,要么全部失败)。
- 原理:在事务开始时,记录数据修改前的版本(如更新前的旧值、插入前的空记录状态、删除前的记录内容)。通过
trx_id
(事务 ID)标记修改所属事务,通过roll_pointer
(回滚指针)将同一事务的 undo log 串联成版本链。当事务回滚时,根据 undo log 的记录反向操作:
- 对
UPDATE
,将数据恢复为旧值;- 对
INSERT
,执行DELETE
移除新插入数据;- 对
DELETE
,执行INSERT
恢复被删数据。- 示例:
假设表中有一条记录id=1
,value='old'
。事务执行UPDATE table SET value='new' WHERE id=1
,undo log 会记录修改前的value='old'
。若事务回滚,根据 undo log 将value
恢复为old
。- 使用:由 InnoDB 引擎自动管理,无需手动干预。事务提交后,undo log 进入删除列表,由后台线程回收;在 MVCC 中,读操作通过 undo log 读取历史版本数据(如
SELECT
操作在特定隔离级别下借助 undo log 实现非阻塞读)。redo log(重做日志)
- 定义与作用:redo log 是 InnoDB 存储引擎生成的日志,用于保证事务的持久性(事务提交后,数据即使在系统崩溃时也不丢失)。
- 原理:记录事务对数据页的物理修改(如 “某个表空间的某页,偏移量 X 处的值被改为 Y”)。事务提交时,先将修改写入 redo log(内存中的
redo log buffer
),再刷盘到redo log
文件。若系统崩溃,重启后 InnoDB 通过扫描 redo log,重新执行未完全刷盘的修改操作,确保数据落盘。- 示例:
事务对多个数据页进行修改(如插入、更新),这些操作先记录到 redo log。若事务提交后、数据刷盘前系统崩溃,重启后根据 redo log 重新执行这些修改,保证数据完整。- 使用:
- 由 InnoDB 引擎内部处理,通过参数
innodb_flush_log_at_trx_commit
控制刷盘策略:
1
(默认):每次事务提交时,强制将redo log buffer
内容刷盘,保证强持久性,但性能略低;2
:事务提交时,将redo log
写入操作系统缓冲区,不立即刷盘,系统崩溃可能丢失少量数据,性能较高。总结对比
日志类型 核心作用 记录内容 保证特性 典型场景 undo log 事务回滚、MVCC 数据修改前的旧版本 原子性 事务回滚、非锁定读(MVCC) redo log 崩溃后恢复数据 数据页的物理修改操作 持久性 系统崩溃后恢复未刷盘的修改
二、 并发读异常
数据库并发读异常主要包括 脏读、不可重复读、幻读,本质是多事务并发时,一个事务的写操作影响了另一个事务的读结果。以下是具体分析:
1.1 脏读(Dirty Read)
定义:一个事务读取到另一个未提交事务修改的数据(“脏数据”),若未提交事务回滚,读取的数据将无效。
示例(会话流程):
步骤 Session A(修改事务) Session B(读取事务) 结果说明 1 SET @@tx_isolation='READ UNCOMMITTED';
SET @@tx_isolation='READ UNCOMMITTED';
双方设置隔离级别为 READ UNCOMMITTED
(允许脏读)。2 BEGIN;
BEGIN;
事务启动。 3 UPDATE account_t SET money=money+100 WHERE name='A';
— Session A 修改 A
的余额(未提交)。4 — SELECT money FROM account_t WHERE name='A';
Session B 读取到 A
的余额已增加 100(脏数据)。5 ROLLBACK;
— Session A 回滚,修改被撤销。 6 — SELECT money FROM account_t WHERE name='A';
Session B 再次读取,发现余额恢复原值(之前读到的是脏数据)。 特点:
- 产生条件:隔离级别为
READ UNCOMMITTED
(最低隔离级别)。- 应用场景:读写分离架构中,从库(Slave)可设置
READ UNCOMMITTED
,允许脏读以提升读性能(对数据实时性要求不高)。1.2 不可重复读(Non-Repeatable Read)
定义:一个事务内两次读取同一记录,结果不一致(因另一个事务已提交修改)。
示例(会话流程):
步骤 Session A(读取事务) Session B(修改事务) 结果说明 1 SET @@tx_isolation='READ COMMITTED';
SET @@tx_isolation='READ COMMITTED';
双方设置隔离级别为 READ COMMITTED
(读已提交)。2 BEGIN;
BEGIN;
事务启动。 3 SELECT money FROM account_t WHERE name='A';
— Session A 第一次读取 A
的余额(假设为 1000)。4 — UPDATE account_t SET money=money+100 WHERE name='A'; COMMIT;
Session B 修改 A
的余额并提交(余额变为 1100)。5 SELECT money FROM account_t WHERE name='A';
— Session A 第二次读取,发现余额变为 1100(与第一次不一致)。 特点:
- 产生条件:隔离级别为
READ COMMITTED
或更低(如READ UNCOMMITTED
)。- 可接受性:因读取的是已提交数据,通常对业务影响较小(如大多数数据库默认隔离级别为
READ COMMITTED
,如 Oracle、SQL Server)。1.3 幻读(Phantom Read)
定义:一个事务内两次读取同一范围的记录,结果集不一致(因另一个事务插入 / 删除了符合条件的记录)。
示例(会话流程):
步骤 Session A(读取事务) Session B(插入事务) 结果说明 1 SET @@tx_isolation='REPEATABLE READ';
SET @@tx_isolation='REPEATABLE READ';
双方设置隔离级别为 REPEATABLE READ
(可重复读)。2 BEGIN;
BEGIN;
事务启动。 3 SELECT * FROM account_t WHERE id >= 2;
— Session A 第一次查询 id≥2
的记录(假设结果为id=2,3
)。4 — INSERT INTO account_t(id,name,money) VALUES(4,'D',1000); COMMIT;
Session B 插入 id=4
的记录并提交。5 SELECT * FROM account_t WHERE id >= 2;
— Session A 第二次查询,结果包含 id=4
(幻读:结果集新增记录)。解决方法:
- 加锁读:在
REPEATABLE READ
隔离级别下,使用SELECT ... LOCK IN SHARE MODE
(共享锁)或SELECT ... FOR UPDATE
(排他锁),通过Next-Key Lock
锁定查询范围,阻止其他事务插入 / 删除。
1.4 三者区别总结
异常类型 核心特征 影响的事务操作 数据状态(被读) 脏读 读取未提交的 “脏数据” 写(未提交)→ 读 未提交(可能回滚) 不可重复读 两次读同一记录结果不同(因已提交修改) 写(已提交)→ 读 已提交(修改后的值) 幻读 两次读同一范围结果集不同(因插入 / 删除) 写(已提交)→ 读 已提交(新增 / 删除的记录) 二、丢失更新(Lost Update)
定义:两个事务并发修改同一数据,后提交的事务覆盖了先提交事务的修改,导致部分操作丢失。
类型与示例
(1)提交覆盖(更常见)
两个事务均提交修改,后提交的覆盖先提交的。
步骤 Session A(事务 1) Session B(事务 2) 结果说明 1 SET @@tx_isolation='REPEATABLE READ';
SET @@tx_isolation='REPEATABLE READ';
双方设置隔离级别为 REPEATABLE READ
。2 BEGIN;
BEGIN;
事务启动。 3 SELECT money FROM account_t WHERE name='A';
(假设为 1000)SELECT money FROM account_t WHERE name='A';
(假设为 1000)两个事务均读取到 A
的余额为 1000。4 UPDATE account_t SET money=1000+100 WHERE name='A';
— Session A 修改余额为 1100(未提交)。 5 COMMIT;
UPDATE account_t SET money=1000+100 WHERE name='A'; COMMIT;
Session B 修改余额为 1100(覆盖 Session A 的修改)。 (2)回滚覆盖(数据库自动避免)
一个事务提交,另一个事务回滚,回滚的事务覆盖已提交的修改。但数据库通过锁机制(如行锁)可避免此情况。
解决方法:
- 加锁:使用
SELECT ... FOR UPDATE
对记录加排他锁,确保一次仅一个事务修改。- 乐观锁:通过版本号(如
version
字段)判断数据是否被修改,若版本号不符则重试。三、死锁(Deadlock)
定义:两个或多个事务因争夺锁资源,导致互相等待无法继续执行的现象。
3.1 常见类型
(1)相反加锁顺序死锁
- 场景:两个事务以相反顺序访问同一组资源(如表或行)。
- 示例:
- Session A 先锁
id=1
再锁id=2
;- Session B 先锁
id=2
再锁id=1
;- 若两者同时执行,会因互相等待对方释放锁而死锁。
(2)锁冲突死锁(RR 隔离级别常见)
- 场景:一个事务持有
Gap Lock
(间隙锁)或Next-Key Lock
(临键锁),另一个事务尝试插入数据时请求Insert Intention Lock
(插入意向锁),因锁冲突导致等待。3.2 检测与查看
- 系统表:
information_schema.INNODB_TRX
:查看当前事务。information_schema.INNODB_LOCKS
:查看当前锁。information_schema.INNODB_LOCK_WAITS
:查看锁等待关系。- 监控参数:
- 开启死锁日志:
SET GLOBAL innodb_print_all_deadlocks=ON;
(死锁信息记录到错误日志)。3.3 解决与避免策略
策略 说明 调整加锁顺序 确保所有事务按相同顺序访问资源(如按 id
升序加锁)。降低隔离级别 如从 REPEATABLE READ
调整为READ COMMITTED
,减少锁范围。优化索引 避免全表扫描(全表扫描会为所有行加锁,增加死锁概率)。 拆分大事务 大事务持有锁时间长,拆分为小事务可减少锁竞争。 四、隔离级别与并发异常的关系
不同隔离级别对并发异常的容忍度不同,如下表:
隔离级别 脏读 不可重复读 幻读 丢失更新(提交覆盖) READ UNCOMMITTED ✔️ ✔️ ✔️ ✔️ READ COMMITTED ❌ ✔️ ✔️ ✔️ REPEATABLE READ(默认) ❌ ❌ ✔️(需加锁) ✔️(需加锁) SERIALIZABLE ❌ ❌ ❌ ❌