【问题标题】:Why does MySQL explicit predicate locking disallow INSERT statements outside of the predicate lock为什么 MySQL 显式谓词锁定不允许在谓词锁之外插入语句
【发布时间】:2017-07-08 02:18:53
【问题描述】:

假设我们有以下数据库表:

create table department (
    id bigint not null, 
    budget bigint not null, 
    name varchar(255), 
    primary key (id)
) ENGINE=InnoDB

create table employee (
    id bigint not null, 
    name varchar(255), 
    salary bigint not null, 
    department_id bigint, primary key (id)
) ENGINE=InnoDB

alter table employee 
add constraint FK_department_id 
foreign key (department_id) 
references department (id)

我们有 2 个departments:

insert into department (name, budget, id) 
values ('Hypersistence', 100000, 1)

insert into department (name, budget, id) 
values ('Bitsystem', 10000, 2)

还有 3 个employees 在第一部门:

insert into employee (department_id, name, salary, id) 
values (1, 'John Doe 0', 30000, 0)

insert into employee (department_id, name, salary, id) 
values (1, 'John Doe 1', 30000, 1)

insert into employee (department_id, name, salary, id) 
values (1, 'John Doe 2', 30000, 2)

假设我们有两个并发用户:Alice 和 Bob。

首先,Alice 锁定属于第一个 department 的所有员工,并获得该特定 department 的工资总和:

SELECT * 
FROM employee 
WHERE department_id = 1 
FOR UPDATE

SELECT SUM(salary) 
FROM employee 
where department_id = 1 

现在,与此同时,预计 Bob 不能使用相同的 department_id 插入新的 employee

insert into employee (department_id, name, salary, id) 
values (1, `Carol`, 9000, 4)

com.mysql.jdbc.exceptions.jdbc4.MySQLTransactionRollbackException: 
    Lock wait timeout exceeded; try restarting transaction

因此,锁阻止了 Bob 针对同一谓词发出插入。

但是,即使 Bob 尝试在不同的 department 中插入 employee,也会引发相同的异常:

insert into employee (department_id, name, salary, id) 
values (2, `Dave`, 9000, 5)

com.mysql.jdbc.exceptions.jdbc4.MySQLTransactionRollbackException: 
    Lock wait timeout exceeded; try restarting transaction

最后一个插入语句正在使用第二个department_id,因此该行不应与我们为其获取谓词锁的选择语句重叠。

为什么 MySQL 会阻止与第一个事务获取的谓词锁不重叠的第二个插入?

在 SQL Server 上也可以观察到相同的行为。

更新

将隔离级别更改为 READ_COMMITTED 时,谓词锁不会阻止 Bob 发出的两个插入语句中的任何一个。

如果考虑到this Percona blog post 的以下陈述,这可以解释:

在 REPEATABLE READ 中,事务期间获得的每个锁都被持有 在交易期间。

在 READ COMMITTED 中,与扫描不匹配的锁被释放 在 STATEMENT 完成后。

但是,找出为什么谓词锁定像在 REPEATABLE READ 上那样工作仍然很有趣。

【问题讨论】:

    标签: mysql concurrency transactions locking


    【解决方案1】:

    SELECT FOR UPDATE 锁定在 1 和员工表中的下一个值之间。由于没有下一个值,它一直锁定到supremum pseudo-record。这可以在information_schema.innodb_locks看到:

    mysql> select * from innodb_locks;
    +----------------+-------------+-----------+-----------+-------------------+------------+------------+-----------+----------+------------------------+
    | lock_id        | lock_trx_id | lock_mode | lock_type | lock_table        | lock_index | lock_space | lock_page | lock_rec | lock_data              |
    +----------------+-------------+-----------+-----------+-------------------+------------+------------+-----------+----------+------------------------+
    | 28275:1448:3:1 | 28275       | X         | RECORD    | `test`.`employee` | PRIMARY    |       1448 |         3 |        1 | supremum pseudo-record |
    | 28273:1448:3:1 | 28273       | X         | RECORD    | `test`.`employee` | PRIMARY    |       1448 |         3 |        1 | supremum pseudo-record |
    +----------------+-------------+-----------+-----------+-------------------+------------+------------+-----------+----------+------------------------+
    2 rows in set, 1 warning (0.00 sec)
    

    如果您稍微更改测试用例,让员工中有一行代表 dept-id=2,然后尝试添加一个代表 dept-id=3 的员工,它将起作用。示例:

    create table department (
        id bigint not null, 
        budget bigint not null, 
        name varchar(255), 
        primary key (id)
    ) ENGINE=InnoDB;
    
    create table employee (
        id bigint not null, 
        name varchar(255), 
        salary bigint not null, 
        department_id bigint, primary key (id)
    ) ENGINE=InnoDB;
    
    
    alter table employee 
    add constraint FK_department_id 
    foreign key (department_id) 
    references department (id);
    
    
    insert into department (name, budget, id) 
    values ('Hypersistence', 100000, 1);
    
    insert into department (name, budget, id) 
    values ('Bitsystem', 10000, 2);
    
    insert into department (name, budget, id) 
    values ('XX', 10000, 3);
    
    
    insert into employee (department_id, name, salary, id) 
    values (1, 'John Doe 0', 30000, 0);
    
    insert into employee (department_id, name, salary, id) 
    values (1, 'John Doe 1', 30000, 1);
    
    insert into employee (department_id, name, salary, id) 
    values (2, 'John Doe 2', 30000, 2);
    
    
    start transaction;
    
    SELECT * 
    FROM employee 
    WHERE department_id = 1 
    FOR UPDATE;
    
    
    # new session
    
    insert into employee (department_id, name, salary, id) 
    values (3, 'Dave', 9000, 5)
    

    【讨论】:

      【解决方案2】:

      InnoDB 只锁定正在使用的行,但是当 Alice 正在做的时候:

      SELECT SUM(salary) 
      FROM employee 
      where department_id = 1 
      

      我认为所有表都被锁定了,因为她正在使用她的事务中的所有表行(尽管事务只是读取表)。

      【讨论】:

      • 即使我删除了这个查询,并且 Alice 只是发出 FOR UPDATE 语句,也没有任何变化。 Bob 的两个插入语句都将超时。
      【解决方案3】:

      来自 MySQL manual(对于可重复读取隔离级别):

      • 对于具有唯一搜索条件的唯一索引,InnoDB 只锁定找到的索引记录,而不锁定它之前的间隙。

      • 对于其他搜索条件,InnoDB 锁定扫描的索引范围,使用间隙锁或 next-key 锁来阻止其他会话插入该范围覆盖的间隙(这是您的情况)。

      当您执行以下查询时:

      SELECT * 
      FROM employee 
      WHERE department_id = 1 
      FOR UPDATE
      

      where 子句条件检查确实依赖于非索引的department_id 列,因此应进行全表扫描以查找所有匹配行。在可重复读取隔离级别中,Innodb 锁定所有扫描的记录,直到事务回滚或提交,无论条件是否匹配该行,Gap Locks 也应用于聚集索引中每条记录之前和最后一条记录之后的间隙。因此,您将获得一个完全锁定的表,在当前事务执行 Commit 或 Rollback 之前,无法从任何其他事务中插入、更新或删除任何内容。

      我最近发布了一个question,您可能也会感兴趣。

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 2016-01-06
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2014-10-12
        • 2021-04-19
        相关资源
        最近更新 更多