为什么MySQL显式谓词锁定不允许在谓词锁

时间:2017-02-18 15:08:18

标签: mysql concurrency transactions locking

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

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无法使用相同的employee插入新的department_id

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尝试在另一个employee中插入department,也会抛出相同的异常:

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

这最后一个insert语句使用的是第二个department_id,因此该行不应与我们获取谓词锁的select语句重叠。

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

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

更新

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

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

  

在REPEATABLE READ中,保持在交易期间获得的每个锁   在交易期间。

     

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

然而,现在仍然无法找到谓词锁定的原因,就像在REPEATABLE READ上一样。

3 个答案:

答案 0 :(得分:3)

SELECT FOR UPDATE锁定在1和employee表中的下一个值之间。由于没有下一个值,因此它会锁定到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)

答案 1 :(得分:0)

InnoDB仅锁定正在使用的行,但是当Alice正在执行时:

SELECT SUM(salary) 
FROM employee 
where department_id = 1 

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

答案 2 :(得分:0)

从MySQL manual(用于可重复读取隔离级别):

  
      
  • 对于具有唯一搜索条件的唯一索引,InnoDB仅锁定找到的索引记录,而不锁定其前的空白。

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

  •   

执行以下查询时:

SELECT * 
FROM employee 
WHERE department_id = 1 
FOR UPDATE

where子句条件检查确实取决于未索引的department_id列,因此应进行全表扫描以查找所有匹配的行。在“可重复读取”隔离级别中,Innodb锁定所有扫描的记录,直到事务回滚或提交为止,无论条件是否与行匹配都无关紧要,并且“间隙锁定”也会应用于“聚集索引”中每个记录之前和最后一个记录之后的间隔。因此,您将获得一个完全锁定的表,在该表中没有任何东西可以插入,更新或从任何其他事务中删除,直到当前事务执行Commit或Rollback。

我最近发布了一个question,这对您也可能很有趣。