03.1. 写入偏差与幻读
写入偏差与幻读
当不同的事务并发地尝试写入相同的对象时,会出现两种竞争条件,脏写与丢失更新。为了避免数据损坏,这些竞争条件需要被阻止;既可以由数据库自动执行,也可以通过锁和原子写操作这类手动安全措施来防止。但是,并发写入间可能发生的竞争条件还没有完。在本节中,我们将看到一些更微妙的冲突例子。
首先,想象一下这个例子:你正在为医院写一个医生轮班管理程序。医院通常会同时要求几位医生待命,但底线是至少有一位医生在待命。医生可以放弃他们的班次(例如,如果他们自己生病了
在两个事务中,应用首先检查是否有两个或以上的医生正在值班;如果是的话,它就假定一名医生可以安全地休班。由于数据库使用快照隔离,两次检查都返回
写偏差的特征
这种异常称为写偏差,它既不是脏写,也不是丢失更新,因为这两个事务正在更新两个不同的对象(
可以将写入偏差视为丢失更新问题的一般化。如果两个事务读取相同的对象,然后更新其中一些对象(不同的事务可能更新不同的对象
由于涉及多个对象,单对象的原子操作不起作用。不幸的是,在一些快照隔离的实现中,自动检测丢失更新对此并没有帮助。在
某些数据库允许配置约束,然后由数据库强制执行(例如,唯一性,外键约束或特定值限制
BEGIN TRANSACTION;
SELECT * FROM doctors
WHERE on_call = TRUE
AND shift_id = 1234 FOR UPDATE;
UPDATE doctors
SET on_call = FALSE
WHERE name = 'Alice'
AND shift_id = 1234;
COMMIT;
和以前一样,
会议室预订系统
假设你想强制执行,同一时间不能同时在两个会议室预订。当有人想要预订时,首先检查是否存在相互冲突的预订(即预订时间范围重叠的同一房间
BEGIN TRANSACTION;
-- 检查所有现存的与12:00~13:00重叠的预定
SELECT COUNT(*) FROM bookings
WHERE room_id = 123 AND
end_time > '2015-01-01 12:00' AND start_time < '2015-01-01 13:00';
-- 如果之前的查询返回0
INSERT INTO bookings(room_id, start_time, end_time, user_id)
VALUES (123, '2015-01-01 12:00', '2015-01-01 13:00', 666);
COMMIT;
不幸的是,快照隔离并不能防止另一个用户同时插入冲突的会议。为了确保不会遇到调度冲突,你又需要可序列化的隔离级别了。
多人游戏
我们使用一个锁来防止丢失更新(也就是确保两个玩家不能同时移动同一个棋子
抢注用户名
在每个用户拥有唯一用户名的网站上,两个用户可能会尝试同时创建具有相同用户名的帐户。可以在事务检查名称是否被抢占,如果没有则使用该名称创建账户。但是像在前面的例子中那样,在快照隔离下这是不安全的。幸运的是,唯一约束是一个简单的解决办法(第二个事务在提交时会因为违反用户名唯一约束而被中止
防止双重开支
允许用户花钱或积分的服务,需要检查用户的支付数额不超过其余额。可以通过在用户的帐户中插入一个试探性的消费项目来实现这一点,列出帐户中的所有项目,并检查总和是否为正值。有了写入偏差,可能会发生两个支出项目同时插入,一起导致余额变为负值,但这两个事务都不会注意到另一个。
导致写入偏差的幻读
所有这些例子都遵循类似的模式:
-
一个
SELECT
查询找出符合条件的行,并检查是否符合一些要求。 (例如:至少有两名医生在值班;不存在对该会议室同一时段的预定;棋盘上的位置没有被其他棋子占据;用户名还没有被抢注;账户里还有足够余额) -
按照第一个查询的结果,应用代码决定是否继续
。 (可能会继续操作,也可能中止并报错) -
如果应用决定继续操作,就执行写入(插入、更新或删除
) ,并提交事务。这个写入的效果改变了步骤2 中的先决条件。换句话说,如果在提交写入后,重复执行一次步骤1 的SELECT 查询,将会得到不同的结果。因为写入改变符合搜索条件的行集(现在少了一个医生值班,那时候的会议室现在已经被预订了,棋盘上的这个位置已经被占据了,用户名已经被抢注,账户余额不够了) 。
这些步骤可能以不同的顺序发生。例如可以首先进行写入,然后进行
在医生值班的例子中,在步骤SELECT FOR UPDATE
)来使事务安全并避免写入偏差。但是其他四个例子是不同的:它们检查是否不存在某些满足条件的行,写入会添加一个匹配相同条件的行。如果步骤SELECT FOR UPDATE
锁不了任何东西。
这种效应:一个事务中的写入改变另一个事务的搜索查询的结果,被称为幻读。快照隔离避免了只读查询中幻读,但是在像我们讨论的例子那样的读写事务中,幻影会导致特别棘手的写歪斜情况。
物化冲突
如果幻读的问题是没有对象可以加锁,也许可以人为地在数据库中引入一个锁对象?例如,在会议室预订的场景中,可以想象创建一个关于时间槽和房间的表。此表中的每一行对应于特定时间段(例如
现在,要创建预订的事务可以锁定(SELECT FOR UPDATE)表中与所需房间和时间段对应的行。在获得锁定之后,它可以检查重叠的预订并像以前一样插入新的预订。请注意,这个表并不是用来存储预订相关的信息——它完全就是一组锁,用于防止同时修改同一房间和时间范围内的预订。
这种方法被称为物化冲突(materializing conflicts