假设我们有以下数据库表:
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上一样。
答案 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,这对您也可能很有趣。