数据库常见并发冲突详解
关系型数据库常见并发冲突详解与总结
为什么会有冲突#
为什么保证数据库的 AICD 特性, 通常数据库调度的时候希望事务的执行是一个串行调度, 但是实际中, 由于事务是并发执行的, 因此会出现各种冲突, 而事务的并发控制协议, 并发控制算法的目的则是处理并发事务中的冲突以实现并发冲突的可串行化. 并发冲突的可串行化在 传送门 这篇博客中已经解释了.
我们总结并举例常见的冲突类型如下:
不可重复读(Non-Repeatable Read)#
定义#
读-写(R-W)冲突是指一个事务(T1)读取某个数据后, 另一个事务(T2)对该数据进行了修改并提交, 导致 T1 在同一事务中再次读取该数据时, 发现其值已经发生了变化.
这种现象也称为 不可重复读(Non-Repeatable Read), 因为 T1 在事务执行过程中, 无法保证对同一数据的两次读取返回相同的值.
示例#
假设数据库中有一张 账户表(Accounts), 初始数据如下:
+----+--------+
| ID | Balance |
+----+--------+
| 1 | 100 |
+----+--------+
事务 T1(读取账户余额):
BEGIN TRANSACTION;
SELECT Balance FROM Accounts WHERE ID = 1; -- 读取结果:100
事务 T2(更新余额并提交):
BEGIN TRANSACTION;
UPDATE Accounts SET Balance = 50 WHERE ID = 1;
COMMIT;
事务 T1 再次读取:
SELECT Balance FROM Accounts WHERE ID = 1; -- 读取结果:50
COMMIT;
问题:
T1 在事务执行过程中, 两次读取同一行数据, 结果却不一致(从 100 变成了 50). 这种现象违背了事务的 隔离性(Isolation), 导致事务执行过程中数据视图不稳定.
写-读(W-R)冲突 / 脏读(Dirty Read)#
定义#
写-读(W-R)冲突, 也称为 脏读(Dirty Read), 指的是: 一个事务(T1)修改了某个数据, 但 未提交. 另一个事务(T2)读取了这个未提交的修改.
如果 T1 最后回滚(Rollback), T2 读取的数据就变成了无效的脏数据. 这会导致 T2 依赖了一个可能不存在或不稳定的状态, 违反了事务的一致性(Consistency)
示例#
假设数据库中有一张 账户表(Accounts), 初始数据如下:
+----+--------+
| ID | Balance |
+----+--------+
| 1 | 100 |
+----+--------+
事务 T1(未提交的写入):
BEGIN TRANSACTION;
UPDATE Accounts SET Balance = 50 WHERE ID = 1; -- 账户余额修改为 50
-- 但还没有提交(COMMIT)
事务 T2(读取未提交数据):
BEGIN TRANSACTION;
SELECT Balance FROM Accounts WHERE ID = 1; -- 读取结果: 50
COMMIT;
此时, T2 读取到了 T1 未提交的数据(50), 这就是脏读.
事务 T1 回滚(撤销更改):
ROLLBACK; -- 取消修改, 账户余额恢复为 100
现在, T2 读到的 50 实际上从未正式存在过, 但它已经基于这个错误数据进行了决策或计算, 导致数据不一致.
写-写(W-W)冲突 / 丢失更新(Lost Update)#
定义#
写-写(W-W)冲突, 也叫 丢失更新(Lost Update), 指的是: 两个并发事务(T1 和 T2)同时修改 同一行数据.
其中一个事务的修改 被另一个事务覆盖, 导致更新丢失. 最终, 数据库中的数据状态 不符合任何串行化调度的结果.
示例: 丢失更新#
假设数据库中有一张 账户表(Accounts), 初始数据如下:
+----+--------+
| ID | Balance |
+----+--------+
| 1 | 100 |
+----+--------+
事务 T1(读取并更新账户余额):
BEGIN TRANSACTION;
SELECT Balance FROM Accounts WHERE ID = 1; -- 读取到 Balance = 100
UPDATE Accounts SET Balance = Balance - 30 WHERE ID = 1; -- 余额修改为 70
-- 但还没有提交(COMMIT)
事务 T2(并发执行, 也读取并更新账户余额):
BEGIN TRANSACTION;
SELECT Balance FROM Accounts WHERE ID = 1; -- 读取到 Balance = 100
UPDATE Accounts SET Balance = Balance - 20 WHERE ID = 1; -- 余额修改为 80
COMMIT;
事务 T1 提交:
COMMIT; -- 余额变为 70
问题:
T1 和 T2 都读取了初始余额 100, 然后各自执行了扣款操作.T2 先提交, 数据库余额更新为 80.T1 再提交, 余额变为 70, 覆盖了 T2 的修改! T2 的扣款 20 元的操作丢失了, 最终余额错误!
如果按正确的 串行化调度, 事务 T1 和 T2 应该依次执行, 即: T2 先执行完, 则余额应该是 80, 再由 T1 执行后变成 50.
但现在由于 W-W 冲突, 余额变成了 70, 不符合任何正确的串行调度.
Write Skew(写偏差)#
定义#
写偏差(Write Skew) 是指两个事务并发执行时, 分别读取某个数据集, 并基于该数据集的状态 做出不同的更新, 导致最终结果不符合串行化调度.
这种情况通常发生在 多个独立对象 但具有某种约束(constraint)的情况下.
例如在 MVCC 中由于事务是基于快照进行操作, 因此无法检测到数据已经被修改.
示例: 黑白弹珠问题
假设数据库中有一张 marbles 表:
CREATE TABLE marbles (
id INT PRIMARY KEY,
color TEXT -- 'white' 或 'black'
);
初始状态:
id | color
---+------
1 | white
2 | black
两个事务并发执行
T1(事务 1)修改白色弹珠变黑:
BEGIN;
SELECT * FROM marbles; -- 看到 (1, 'white') 和 (2, 'black')
UPDATE marbles SET color = 'black' WHERE color = 'white';
T2(事务 2)修改黑色弹珠变白:
BEGIN;
SELECT * FROM marbles; -- 也看到 (1, 'white') 和 (2, 'black')
UPDATE marbles SET color = 'white' WHERE color = 'black';
T1 和 T2 提交事务
COMMIT; -- T1 先提交
COMMIT; -- T2 也提交
最终状态:
id | color
---+------
1 | black
2 | white
问题: 这个最终状态不是任何串行调度的结果!
如果先执行 T1, 再执行 T2, 所有弹珠应该都是白色.
如果先执行 T2, 再执行 T1, 所有弹珠应该都是黑色.
但这里的最终状态是 (1, black), (2, white), 完全违背了串行化规则.
WVCC 仅根据快照隔离实现版本控制无法解决该问题, 因为 T1 和 T2 事务在开始执行的时候可能读取到的是同一个快照, 但是可以看到执行结果却不正确.
Phantom Read(幻读)#
定义#
幻读(Phantom Read) 发生在事务执行两次相同的查询, 但第二次查询结果不一样, 因为另一个事务插入或删除了数据.
事务读取的不是一个稳定的数据集, 而是一个不断变化的快照.
示例: 银行账户最低余额#
假设数据库中有一张 账户表(Accounts):
+----+--------+
| ID | Balance |
+----+--------+
| 1 | 500 |
| 2 | 600 |
+----+--------+
事务 T1(检查所有账户是否余额 >= 500)
BEGIN TRANSACTION;
SELECT COUNT(*) FROM Accounts WHERE Balance < 500; -- 读取结果:0
事务 T2(插入一个新账户)
BEGIN TRANSACTION;
INSERT INTO Accounts (ID, Balance) VALUES (3, 400);
COMMIT;
事务 T1(再次检查账户余额)
SELECT COUNT(*) FROM Accounts WHERE Balance < 500; -- 读取结果:1
COMMIT;
问题
T1 第一次查询 发现没有余额低于 500 的账户, 但 第二次查询 却发现了余额 400 的账户.
T1 以为数据集是稳定的, 但其实数据在事务执行期间发生了变化.
幻读与不可重复的区别比较模糊, 总体来上说就是某个事务执行两次相同的查询但是查询结果不一致. 硬要说区别的话, 不可重复度强调同一个事务在不同时间读取相同的数据, 但由于并发事务的修改, 导致数据内容不一致. 而幻读更强调同一个事务在不同时间执行相同的查询, 但由于并发事务的插入或删除, 导致返回的数据行数不一致. 幻读中由于存在插入新数据的操作, 我们不能对数据库中不存在的数据加锁, 因此更加难以避免, 例如仅仅设置行级锁无法解决, 因为可能插入新的行, 只有设置表级锁或者范围锁才可以.
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· TypeScript + Deepseek 打造卜卦网站:技术与玄学的结合
· 阿里巴巴 QwQ-32B真的超越了 DeepSeek R-1吗?
· 【译】Visual Studio 中新的强大生产力特性
· 10年+ .NET Coder 心语 ── 封装的思维:从隐藏、稳定开始理解其本质意义
· 【设计模式】告别冗长if-else语句:使用策略模式优化代码结构