1. 前言
MySQL中的锁按照分类标准不同,有多种锁定义。

2. 按粒度分类
按照粒度不同,MySQL数据库有全局锁、表级锁和行级锁。
2.1 全局锁
全局锁就是对整个数据库实例加锁。MySQL提供了一个加全局读锁的方法,命令是Flush tables with read lock(FTWRL)。当你需要让整个库处于只读状态的时候,可以使用这个命令,之后其他线程的以下语句会被阻塞:数据更新语句(数据的增删改)、数据定义语句(包括建表、修改表结构等)和更新类事务的提交语句。
典型使用场景
全局锁的典型使用场景是做全库逻辑备份
不使用全局锁做全库逻辑备份会出现什么问题?
不加锁的话,备份系统备份的得到的库不是一个逻辑时间点,这个视图是逻辑不一致的。
使用事务实现备份
可重复读隔离级别下使用事务做全局备份
官方自带的逻辑备份工具是mysqldump。当mysqldump使用参数–single-transaction的时候,备份数据之前就会启动一个事务,来确保拿到一致性视图。而由于MVCC的支持,这个过程中数据是可以正常更新的。
有事务支持为什么还要FTWRL?
一致性读是好,但前提是引擎要支持事务。
为什么不使用set global readonly=true 方式做全库只读?
- 在有些系统中, readonly的值会被用来做其他逻辑, 比如用来判断一个库是主库还是备库。
- 出现异常时的处理情况不同。FTWRL会释放全局锁,当readonly会一直保持只读状态。
**总结:全局锁主要用在逻辑备份过程中。 对于全部是InnoDB引擎的库, 我建议你选择使用–singletransaction参数, 对应用会更友好。 **
2.2 表级锁
MySQL里面表级别的锁有两种:一种是表锁,一种是元数据锁(meta data lock,MDL)。
表锁
表锁的语法是 lock tables …read/write。 与FTWRL类似, 可以用unlock tables主动释放锁,也可以在客户端断开的时候自动释放。 需要注意, lock tables语法除了会限制别的线程的读写外, 也限定了本线程接下来的操作对象。
举个例子:
如果在某个线程A中执行lock tables t1 read, t2 write; 这个语句,则其他线程写t1、读写t2的语句都会被阻塞。同时,线程A在执行unlock tables之前,也只能执行读t1、读写t2的操作。连写t1都不允许,自然也不能访问其他表。
在没出现更细粒度的锁之前,表锁是最常用的处理并发的方式,但像InnoDB这种支持行锁的引擎一般不使用表锁。
元数据锁
元数据锁不需要显式使用, 在访问一个表的时候会被自动加上。 MDL的作用是, 保证读写的正确性,不会因为变更表结构和操作表数据同时进行而出现错误。
在MySQL 5.5版本中引入了MDL, 当对一个表做增删改查操作的时候, 加MDL读锁; 当要对表做结构变更操作的时候, 加MDL写锁。
- 读锁之间不互斥, 因此你可以有多个线程同时对一张表增删改查(只是不能改表,但是数据操作正常)
- 读写锁之间、 写锁之间是互斥的, 用来保证变更表结构操作的安全性。 因此, 如果有两个线程要同时给一个表加字段, 其中一个要等另一个执行完才能开始执行。
如何安全地给小表加字段?
首先我们要解决长事务, 事务不提交, 就会一直占着MDL锁。 在MySQL的information_schema库的 innodb_trx表中, 你可以查到当前执行中的事务。 如果你要做DDL变更的表刚好有长事务在执行, 要考虑先暂停DDL, 或者kill掉这个长事务。
如果你要变更的表是一个热点表, 虽然数据量不大, 但是上面的请求很频繁, 而你不得不加个字段, 你该怎么做呢?
这时候kill可能未必管用, 因为新的请求马上就来了。 比较理想的机制是, 在alter table语句里面设定等待时间, 如果在这个指定的等待时间里面能够拿到MDL写锁最好, 拿不到也不要阻塞后面的业务语句, 先放弃。 之后开发人员或者DBA再通过重试命令重复这个过程。
总结:
-
表锁一般是在数据库引擎不支持行锁的时候才会被用到的。 如果你发现你的应用程序里有locktables这样的语句, 你需要追查一下, 比较可能的情况是:
- 要么是你的系统现在还在用MyISAM这类不支持事务的引擎, 那要安排升级换引擎;
- 要么是你的引擎升级了, 但是代码还没升级。 我见过这样的情况, 最后业务开发就是把lock tables 和 unlock tables 改成 begin 和 commit, 问题就解决了。
-
MDL会直到事务提交才释放, 在做表结构变更的时候, 你一定要小心不要导致锁住线上查询和更新
2.3 行锁
MySQL的行锁是在引擎层由各个引擎自己实现的。但并不是所有的引擎都支持行锁,比如MyISAM引擎就不支持行锁 。这也是MyISAM被InnoDB替代的重要原因之一。
行锁就是针对数据表中行记录的锁。这很好理解,比如事务A更新了一行,而这时候事务B也要更新同一行,则必须等事务A的操作完成后才能进行更新。
分类
读锁=共享锁=S锁:获得读锁后不允许其他线程修改,但允许读
写锁=排他锁=X锁:获得写锁后不允许其他线程读取和修改
间隙锁:在InnoDB下,对数据加锁不仅仅是对已存在的数据加锁,也会对在我们要读取范围了但还不存在的数据加锁,这样就保证了不会中途插入,解决了幻读,这就是间隙锁。
二阶段锁
在InnoDB事务中, 行锁是在需要的时候才加上的, 但并不是不需要了就立刻释放, 而是要等到事务结束时才释放。 这个就是两阶段锁协议。
知道了这个设定, 对我们使用事务有什么帮助呢?
如果你的事务中需要锁多个行,要把最可能造成锁冲突、最可能影响并发度的锁尽量往后放。
死锁和死锁检测
当并发系统中不同线程出现循环资源依赖,涉及的线程都在等待别的线程释放资源时,就会导致这几个线程都进入无限等待的状态,称为死锁。
怎么解决死锁?
- 一种策略是, 直接进入等待,直到超时。这个超时时间可以通过参数innodb_lock_wait_timeout来设置(默认50s)。
- 另一种策略是, 发起死锁检测, 发现死锁后, 主动回滚死锁链条中的某一个事务, 让其他事务得以继续执行。 将参数innodb_deadlock_detect设置为on, 表示开启这个逻辑。(死锁检测耗费大量CPU资源)
死锁检测的思想是怎样的?
每当一个事务被锁的时候,就要看看它所依赖的线程有没有被别人锁住,如此循环,最后判断是否出现了循环等待,也就是死锁。
主动死锁检测在发生死锁的时候,是能够快速发现并进行处理的,但是它也是有额外负担的。
死锁检测有什么问题?
死锁检测要耗费大量的CPU资源。每个新来的被堵住的线程,都要判断会不会由于自己的加入导致了死锁,这是一个时间复杂度是O(n)的操作。假设有1000个并发线程要同时更新同一行,那么死锁检测操作就是100万这个量级的。虽然最终检测的结果是没有死锁,但是这期间要消耗大量的CPU资源。
怎么解决死锁检测耗费大量CPU资源的问题?
第一种思路是把死锁检测关闭,设置等待超时时间,超时就回滚。但是
第二种思路是控制并发度。这个并发控制要做在数据库服务端。如果你有中间件,可以考虑在中间件实现;如果你的团队有能修改MySQL源码的人,也可以做在MySQL里面。基本思路就是,对于相同行的更新,在进入引擎之前排队。这样在InnoDB内部就不会有大量的死锁检测工作了。
如果团队里暂时没有数据库方面的专家, 不能实现这样的方案, 能不能从设计上优化这个问题呢?
你可以考虑通过将一行改成逻辑上的多行来减少锁冲突。
3. 按使用方式分类
3.1 意向锁
意向锁是InnoDB内部使用的锁(数据库隐式帮我们做了,不用程序员操心),在事务打算给数据行加行共享锁或者行排他锁时,事务必须先取得该表的意向共享锁(IS)或意向排他锁(IX)
3.2 乐观锁
乐观锁并不是特指某种锁,而是一种设计思想。它总是假设不会有并发问题,在最后比较是否有其他事务更新了这个数据。无更新则完成并结束,有更新则回滚重试直到无更新。常用的实现方式是CAS。我们往往是在表中加个版本号解决ABA问题。
3.3 悲观锁
悲观锁总是认为会有并发问题,如读锁、写锁等事先就把数据的修改权限锁住的都是悲观锁。
本文介绍了MySQL数据库中锁的分类及应用。按粒度可分为全局锁、表级锁和行级锁,全局锁用于全库逻辑备份,表级锁包括表锁和元数据锁,行级锁由引擎实现;按使用方式分为意向锁、乐观锁和悲观锁,还提及了死锁及检测问题的解决思路。
1236

被折叠的 条评论
为什么被折叠?



