MySQL进阶(五)锁

有并发的时候,需要锁来控制资源的访问规则。

MySQL的锁分为全局锁、表级锁和行锁。

全局锁

对整个数据库实例进行加锁,加完锁后数据库处于只读状态,一般用于全量备份的场景。虽然毫无疑问会对业务有很大的影响。

全局锁的命令是: (FTWRL)

1
Flush tables with read lock

但是如果不加全局锁那么就会造成问题,比如依次备份表A和表B,刚好备份完表A的时候有个操作同时修改了两张表,然后备份表B,这就造成了两个表内容的不一致。

如果引擎支持事务(如InnoDB),那么导数据之前启动一个事务,拿到一致性视图,依靠MVCC,就可以在边导出的时候还能更新数据。 但如果使用了不支持事务的引擎(如MyISAM),就只能采用FTWRL。

表级锁

表级锁包括表锁和元数据锁。

表锁

1
lock tables  read/write

元数据锁(MDL)访问一个表的时候会自动加上。

增删改查的时候加MDL读锁,对表结构进行变更的时候加MDL写锁。 读锁之间不互斥,但是读写和写写之间互斥,需要串行执行,否则会出现数据不一致。

如何安全地给表加字段

事务需要提交才会释放锁,试想这样一个场景,某个表经常被频繁访问,事务A发起查询,加MDL读锁,事务B增加一个字段,但是被阻塞,因为读锁没释放,之后又来了很多事务发起查询,都被阻塞了。

如何解决? 可以先查询长事务,在information_schema 库的 innodb_trx 表中查找长事务,可以先kill掉长事务。

如果访问过于频繁,可以在alter table的设置等待时间,如果在一段时间内还抢不到MDL写锁,就自动放弃。以免阻塞后面的事务。

行锁

行锁由具体的引擎自行实现,MyISAM就不支持航说,InnoDB支持。

行锁的粒度比较细,只有当两个事务都同时更新某一行的时候才会生效,并且在事务提交之后才会释放,其他需要操作同一行数据的事务会被阻塞。

如果事务需要锁多个行,把最可能造成锁冲突的尽量放后面。

比如先来的事务A操作一系列其他的表和表C的m行,后来的事务B也操作其他的表和表C的m行,在表C的m行会触发行锁,最好的方法是事务A最后才更改表C的m行,然后直接提交释放锁,因为这样的话事务A持有锁的时间会短很多,提高并发效率。

死锁和死锁检测

不同线程陷入循环资源依赖,比如事务A先访问某表的行m,加行锁,事务B访问行n,此时事务A要访问行n,事务B要访问行m。由于两个事务都没有提交,所以锁都没释放,陷入死锁。

解决死锁的办法:

  1. 设置超时时间
  2. 死锁检测,如果检测到死锁就主动回滚某个事务,等其他事务先执行完。

InnoDB的超时时间默认是50s,过大会浪费时间,可能花费快一分钟陷入死锁,很多场景无法接受。过小则容易误伤。

更好的方式是死锁检测,innoDB本身也开启了死锁检测,但是有额外的CPU负担。但是如果为解决死锁而大量地进行死锁检测,如果对于某一行有非常庞大的并发线程,那么检测死锁的代价会很高。

有以下的解决思路:

  1. 在业务本身设计的时候规避死锁的问题,从而不需要死锁检测。当然这样比较困难。

  2. 控制并发度。

  3. 将一行的逻辑改为多行,从而减少锁冲突。

幻读

幻读指的是一个事务在前后两次查询同一个范围的时候,后一次查询看到了前一次查询没有看到的行,前一次读取的数据状态不能支撑后续的事务操作。

但不是所有两次读取不一样就叫幻读。举个具体的例子,比如select查询发现某个记录没有,然后插入这个记录的时候又发现该记录已存在,和幻觉一样。

在可重复读的隔离级别下,查询的读用的是快照,所以不能看到别的事务插入的数据,幻读在RU/RC/RR级别下都会出现,在SERIALIZABLE事务级别则不会出现。

假设表为:

id c d
0 0 0
5 5 5
10 10 10

事务A三次执行

1
select * from t where d = 5 for update;

看起来似乎没问题。

但是语义上,事务A在T1的时刻应该会有行锁将d=5的行都锁住,这样就会破坏这个语义。

从数据一致性角度来看,如果事务A在选择d=5的所有行之后对其数据有更改,比如A的T1改为:

1
2
select * from t where d = 5 for update;
update t set d = 100 where d = 5;

但是A是最后才提交的,也就是binlog记录的这个修改时最后才执行的,那么id=0,1,5的d字段都会变成100,从而造成了数据不一致。

如何解决幻读?

如果把所有的行都加行锁,这样事务B正常,但是事务C的插入还是导致幻读无法避免。因为行锁只能锁住行,但是插入记录更新的是记录之间的间隙,所以InnoDB引入间隙锁,执行:

1
select * from t where d = 5 for update;

的时候,会给数据库的所有记录都加行锁,然后给字段d存在的4个间隙都加间隙锁,间隙锁会阻塞在间隙中插入新的记录的操作。

间隙锁和行锁合称为next-key lock,比如上面的例子就是:$(-\infin, 0], (0, 5], (5, 10], (10, +\infin)$

不过并发事务的多个间隙锁之间可能会导致死锁