likes
comments
collection
share

事务之写倾斜和幻读

作者站长头像
站长
· 阅读数 11

本系列主要是《数据密集型应用系统设计》阅读笔记,本文记录事务主题的笔记心得。

当多个事务同时写入同一对象时引入了两种竞争条件,也就是脏写和更新丢失。为了避免数据不一致,需要借助数据库的一些内置机制,或者采取手动加锁、执行原子操作等。 然而这不算并发写引发的全部问题。还有一些更微妙的并发写的问题。

写倾斜场景

首先,设想你正在开发一个应用程序来帮助医生管理医院的轮班。通常,医院会安排多个医生值班,医生也可以申请调整班次,但前提是确保至少一位医生还在该班次中值班。

现在情况是,老王和老张说两位值班医生,碰巧都身体不适决定请假。他们几乎同一时刻点击了调班按钮,接下来的事情如下图所示。

事务之写倾斜和幻读

每个事务总是首先检查是否至少有两个医生在值班,如果是,则当前的医生可以安全的离开。这里使用快照隔离级别,但是上面这种场景下,这两个医生都成功提交,都可以正常休假,最终是没有任何医生在值班了

这种情况称为写倾斜,两个事务更新的是不同的对象,写冲突看起来不那么直接,但是在这里,两个事务读取相同的一组对象,然后更新其中一部分,不同的事务可能更新不同的对象,则可能发生写倾斜。最终是违背了业务逻辑。

对于这种的问题,很难处理

  • 单对象的原子操作不起作用了。
  • 快照隔离级别都解决不了这个问题,需要串行化隔离级别
  • 数据库虽然支持一些自定义约束,但是对涉及多个对象的约束支持不好
  • 不使用串行化隔离级别的次优选择是对选择的多行对象显示的加锁,可以这样:
BEGIN ;
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 

for update会给返回的所有结果行自动加锁。

更多的场景

这样的问题看起来有点晦涩拗口,但了解问题的本质,可以看到更多的场景。比如:

  • 会议室预定系统: 一个会议室、同一时间不能被预定两次。当有人想要预定时,首先检查是否有冲突的预定,如果没有则提交申请。
BEGIN ;
select count(*) from bookings
where  room_id=1234
and end_time>'2022-06-06 12:00' and start_time<'2022-06-06 13:00';
//上面的条件返回0则执行
insert into  bookings(room_id,start_time,end_time,user_id)
values(123,'2022-06-06 12:00','2022-06-06 13:00',666);
commit

这个时候,如果 room_id=1234 and end_time>'2022-06-06 12:00' and start_time<'2022-06-06 13:00'过滤条件没有对应的行,则加for update也没有用,此时需要串行化的隔离

  • 多人游戏: 假设玩家将两个不同的数字移动到棋盘的同一个位置,则对数字加锁也不能解决。则需要更多的条件约束,否则很容易发生写倾斜。

问题的本质

上面的场景以及更多的一些场景都遵循了以下模式;

  1. 首先select查询所有满足条件的行
  2. 根据查询的结果,应用层代码进行进一步的操作
  3. 如果应用程序决定执行,将发起数据库写入(update/delete或者insert)并提交操作。

对于医生值班的例子,我们可以使用for update加锁步骤1的结果来保证事务安全。但是对于会议室预定系统这个场景则不适合,因为for update无从加锁。串行化的隔离的能解决这个问题,如果有更高的并发要求,也许可以尝试实体化冲突的解决方案:构造一个会议室预定(6个月)的表(每一行对应特定时间段的特定房间),这样查询的时候便可以先加锁。

这种在一个事务中的写入改变了另一个事务查询结果的现象,也称为幻读