有并发的时候,需要锁来控制资源的访问规则。
MySQL的锁分为全局锁、表级锁和行锁。
全局锁
对整个数据库实例进行加锁,加完锁后数据库处于只读状态,一般用于全量备份的场景。虽然毫无疑问会对业务有很大的影响。
全局锁的命令是: (FTWRL)
|
|
但是如果不加全局锁那么就会造成问题,比如依次备份表A和表B,刚好备份完表A的时候有个操作同时修改了两张表,然后备份表B,这就造成了两个表内容的不一致。
如果引擎支持事务(如InnoDB),那么导数据之前启动一个事务,拿到一致性视图,依靠MVCC,就可以在边导出的时候还能更新数据。 但如果使用了不支持事务的引擎(如MyISAM),就只能采用FTWRL。
表级锁
表级锁包括表锁和元数据锁。
表锁
|
|
元数据锁(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。由于两个事务都没有提交,所以锁都没释放,陷入死锁。
解决死锁的办法:
- 设置超时时间
- 死锁检测,如果检测到死锁就主动回滚某个事务,等其他事务先执行完。
InnoDB的超时时间默认是50s,过大会浪费时间,可能花费快一分钟陷入死锁,很多场景无法接受。过小则容易误伤。
更好的方式是死锁检测,innoDB本身也开启了死锁检测,但是有额外的CPU负担。但是如果为解决死锁而大量地进行死锁检测,如果对于某一行有非常庞大的并发线程,那么检测死锁的代价会很高。
有以下的解决思路:
-
在业务本身设计的时候规避死锁的问题,从而不需要死锁检测。当然这样比较困难。
-
控制并发度。
-
将一行的逻辑改为多行,从而减少锁冲突。
幻读
幻读指的是一个事务在前后两次查询同一个范围的时候,后一次查询看到了前一次查询没有看到的行,前一次读取的数据状态不能支撑后续的事务操作。
但不是所有两次读取不一样就叫幻读。举个具体的例子,比如select查询发现某个记录没有,然后插入这个记录的时候又发现该记录已存在,和幻觉一样。
在可重复读的隔离级别下,查询的读用的是快照,所以不能看到别的事务插入的数据,幻读在RU/RC/RR级别下都会出现,在SERIALIZABLE事务级别则不会出现。
假设表为:
id | c | d |
---|---|---|
0 | 0 | 0 |
5 | 5 | 5 |
10 | 10 | 10 |
事务A三次执行
|
|
看起来似乎没问题。
但是语义上,事务A在T1的时刻应该会有行锁将d=5的行都锁住,这样就会破坏这个语义。
从数据一致性角度来看,如果事务A在选择d=5的所有行之后对其数据有更改,比如A的T1改为:
|
|
但是A是最后才提交的,也就是binlog记录的这个修改时最后才执行的,那么id=0,1,5的d字段都会变成100,从而造成了数据不一致。
如何解决幻读?
如果把所有的行都加行锁,这样事务B正常,但是事务C的插入还是导致幻读无法避免。因为行锁只能锁住行,但是插入记录更新的是记录之间的间隙,所以InnoDB引入间隙锁,执行:
|
|
的时候,会给数据库的所有记录都加行锁,然后给字段d存在的4个间隙都加间隙锁,间隙锁会阻塞在间隙中插入新的记录的操作。
间隙锁和行锁合称为next-key lock,比如上面的例子就是:$(-\infin, 0], (0, 5], (5, 10], (10, +\infin)$
不过并发事务的多个间隙锁之间可能会导致死锁