MySQL---全局锁、表级锁和行锁

全局锁

全局锁是对整个数据库实例加锁,当需要让整个数据库处于只读(read only)状态的时候,就可以使用全局锁,之后其他线程的以下语句会被阻塞:数据更新语句(数据的增删改)、数据定义语句(包括建表、修改表结构等)和更新类事务的提交语句,全局锁操作命令是Flush tables with read lock(FTWRL)。

全局锁的典型使用场景是做全库逻辑备份,也就是把整库每个表都select出来存成文本。

但是通过全局锁做全局备份会有一个问题:

  • 如果在主库上备份,那么备份期间都不能执行更新,业务基本上停滞了。
  • 如果在从库上备份,那么备份期间从库不能执行主库同步过来的binlog,会导致主从延迟。

但是如果不加锁,也会出现问题。假如有以下业务:

  1. 备份user余额表
  2. user进行购买商品
  3. 备份user资产表

此时会发生user的余额没扣,但是却购买了商品。相比主从延迟,这个问题更加严重。

MySQL提供一个mysqldump逻辑备份工具。当mysqldump使用参数–single-transaction的时候,导数据之前就会启动一个事务,来确保拿到一致性视图。相比较执行FTWRL,mysqldump看起来是更好的选择,但是前提是要有事务的支持,我们都知道MyISAM不支持事务,所以就需要使用FTWRL。

全库只读还有一种方法,就是设置全局只读,命令set global readonly=true,表面看上去与FTWRL没什么区别,但是实际上还是有区别:

  1. 有的系统readonly的值主要是被用来做其他逻辑,比如用来判断一个库是主库还是备库。因此修改global变量的方式影响面更大。
  2. 执行FTWRL命令之后如果客户端发生异常断开,MySQL会自动释放这个全局锁,整个库回到可以正常更新的状态。而将整个库设置为readonly之后,如果客户端发生异常,则数据库就会一直保持readonly状态,导致整个库长时间处于不可写状态。

相比设置全库只读,FTWRL是更好的选择。

表级锁

MySQL表级别的锁有两种:一种是表锁,一种是元数据锁(meta data lock,简称MDL)。

表锁

表锁的语法是 lock tables …read/write。与FTWRL类似,可以用unlock tables主动释放锁,也可以在客户端断开的时候自动释放。lock tables语法除了会限制别的线程的读写外,也限定了本线程接下来的操作对象。

比如在线程Thread-01中执行lock tables t1 read, t2 write; ,则其他线程写t1、读写t2的语句都会被阻塞。同时,线程Thread-01在执行unlock tables之前,也只能执行读t1、读写t2的操作。

在没有出现更细粒度的锁的时候,表锁是最常用的处理并发的方式。InnoDB默认的是行级锁,一般不使用lock tables命令来控制并发,因为锁住整个表的影响面比较大。但是行级锁都是基于索引的,如果一条 SQL 语句用不到索引是不会使用行级锁的,而会使用表级锁把整张表锁住。

元数据锁

MDL不需要显式使用,在访问一个表的时候会被自动加上。MDL的作用是为了保证读写的正确性。

MySQL 5.5版本中引入了MDL,当对一个表做增删改查操作的时候,加MDL读锁;当要对表做结构变更操作的时候,加MDL写锁。

读锁之间不互斥,因此可以有多个线程同时对一张表增删改查。读写锁之间、写锁之间是互斥的,用来保证变更表结构操作的安全性。因此,如果有两个线程要同时给一个表加字段,其中一个要等另一个执行完才能开始执行(有一点ReentrantReadWriteLock和ReentrantReadWriteLock味道了)。

MDL阻塞问题

对一个表添加字段、修改字段或者加索引,需要扫描全表的数据。

假如有以下顺序操作:

  1. sessionA执行select * from person limit 1;
  2. sessionB执行select * from person limit 1;
  3. sessionC执行alter table person add age int
  4. sessionD执行select * from person limit 1;

sessionA拿到MDL读锁正常执行,紧接着sessionB也是可以执行的,因为读锁之间不互斥。sessionC需要MDL写锁,因为session A的MDL读锁还没有释放,读写之间互斥,所以sessionC被堵塞了,这就导致了sessionD也被堵塞了,紧接着之后其它的session都会被堵塞。

如果客户端有重试机制同时请求频繁的话,这就会导致线程很快就会爆满。

解决方法

问题的关键是长事务占用MDL锁不释放导致堵塞,可以在MySQL的information_schema库的 innodb_trx表中,查到当前执行中的事务。如果你要做DDL变更的表刚好有长事务在执行,可以考虑先暂停DDL或者kill掉这个长事务。

对于频繁请求的这时候kill可能未必管用,因为新的请求马上就来了,杀不完啊!!!此时可以在alter table语句里面设定等待时间,如果指定的等待时间里面能够拿到MDL写锁最好,拿不到的话会放弃,避免阻塞后面的语句(ReentrantLock 里的tryLock方法也可以设置等待时间,有内味了)。之后可以通过重试命令重复这个过程。

MariaDB已经合并了AliSQL的这个功能,这两个开源分支目前都支持DDL NOWAIT/WAITn这个语法。

ALTER TABLE tbl_name NOWAIT add column ...
ALTER TABLE tbl_name WAIT N add column ...

行锁

由于表锁锁住的粒度较大,同一张表上任何时刻只能有一个更新在执行,这就会影响到业务并发度。而行锁是一种粒度更小的锁,这就大大提高了业务的并发度。MySQL的行锁是在引擎层由各个引擎自己实现的,并不是所有的引擎都支持行锁。MyISAM引擎不支持行锁,而InnoDB支持行锁,这也是MyISAM被InnoDB替代的重要原因之一。

在InnoDB事务中,行锁是在需要的时候才加上的,并不是不需要了就立刻释放,而是要等到事务结束时才释放。这个就是两阶段锁协议

有了这个协议,如果事务中需要锁多个行,要把最可能造成锁冲突、最可能影响并发度的锁尽量往后放,这样可以最大程度地减少了事务之间的锁等待,提升并发度。

死锁

有人的地方就有江湖,有锁的地方就有死锁!!!

假如有事务A和事务B,两者执行的顺序正好相反,事务A在等待事务B释放目标行的行锁,而事务B在等待事务A释放目标行的行锁。 此时事务A和事务B在互相等待对方的资源释放,进入了死锁状态。

当出现死锁以后,有两种策略:

  1. 通过参数innodb_lock_wait_timeout来设置超时时间,直接进入等待,直到超时。
  2. 通过参数innodb_deadlock_detect设置为on,开启死锁检测,发现死锁后,主动回滚死锁链条中的某一个事务,让其他事务得以继续执行。

策略1有一个弊端是innodb_lock_wait_timeout的默认时间是50s,超时时间过久。但是设置的值过小,会导致不是死锁而是普通的锁等待误退出。

看样子策略2是个更好的解决的方法,但是策略2也有一个弊端,每当一个事务被锁的时候,就要查看它所依赖的线程有没有被别的锁住,如此循环最后判断是否出现了循环等待,也就是死锁。此时需要控制并发度,在服务端通过中间件减少同时更新的并发。

猜你喜欢

转载自blog.csdn.net/MAKEJAVAMAN/article/details/118466245