为啥在可重复读取中会发生写入偏斜?

Posted

技术标签:

【中文标题】为啥在可重复读取中会发生写入偏斜?【英文标题】:Why write skew can happen in Repeatable reads?为什么在可重复读取中会发生写入偏斜? 【发布时间】:2018-07-03 05:05:31 【问题描述】:

Wiki 说;

可重复阅读: 在这个隔离级别,一个基于锁的 并发控制 DBMS 实现保持读写锁 (获取所选数据)直到交易结束。然而, 范围锁不受管理,因此可能发生幻读。

在此隔离级别可能会出现写入偏差,这种现象会导致两个 允许两个不同的写入表中的相同列 作家(之前已经阅读过他们正在更新的专栏), 导致该列的数据是两者的混合 交易。

我很好奇为什么write skew 会发生在Repeatable reads 中? 它说它将保持读写锁直到事务结束并且write skew发生在previously read the columns they are updating时,那么当读锁被锁定时如何锁定写锁?​​

【问题讨论】:

【参考方案1】:

可重复读隔离级别保证每个事务都会从数据库的consistent snapshot 中读取。换句话说,在同一事务中检索两次的行总是具有相同的值。

许多数据库(如 Postgres、可重复读取隔离级别的 SQLServer)可以检测到lost update(写入倾斜的一种特殊情况),但其他数据库则不能。 (即:mysql 中的 InnoDB 引擎)

我们回来写歪斜现象问题。在可重复读隔离中,存在大多数数据库引擎无法检测到的情况。一种情况是 2 个并发事务 修改 2 个不同的对象 并产生竞争条件。

我以Designing Data-Intensive Application 书中的一个例子为例。这是场景:

您正在为医生编写一个应用程序来管理他们的待命情况 在医院轮班。医院通常会尝试有几个 随时待命的医生,但绝对必须至少有 一名随叫随到的医生。医生可以放弃轮班(例如,如果他们 自己生病了),前提是至少有一名同事留在 调用那个班次

下一个有趣的问题是我们如何在数据库下实现它。这是伪代码SQL代码:

BEGIN TRANSACTION;
    SELECT * FROM doctors
        WHERE on_call = true
        AND shift_id = 1234;
    if (current_on_call >= 2) 
        UPDATE doctors
        SET on_call = false WHERE name = 'Alice' AND shift_id = 1234;
    
COMMIT;  

下面是插图:

如上图所示,我们看到 Bob 和 Alice 在 SQL 代码之上同时运行。但是 Bob 和 Alice 修改了不同的数据,Bob 修改了 Bob 的记录,Alice 修改了 Alice 的记录。处于可重复读取隔离级别的数据库无法知道并检查是否违反了条件(总医生> = 2)。发生了写歪斜现象。

为了解决这个问题,提出了2种方法:

    锁定所有手动调用的记录。所以 Bob 或 Alice 都会等到其他人完成交易。

这是一些使用SELECT .. FOR UPDATE 查询的伪代码。

BEGIN TRANSACTION;
    SELECT * FROM doctors
        WHERE on_call = true
        AND shift_id = 1234 FOR UPDATE; // important here: locks all records that satisfied requirements.

    if (current_on_call >= 2) 
        UPDATE doctors
        SET on_call = false WHERE name = 'Alice' AND shift_id = 1234;
    
  COMMIT;  
    使用更严格的隔离级别。 MySQL、Postgres T-SQL 都提供序列化隔离级别。

【讨论】:

感谢您提供非常清晰的解释。我有一点意见。您说 MySQL Serializeable IL 默认情况下可能会阻止 Write Skew 现象,对吧?MySQL manual 在 Serializeable IL InnoDB implicitly converts all plain SELECT statements to SELECT ... FOR SHARE 中声明。我认为这意味着即使在 MySQL 中使用 Serializeable IL,纯 2PL 共享锁也无法防止所述场景中的写入倾斜,因为两个事务仍然可以并行读取行并评估 if 语句 继续我之前的评论:实际上你写的一切都是正确的。在 MySQL 中的 Serializable 隔离级别的情况下,假设事务语句的交错与您的示例中相同,两个事务都将在每个医生记录上获取 S 锁定,然后两者都将评估 current_on_call >= 2 语句导致 true,然后两者事务将被阻止,因为执行Update 语句他们需要X 锁,但他们无法获取它,因为其他事务持有S 锁定所有医生记录。结果 - 死锁。一项事务将被回滚 - 防止写入倾斜 我写了一篇博客来测试 MariaDB/MySQL 中的隔离级别。 medium.com/@huynhquangthao/…本次测试的重要部分是可重复读取和可序列化的测试。用 MySQL 测试后,我看到: - 可重复读隔离可以检测幻读。但是您仍然可以更新由不同事务创建的记录。 - 可序列化隔离可以防止这种行为:不同的事务不能修改可能改变其他事务结果的数据。 这种解释似乎很奇怪——可重复的读取隔离不应该为在事务中执行的选择提供相同的结果吗?那么“current_on_call”的值不应该改变吗?我对 SQL Server 的基本测试表明更新被锁定在可重复读取中。 "在同一事务中检索两次的行总是具有相似的值。" - 相似的?不一样? (假设事务中没有 UPDATE - 但它可能根本不相似)

以上是关于为啥在可重复读取中会发生写入偏斜?的主要内容,如果未能解决你的问题,请参考以下文章

不可重复读和幻读的区别

Mysql在可重复读隔离级别下死锁排查过程

锁定 SQL Server 表以进行写入但不用于读取 [重复]

事务的四种隔离级别和七种传播行为

MySQL可重复读防止幻读

MySQL的可重复读级别能解决幻读问题吗?