vlambda博客
学习文章列表

MySQL之数据库中的行锁与表锁概念

事务并发带来的问题

在并发的情况下,多个事务访问相同记录的情况分为下面三种:

  1. 读-读:多个事务同时读取相同的数据

  2. 写-写:多个事务同时对相同的记录进行写入

  3. 读-写或写-读:部分事务读部分事务写

第一种情况由于并不会对记录产生影响,因此是允许发生的,后两种因为对记录做出修改,会对运行中的其他事务造成影响,因此要采用一些方式避免。

针对写-写的处理

多个事务同时对一条记录做出修改时,会造成脏写的情况,任何一种事务隔离级别都不允许出现这种情况,这种情况采用加锁处理,

多个未提交事务修改同一条记录时,需要让它们排队执行,排队过程通过为该记录加锁实现,锁是一个与记录关联的内存结构,默认一条记录是没有关联的锁结构的,

当一个事务想对记录进行改动时,会先查看该记录是否有关联的锁结构,如果没有则生成一个锁结构与之关联:

锁结构中有两个比较重要的信息:

  1. trx信息:表示锁结构与哪个事务关联

  2. is_waiting:表示事务是否在等待,false代表事务正在运行,true代表在等待

假设事务T1,想对一条记录做出修改,此时发现这条记录没有对应的锁结构,于是创建一个锁结构信息与之关联,is_waiting为false,进一步对记录做出修改,

此时T2也想对该记录做出修改,于是来查看该记录有没有锁结构,发现有的,于是也创建一个锁结构,不过is_waiting为true,随之自己陷入等待:

当T1提交事务后,将自己的锁结构释放掉,并且检查该记录有没有其他的锁结构关联,发现有一个T2,于是将T2的is_waiting设置为false,并将其唤醒,让T2进入执行:

针对写-读或读-写处理

在写读或读写情况下,并发事务会出现脏读、不可重复读、幻读的现象,通过设置相应的隔离级别可以避免这些问题,避免脏读、不可重复读、幻读有如下两种方案:

1. MVCC多版本并发控制

通过MVCC的ReadView,查询语句只能查询到ReadView生成之前已提前事务做出的修改,生成ReadView之后的事务所做的修改提交都是看不到的,

而写操作针对的都是最新的记录版本,因此读历史版本(undo日志)和改动最新记录版本并不会发生冲突。

2. 读/写加锁

不管是读操作还是写操作,都按照上面说的方式加锁进行排队执行,因为都是挨个执行的,上一个事务提交后,这个事务执行,读写肯定都是最新版本,所以不会发生脏读等。

因此,采用MVCC方式,读写操作不会冲突,这种性能是较高的,当时如果加锁的话,就会严重影响性能,一般情况下都会采用MVCC的方式。

何时加锁?

一致性读

通过MVCC进行的读取操作称为一致性读(Consistent Read),或者一致性无锁读(也可称为快照读),所有普通的select语句,在读已提交或可重复读的隔离级别下都是一致性读,例如:

select * from t1;
select * from t1 inner join t2 on t1.id = t2.id

一致性读不会对表中的记录进行加锁,其他事务可以自由对记录作出改动。

锁定读

1. 共享锁和独占锁

使用MVCC可以解决事务并发读写的问题,但是有些情况下我们需要使用加锁来做出一些特殊的处理,我们需要让读读之间互不影响,

但是需要写-写、读-写、写-读这几种情况中相互阻塞,针对这些情况分成下面两个锁:

  1. 共享锁(Shared Lock):简称S锁,在事务要读取一条记录时,获取该记录的S锁

  2. 独占锁(Exclusive Lock):简称X锁,也可称为排他锁,在事务要对一条记录改动时,获取该记录的X锁

S锁之间是兼容的,即多个S锁可以一起执行,S锁和X锁是不兼容的,二者必须有一个要进行排队等待。

例如:

  1. T1获取了A记录的S锁

  2. T2想获取A记录的X锁,因为该记录已经有S锁,因此T2陷入等待,等S锁释放才能拿到X锁

上面的情况反过来先X,再S也是一样。

锁定读的写法

在读取记录前为该记录加上S锁或者X锁的方式,被称为锁定读。

S锁的写法

select ... lock in share mode;

使用这种语法,会为读取到的记录加上S锁,之后其他事务依然可以获取这些记录的S锁,但是如果有人想获取X锁,就必须等S锁都释放掉。

X锁的写法

select ... for update;

使用这种语法,会为读取到的记录加上X锁,之后其他事务不可以获取这些记录的S锁或X锁,需要等待本次事务的X锁释放才能获取到。

写操作对应的锁定读

delete、update、insert三种都是写操作,写操作是必须要获取到X锁之后才可以进行的,下面是三种操作获取锁的过程(可以将获取X锁的过程视为获取X锁的锁定读):

  1. delete:先在B+树中定位到该记录的位置,然后获取该记录的X锁,最后执行delete mark操作,

  2. update:update比较特殊需要分为三种情况

    • 如果修改的列前后大小没有变化,则在B+树中定位到该记录的位置,然后获取记录的X锁,最后在原记录位置进行修改操作

    • 修改的列发生大小变化,则在B+树中定位到该记录的位置,然后获取记录的X锁,将该记录移入垃圾链表,最后插入一条新记录,之后锁结构移到新记录上来

    • 如果修改了主键值,则是将原本的记录删除再insert,加锁操作按照delete和insert来即可

  3. insert:一般情况下,insert操作受到隐式锁保护,不生成锁结构。

多粒度锁

除了给记录加锁,也可以给表加锁,记录锁也被称为行级锁或者行锁,对一个记录加锁只会影响到这条记录,对一个表加锁,被称为表级锁或者表锁,会影响表中所有的记录,前者的粒度比较细,后者的粒度比较粗。

表级S锁

表级S锁与行级类似,只是他的目标是整张表,而不是一些行。

  1. 别的事务可以继续获取该表S锁

  2. 别的事务可以继续获取该表某些记录的S锁

  3. 别的事务不可以继续获取该表X锁

  4. 别的事务不可以继续获取该表某些记录的X锁

表级X锁

表级X锁与行级类似,只是他的目标是整张表,而不是一些行。

  1. 别的事务不可以继续获取该表S锁

  2. 别的事务不可以继续获取该表某些记录的S锁

  3. 别的事务不可以继续获取该表X锁

  4. 别的事务不可以继续获取该表某些记录的X锁

意向锁

表级S锁或X锁存在两个问题,如果获取表级S锁时,表中的某些记录被其他记录获取了X锁怎么办,如果获取表级X锁时,表中的某些记录被其他记录获取了S锁怎么办,

因为S和X是互斥的,虽然一个是表一个是行,但也不许有这种情况发生,但是如何知道获取表级锁时,表中的记录有没有被别事务正在占用的锁呢?不可能遍历表中的所有数据,那样太慢了,因此产生了意向锁的概念。

意向共享锁(IS锁)

Intention Shared Lock,当事务在某条记录上添加S锁时,需要先在表上添加IS锁。

意向独占锁(IX锁)

Intention Exclusive Lock,当事务在某条记录上添加X锁时,需要先在表上添加IX锁。

有了意向锁后,每次获取相应的表级锁时,仅需判断表上面有没有冲突的意向锁即可,例如加表级X锁时,看看表上有没有IS锁和IX锁,加表级S锁时,看看表上有没有IX锁。

注意:锁记录在添加IS锁时不关心表上有没有IX锁,添加IX锁时也并不关心表上有没有IS锁,因为他们仅仅是对表标记这个表中的记录正在被上锁,IS、IX锁仅对表级加锁有用,对行级没用,行级最终判断自己要去获取的记录有没有被其他事务占用即可。

表级别锁的兼容性:

兼容性 X IX S IS
X 不兼容 不兼容 不兼容 不兼容
IX 不兼容 兼容 不兼容 兼容
S 不兼容 不兼容 兼容 兼容
IS 不兼容 兼容 兼容 兼容