根据加锁的范围,MySQL 里面的锁大致可以分成全局锁、表级锁和行锁三类
全局锁
全局锁,顾名思义就是将整个MySQL示例上锁。MySQL里有个Flush tables with read lock(FTWRL),当加了全局锁后,对表以及表中行的增删改等语句都会被阻塞。一般的应用场景常见于没有行锁的表备份中,通过全局锁实现对数据库一致性备份。
如果不加全局锁进行备份,假如一个业务涉及两个表,可能会造成备份数据中一个表更新了一个表没更新,导致数据不一致的情况。
全局锁的弊端:
- 如果在主库上加锁,会导致后续的增删改语句被阻塞,导致业务无法进行。
- 如果在从库上加锁,导致bin log无法更新,从而造成主从数据不一致。
在支持事务隔离的引擎(如InnoDB)中,我们可以使用官方工具mysqldump,当我们使用-single-transaction 这个参数时,会创建一个一致性视图,随后的更新都在这个视图进行。而且由于MVCC的支持,不会对后续的语句造成阻塞。
另外,还有个参数是 set global readonly = true,但是这种做法用来作备份不是很合适。因为这个参数有时会被用来作为主从库使用的,这样做无疑是修改了属性。其次如果期间数据库发生异常,使用全局锁可以解锁,而这种方式会让整个数据库处于只读状态。
表级锁
表级锁有两种:表锁和元数据锁(metadata lock, MDL)。
表锁
表锁由语句 lock tables ... read/write 创建,和全局锁类似,表锁可以用 unlock tabls解锁,也可以在数据库断开时自动解锁。
需要注意的是 read lock 是共享锁,也就是说一张表中可以有多个read lock且互不影响。而write lock是互斥锁,也就是说表中有且仅有一个write lock,当表中还有其他锁时,后创建的锁会被阻塞。
在InnoDB之前,表锁是最常用的解决并发数据一致性问题的加锁方式。
元数据锁
元数据锁(metadata lock, MDL)是MySIAM默认的锁(这个锁是MySQL 5.5 版本中引入的,是server层的锁),是针对于 DDL(数据定义语句:create 、 alter、 drop) 与 DML(数据操纵语句:update、delte、insert)、DQL(数据查询语句:select) 操作加锁,执行 DDL 自动添加写锁,执行 DML、DQL 自动添加读锁,也就是说 DML 语句可以同时执行(不考虑其他锁),而 DDL 间则会相互阻塞。元数据锁的出现是为了防止对表的更新造成数据不一致的问题。
另外,MDL的解锁并不是语句执行完毕才进行解锁的,而是当事务commit才进行解锁。如果一张表中有了读锁,后面有需要对这张表上写锁,就会造成后续对该表做的所有操作都会被阻塞。如果你需要对这张表进行频繁读取操作,那么必然会导致效率断崖式下跌。如图所示:
图中sessionA和sessionB都是读锁,因此可以正常执行。但sessionC的操作是MDL语句,需要申请写锁。由于之前申请了读锁且没有释放,必然会导致sessionC被阻塞,随后sessionD因为sessionC的阻塞而阻塞。
所以,现在就引发一个问题:如何安全地给小表加字段?
-
解决长事务问题:事务不提交,就会一直占着 MDL 锁。可以先通过MySQL 的 information_schema 库的 innodb_trx 表中查到当前执行中的事务,如果你要做的DDL所在表刚好有长事务要执行,那么要考虑暂停DLL,或者kill掉该长事务。
-
如果这个表是个热点表,那么kill掉事务未必有用。一个解决办法是:在alter table中设定等待时间,如果在等待时间内拿到写锁最好,拿不到就先放弃,以免影响后面的事务进行。DBA或者开发人员重复以上步骤直到拿到锁。
行锁
行锁是InnoDB支持的更小力度的锁,当对表进行数据更新查询是就会对操作的行加锁。对比表锁来说,行锁的出现提高了并发度。所以这是InnoDB为什么逐渐提到MySIAM的重要原因。
当然行锁使用不当也会造成效率的降低,下面先从两阶段锁协议说起:
两阶段锁协议
对行的上锁的时机是产生对行的更新查询语句的时候,执行完语句不会立即解锁,当整个事务commit时,才会进行解锁。
两阶段锁协议解决了数据的一致性问题,可以通过事务隔离及MVCC这篇文章进行深入了解。
所以针对两阶段锁协议,我们的SQL语句执行循序应该要注意:如果你的事务中需要锁多个行,要把最可能造成锁冲突、最可能影响并发度的锁尽量往后放。从而使上锁时间尽可能少,提高并发度。
比如这个例子:假设我要到电影院买电影票,简化后的操作如下:
- 从顾客 A 账户余额中扣除电影票价;
- 给影院 B 的账户余额增加这张电影票价;
- 记录一条交易日志。
也就是说,要完成这个交易,我们需要 update 两条记录,并 insert 一条记录。由于我们已知两阶段锁协议,现在我们需要以怎样的顺序,才能尽量避免锁的影响呢?
我们知道对影院账户余额操作应该是最频繁的,所以最好的顺序是把2放最后,即312,这样影院账户余额这一行的锁时间就最少,减少了事务之间锁等待,提高并发。
死锁
死锁定义是:当出现循环资源依赖,为获取某一资源而互相等待的现象。
死锁的产生
比如说拿行锁举例:
一开始,事务A对id=1的行上了写锁,事务B对id=2的行上写锁。然后事务A也要对id=2的行进行更新,由于事务B有了id=2的写锁,造成事务A阻塞。然后事务B也要对id=1的行进行更新,由于事务A有了id=1的写锁,造成事务B阻塞。这样一来,就造成资源相互依赖,为获取相互依赖的资源造成无限循环等待的现象。
解决方法
-
有一个参数 innodb_deadlock_detect,默认是开启的,可以进行死锁检测。
-
好处是避免了死锁
-
坏处是由于死锁检测是O(n)级别的,如果有1000个线程,就要发起1000000次死锁检测,大大降低并发度。
-
-
有一个参数innodb_lock_wait_timeout,用来设置锁等待时间,我们可以将该项参数的值调小。
-
好处是一定程度上减少了死锁的时间
-
坏处是可能会误伤一些正常的锁,如果只是进行正常的锁等待,因为锁等待时间设置过小可能造成超时失败的现象。
-
怎么解决由热点行更新导致的性能问题呢?问题在于解决死锁检测带来的效率损耗。
-
如果确定事务间不会造成死锁,那就可以临时关闭死锁检测。但是这种做法不是很保险。如果进行死锁检测,出现死锁就回滚;关掉死锁检测意味着可能会出现大量的超时,这是业务有损的。
-
控制并发度。如果同一行同时最多只有 10 个线程在更新,那么死锁检测的成本很低,就不会出现这个问题。但是实际业务会出现很多客户端,如果一个客户端5个线程,会发现这种思路不可行。
-
可以将行拆分成多个行分开存储。比如说电影院票价增加余额,可以每次更新数据是随机一个行进行存储,这样可以降低锁冲突的概率,但是需要考虑如果有退票的情况,就要考虑当某行变为0时应该怎么处理了。
参考资料
-
极客时间 —— MySQL45讲:全局锁和表锁:给表加个字段怎么有这么多阻碍?
-
极客时间 —— MySQL45讲:行锁功过:怎么减少行锁对性能的影响?
还没发表评论,快来发表第一个评论吧~