MySQL/MariaDB的锁超详细讲解(5)

SELECT
    trx_id,
    trx_state,
    lock_id,
    lock_mode,
    lock_type,
    lock_table,
    trx_mysql_thread_id,
    trx_query
FROM
    information_schema.innodb_trx t
JOIN information_schema.innodb_locks l ON l.lock_trx_id = t.trx_id;

MySQL/MariaDB的锁超详细讲解

4.2 innodb表的外键和锁

在innodb表中,创建外键的时候若外键列上没有索引,则会在创建过程中自动在外键列上隐式地创建索引。

存在这样一种情况,当向子表中插入数据的时候,会向父表查询该表中是否存在对应的值以判断将要插入的记录是否满足外键约束,也就是说会对父表中对应的记录加上依赖性的共享锁,并在表上加意向共享锁。如果此时父表上对应的记录正好有独占锁,那么插入就会失败。同理,从子表中删除或更新记录也是一样的。

现在创建父表parent和子表child,并不要在外键列(pid)上显式创建索引。

create table parent(pid int primary key);
create table child(cid int primary key,pid int,foreign key(pid) references parent(pid));
show create table child\G
*************************** 1. row ***************************
      Table: child
Create Table: CREATE TABLE `child` (
  `cid` int(11) NOT NULL,
  `pid` int(11) DEFAULT NULL,
  PRIMARY KEY (`cid`),
  KEY `pid` (`pid`),
  CONSTRAINT `child_ibfk_1` FOREIGN KEY (`pid`) REFERENCES `parent` (`pid`)
) ENGINE=InnoDB DEFAULT CHARSET=latin1

从show的结果中可以发现,已经自动添加了索引列pid。

插入一些测试记录。

insert into parent values(1),(2),(3);

在会话1中执行:

begin;
delete from parent where pid=3;

在会话2中执行:

begin;
insert into child select 3,3;

这时会发现会话2被阻塞了。通过innodb_trx和innodb_locks表的联合,得到如下结果:

SELECT
    trx_id,
    trx_state,
    lock_id,
    lock_mode,
    lock_type,
    lock_table,
    trx_mysql_thread_id,
    trx_query
FROM
    information_schema.innodb_trx t
JOIN information_schema.innodb_locks l ON l.lock_trx_id = t.trx_id\G
*************************** 1. row ***************************
            trx_id: 14951
          trx_state: LOCK WAIT
            lock_id: 14951:185:3:4
          lock_mode: S
          lock_type: RECORD
        lock_table: `test`.`parent`
trx_mysql_thread_id: 34
          trx_query: insert into child select 3,3
*************************** 2. row ***************************
            trx_id: 14946
          trx_state: RUNNING
            lock_id: 14946:185:3:4
          lock_mode: X
          lock_type: RECORD
        lock_table: `test`.`parent`
trx_mysql_thread_id: 1
          trx_query: NULL

不难看出,insert语句想要在父表parent上的资源"14951:185:3:4"加共享锁,但是此时父表上该资源已经有了独占锁,所以被阻塞了。

并且也可以判断出,通过外键读取父表时的模式是lock in share mode,而不是基于快照的行版本读(什么是lock in share mode和行版本快照读见事务隔离级别内容),假如是基于行版本的快照读,那么就可以查出存在pid=3的记录而导致子表插入成功,这样也可能导致父表和子表不满足外键约束。

4.3 innodb锁算法

innodb支持行级锁,但是它还支持范围锁。即对范围内的行记录加行锁。

有三种锁算法:
•1.record lock:即行锁
•2.gap lock:范围锁,但是不锁定行记录本身
•3.next-key lock:范围锁加行锁,即范围锁并锁定记录本身,gap lock + record lock。

record lock是行锁,但是它的行锁锁定的是key,即基于唯一性索引键列来锁定(SQL Server还有基于堆表的rid类型行锁)。如果没有唯一性索引键列,则会自动在隐式列上创建索引并完成锁定。

next-key lock是行锁和范围锁的结合,innodb对行的锁申请默认都是这种算法。如果有索引,则只锁定指定范围内的索引键值,如果没有索引,则自动创建索引并对整个表进行范围锁定。之所以锁定了表还称为范围锁定,是因为它实际上锁的不是表,而是把所有可能的区间都锁定了,从主键值的负无穷到正无穷的所有区间都锁定,等价于锁定了表。

以下示例过程将演示范围锁的情况。

1.有索引的情况

首先创建一个有索引的表t。然后插入几个被分隔的记录。

create table t(id int);
create unique index idx_t on t(id);
insert into t values(1),(2),(3),(4),(7),(8),(12),(15);

在会话1执行:无需知道lock in share mode是什么意思,只需知道它的作用是在读取的时候加上共享锁并且不释放,具体内容在事务章节中会说明。

begin;
select * from t where id<5 lock in share mode;

在会话2执行:

insert into t values(9);
insert into t values(6);

这时发现第一条插入语句是正常插入的,而第二条语句被阻塞。 show engine innodb status 看结果。

内容版权声明:除非注明,否则皆为本站原创文章。

转载注明出处:https://www.heiqu.com/3810f55420a83dd159a316202f796b19.html