多角度彻底理解数据库事务中的脏读,虚读和不可重复读

本文详细解释了数据库事务并发操作中可能出现的各种问题,包括脏读、不可重复读、虚读及丢失更新等,并提供了实例帮助理解。
脏读 dirty   reads:当事务读取还未被提交的数据时,就会发生这种事件。举例来说:Transaction   1   修改了一行数据,然后   Transaction   2   在   Transaction   1   还未提交修改操作之前读取了被修改的行。如果   Transaction   1   回滚了修改操作,那么   Transaction   2   读取的数据就可以看作是从未存在过的。     
  不可重复的读 non-repeatable   reads:当事务两次读取同一行数据,但每次得到的数据都不一样时,就会发生这种事件。举例来说:Transaction   1   读取一行数据,然后   Transaction   2   修改或删除该行并提交修改操作。当   Transaction   1   试图重新读取该行时,它就会得到不同的数据值(如果该行被更新)或发现该行不再存在(如果该行被删除)。     
  虚读 phantom   read:如果符合搜索条件的一行数据在后面的读取操作中出现,但该行数据却不属于最初的数据,就会发生这种事件。举例来说:Transaction   1   读取满足某种搜索条件的一些行,然后   Transaction   2   插入了符合   Transaction   1   的搜索条件的一个新行。如果   Transaction   1   重新执行产生原来那些行的查询,就会得到不同的行。


事务场景是这样的:
对于同一个银行帐户A内有200元,甲进行提款操作100元,乙进行转帐操作100元到B帐户。如果事务没有进行隔离可能会并发如下问题:
1、第一类丢失更新:首先甲提款时帐户内有200元,同时乙转帐也是200元,然后甲乙同时操作,甲操作成功取走100元,乙操作失败回滚,帐户内最终为200元,这样甲的操作被覆盖掉了,银行损失100元。
2、脏读:甲取款100元未提交,乙进行转帐查到帐户内剩有100元,这是甲放弃操作回滚,乙正常操作提交,帐户内最终为0元,乙读取了甲的脏数据,客户损失100元。
3、虚读:和脏读类似,是针对于插入操作过程中的读取问题,如丙存款100元未提交,这时银行做报表进行统计查询帐户为200元,然后丙提交了,这时银行再统计发现帐户为300元了,无法判断到底以哪个为准?
 大家好像觉得统计这个东西肯定是时时更新的,这种情况很正常;但是如果统计是在一个事务中的时候就不正常了,比如我们的一个统计应用需要将统计结果分别输出到电脑屏幕和远程网络某台计算机的磁盘文件中,为了
 提高性能和用户响应我们分成2个线程,这时先完成的和后完成的统计数据就可能不一致,我们就不知道以哪个为准了。
4、不可重复读:甲乙同时开始都查到帐户内为200元,甲先开始取款100元提交,这时乙在准备最后更新的时候又进行了一次查询,发现结果是100元,这时乙就会很困惑,不知道该将帐户改为100还是0。
和脏读的区别是,脏读是读取前一事务未提交的脏数据,不可重复读是重新读取了前一事务已提交的数据。
5、第二类丢失更新:是不可重复读的一种特例,如上,乙不做第二次查询而是直接操作完成,帐户内最终为100元,甲的操作被覆盖掉了,银行损失100元。感觉和第一类丢失更新类似。


在多个事务并发做数据库操作的时候,如果没有有效的避免机制,就会出现种种问题。大体上有三种问题,归结如下:
1、丢失更新
如果两个事务都要更新数据库一个字段X,x=100
事务A             事务B
读取X=100     
                  读取X=100
写入x=X+100
                  写入x=X+200
事务结束x=200
                  事务结束x=300
最后x==300
这种情况事务A的更新就被覆盖掉了、丢失了。
丢失更新说明事务进行数据库写操作的时候可能会出现的问题。
2、不可重复读
一个事务在自己没有更新数据库数据的情况,同一个查询操作执行两次或多次的结果应该是一致的;如果不一致,就说明为不可重复读。
还是用上面的例子
事务A             事务B
读取X=100     
                  读取X=100
读取X=100     
                  写入x=X+100
读取X=200     
事务结束x=200
                  事务结束x=200
这种情况事务A多次读取x的结果出现了不一致,即为不可重复读。
再有一情况就是幻影
事务A读的时候读出了15条记录,事务B在事务A执行的过程中删除(增加)了1条,事务A再读的时候就变成了14(16)条,这种情况就叫做幻影读。
不可重复读说明了做数据库读操作的时候可能会出现的问题。
3、脏读(未提交读)
防止一个事务读到另一个事务还没有提交的记录。
如:
事务A             事务B
                  读取X=100
                  写入x=X+100
读取X=200     
                  事务回滚x=100
读取X=100     
事务结束x=100

x锁 排他锁 被加锁的对象只能被持有锁的事务读取和修改,其他事务无法在该对象上加其他锁,也不能读取和修改该对象
s锁 共享锁 被加锁的对象可以被持锁事务读取,但是不能被修改,其他事务也可以在上面再加s锁。
封锁协议:
一级封锁协议:
在事务修改数据的时候加x锁,直到事务结束(提交或者回滚)释放x锁。一级封锁协议可以有效的防止丢失更新,但是不能防止脏读不可重复读的出现。
二级封锁协议:
在一级封锁的基础上事务读数据的时候加s锁,读取之后释放。二级封锁协议可以防止丢失更新,脏读。不能防止不可重复读。
三级封锁协议:
在一级封锁的基础上事务读数据的时候加s锁,直到事务结束释放。二级封锁协议可以防止丢失更新,脏读,不可重复读。


<think>嗯,用户这次的问题是关于如何避免数据库中的不可重复问题。从用户引用的内容来看,ta已经对隔离级别这些现象有了基本了解,但需要更具体的解决方案。用户可能正在设计一个需要高数据一致性的系统,比如金融应用,或者遇到了相关的并发问题。 上次我详细解释了ACID特性,这次需要聚焦在隔离性相关的具体实践上。用户引用的资料[1][2]提到REPEATABLE READ不能完全避免幻,这点需要重点说明。考虑到用户是技术人员,应该需要知道不同数据库的实现差异(比如MySQL的间隙锁机制)。 用户可能还存在这些隐藏需求:如何平衡隔离级别性能?实际开发中该用哪些SQL语句控制事务?是否需要演示代码示例? 从专业角度,我需要明确几个关键点: 1)的避免最简单,READ COMMITTED就能解决 2)不可重复需要锁定已存在的数据行 3)幻的解决最复杂,需要锁住不存在的范围(间隙锁/临键锁) 4)还要提醒用户MVCC在快照隔离级别下的特殊行为 特别要注意引用[2]提到的"REPEATABLE READ不能完全避免幻"这个误区,需要解释清楚InnoDB如何通过Next-Key Locking真正解决幻。可以准备一个带间隙锁的SQL示例。 最后考虑设计一个对比表格,清晰展示不同隔离级别对应的解决方案。用户似乎喜欢结构化信息,上次的表格反馈应该不错。同时要生成相关问题引导深入学习,比如死锁处理、乐观锁方案等。</think>以下是避免数据库不可重复问题的完整解决方案,通过隔离级别锁机制实现: --- ### 一、三大并发问题本质 | **问题类型** | **现象描述** | **发生场景** | |--------------------|----------------------------------------------------------------------------|---------------------------------------| | ** (Dirty Read)** | 事务A读取事务B**未提交的修改**,事务B回滚导致事务A读取无效数据[^3][^4] | `READ UNCOMMITTED`隔离级别 | | **不可重复 (Non-Repeatable Read)** | 事务A多次读取同一数据,期间事务B**修改并提交**该数据,导致事务A读取结果不一致[^1] | `READ COMMITTED`及以上可避免 | | **幻 (Phantom Read)** | 事务A多次查询同一范围,期间事务B**新增/删除**符合条件的数据,导致事务A两次结果集不同[^2] | `REPEATABLE READ`需配合锁机制完全避免 | --- ### 二、解决方案与实现机制 #### 1. **的避免** - **核心方法**:确保只能读取**已提交的数据** - **实现方案**: ```sql -- 设置隔离级别为READ COMMITTED或更高 SET TRANSACTION ISOLATION LEVEL READ COMMITTED; ``` - **底层机制**: - 写操作加**排他锁(X锁)**,阻止其他事务读取未提交数据[^1] - MVCC机制:只读取已提交的快照版本 #### 2. **不可重复的避免** - **核心方法**:锁定被读取的数据行,阻止其他事务修改 - **实现方案**: ```sql -- 设置隔离级别为REPEATABLE READ SET TRANSACTION ISOLATION LEVEL REPEATABLE READ; -- 显式加共享锁(S锁) (MySQL示例) SELECT * FROM accounts WHERE id = 1 LOCK IN SHARE MODE; ``` - **底层机制**: - 事务持续持有**行级共享锁**,直到事务结束 - MVCC机制:整个事务使用同一数据快照 #### 3. **幻的完全避免** - **核心方法**:锁定**数据范围**而不仅是现有数据行 - **实现方案**: ```sql -- 设置隔离级别为SERIALIZABLE (完全串行化) SET TRANSACTION ISOLATION LEVEL SERIALIZABLE; -- 或使用间隙锁(Gap Lock) (MySQL InnoDB) SELECT * FROM accounts WHERE balance > 1000 FOR UPDATE; -- 锁定balance>1000的范围 ``` - **底层机制**: - **间隙锁(Gap Lock)**:锁定索引记录之间的范围[^2] - **临键锁(Next-Key Lock)**:行锁+间隙锁组合(MySQL默认方案) - 表级锁:`SERIALIZABLE`级别下自动加表锁 --- ### 三、不同隔离级别解决方案对比 | **隔离级别** | | 不可重复 | 幻 | **实现机制** | |----------------------|------|------------|------|----------------------------------------------------------------------------| | `READ UNCOMMITTED` | ❌ | ❌ | ❌ | 无锁读取 | | `READ COMMITTED` | ✅ | ❌ | ❌ | 行级排他锁(X锁),MVCC快照 | | `REPEATABLE READ` | ✅ | ✅ | ⚠️* | 行级共享锁(S锁)+间隙锁,MVCC一致性快照 (*MySQL可避免幻) [^1][^2] | | `SERIALIZABLE` | ✅ | ✅ | ✅ | 表级锁/范围锁,完全串行执行 | > *注:MySQL的`REPEATABLE READ`通过Next-Key Locking可完全避免幻,但某些数据库(如Oracle)此级别仍可能出现幻* --- ### 四、实战示例 #### 场景:银行账户余额校验(避免不可重复) ```sql -- 会话A (事务1) SET TRANSACTION ISOLATION LEVEL REPEATABLE READ; START TRANSACTION; SELECT balance FROM accounts WHERE id = 1; -- 第一次读取(返回1000) -- 会话B (事务2) UPDATE accounts SET balance = 800 WHERE id = 1; -- 提交修改 -- 会话A再次读取 (仍返回1000,避免不可重复) SELECT balance FROM accounts WHERE id = 1; COMMIT; ``` #### 场景:范围查询防幻 ```sql -- 会话A SET TRANSACTION ISOLATION LEVEL REPEATABLE READ; START TRANSACTION; -- 锁定salary>5000的范围 SELECT * FROM employees WHERE salary > 5000 FOR UPDATE; -- 会话B尝试插入新记录 (被阻塞) INSERT INTO employees (name, salary) VALUES ('John', 6000); -- 等待锁释放 -- 会话A提交后会话B才执行 COMMIT; ``` --- ### 五、最佳实践建议 1. **隔离级别选择** - 金融系统:`REPEATABLE READ`(MySQL)或 `READ COMMITTED`(Oracle) - 报表系统:`READ COMMITTED`(平衡一致性性能) 2. **锁优化策略** ```sql -- 缩小锁定范围 SELECT * FROM orders WHERE order_date BETWEEN '2023-01-01' AND '2023-01-31' FOR UPDATE; -- 精确锁定日期区间 ``` 3. **事务设计原则** - 事务尽量短小,减少锁定时间 - 避免事务内交互式操作(如等待用户输入) 4. **监控与调优** - 监控`innodb_row_lock_waits`(MySQL) - 使用`EXPLAIN`分析索引使用,减少全表扫描导致的锁升级 > ⚠️ **死锁预防**:保持一致的资源访问顺序,例如总是按ID升序更新记录[^1]。 ---
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值