幻读

概述

一个事务中的写入改变另一个事务的搜索查询的结果,被称为幻读(Phantom)

幻读是针对多对象查询而言的,可能是看到了不期望看到的数据,从而影响查询,也可能是没看到期望看到的数据,从而影响写入

快照隔离避免了只读查询中幻读,但无法避免读写事务中写倾斜()。

备注

区别于脏读

  • 脏读是当前事务读取到了未提交事务的数据。

  • 幻读的发生可能是读取到了其他事务提交的数据,也可能是没有读到其他事务提交

    • 读取到了其他事务提交的数据,影响了当前事务的判断逻辑:读提交隔离级别下,第一读和第二次读之间,有其他事务新增了一条数据并提交,导致两次读取总行数不同

    • 未读取到其他事务提交的数据,影响了当前事务的判断逻辑:在 MVVC 实现的可重复读隔离级别下,写入的内容依赖之前读取的结果,但是读写之间可能有其他事务提交,影响读的结果,把本不应该写入的数据写入到数据库了,也就是本应该看见的数据却没有看见

区别于读倾斜

  • 读倾斜是针对一行记录,多次读取不同字段的结果不同

  • 幻读是是针对多行记录,多次读取结果不同(比如多一行)

产生情景

影响查询的幻读

读取到了其他事务提交的数据,影响了当前事务的判断逻辑,即读提交隔离级别下,第一读和第二次读之间,有其他事务新增了一条数据并提交,导致两次读取总行数不同

alt text

影响写入的幻读

模式

  1. 一个 SELECT 查询找出符合条件的行,并检查是否符合一些要求。(例如:至少有两名医生在值班;不存在对该会议室同一时段的预定;棋盘上的位置没有被其他棋子占据;用户名还没有被抢注;账户里还有足够余额)

  2. 按照第一个查询的结果,应用代码决定是否继续。(可能会继续操作,也可能中止并报错)

  3. 如果应用决定继续操作,就执行写入(插入、更新或删除),并提交事务。

问题就出在这些步骤在数据库上可能是并发执行,当前事务提交前,可能有其他事务提交导致步骤 2 的判断结果发生变化,就不应该执行步骤 3 的, 但是在可使用快照隔离技术实现的重复读隔离级别下,当前事务时读取不到其他事务提交的数据的,所以依赖当前事务的查询结果进行写入,就可能因为没有读到实际的数据出现幻读,从而发生写入偏差。

值班医生

比如医生值班例子: 你正在为医院写一个医生轮班管理程序。医院通常会同时要求几位医生待命,但底线是至少有一位医生在待命。医生可以放弃他们的班次(例如,如果他们自己生病了),只要至少有一个同事在这一班中继续工作。 现在想象一下,Alice 和 Bob 是两位值班医生。两人都感到不适,所以他们都决定请假。不幸的是,他们恰好在同一时间点击按钮下班, 下图说明了接下来的事情。

alt text

在两个事务中,应用首先检查是否有两个或以上的医生正在值班;如果是的话,它就假定一名医生可以安全地休班。由于数据库使用快照隔离,两次检查都返回 2 ,所以两个事务都进入下一个阶段。Alice 更新自己的记录休班了,而 Bob 也做了一样的事情。两个事务都成功提交了,现在没有医生值班了。违反了至少有一名医生在值班的要求。

其他例子

  • 会议室预订系统

  • 多人游戏(竞争某个资源)

  • 抢注用户名

  • 防止双重开支(允许用户花钱或使用积分的服务,需要检查用户的支付数额不超过其余额)

防止幻读

物化冲突

幻读的问题是没有对象可以加锁,可以人为地在数据库中引入一个锁对象,可以将幻读变为数据库中一组具体行上的锁冲突。

在 MySQL 中可以使用 FOR UPDATE 显式的索引行记录,但建议使用主键或唯一索引来查询锁定一行数据充当锁,如果锁定多行,很容易出现死锁,锁表又很响应性能。

可串行化

使用可串行化(Serializable) 的隔离级别。