【MySQL 读书笔记】全局锁 | 表锁 | 行锁

全局锁python

全局锁是针对数据库实例的直接加锁,MySQL 提供了一个加全局锁的方法, Flush tables with read lock 可使用锁将整个表的增删改操做都锁上其中包括 ddl 语句,只容许全局读操做。mysql

全局锁的典型使用场景是作全库的逻辑备份。web

不过如今使用官方自带工具 mysqldump 使用参数 --single-transaction 的时候,导出数据以前就会启动一个事务。来确保拿到一致性视图。这个应该相似于在可重复读隔离级别下启动一个一致性事务。因为 MVCC 的支持,这个过程当中数据能够正常更新。sql

另外提一点不太容易遇到的, --single-transaction 既然能够不用锁表,为何还须要使用全局锁?缘由是 --single-transaction 的时候须要支持一致性读,可是不支持事务的引擎是不支持一致性读的。这个时候就须要 FTWRL 命令了。数据库

还有另一种方法用来支持设置数据库为只读状态安全

set global readonly=true

这里 丁奇 不建议这样设置来设置数据库为只读有两个缘由session

一是,在有些系统中,readonly 的值会被用来作其余逻辑,好比用来判断一个库是主库仍是备库。所以,修改 global 变量的方式影响面更大,我不建议你使用。并发

二是,在异常处理机制上有差别。若是执行 FTWRL 命令以后因为客户端发生异常断开,那么 MySQL 会自动释放这个全局锁,整个库回到能够正常更新的状态。而将整个库设置为 readonly 以后,若是客户端发生异常,则数据库就会一直保持 readonly 状态,这样会致使整个库长时间处于不可写状态,风险较高。高并发

 

表级锁工具

MySQL 中表级别锁有两种:一种是普通表锁,一种是元数据锁(metadata lock. MDL)

表锁的语法是 lock tables xxx read/write 一样使用 unlock tables 来释放锁。经过加读锁咱们能够限制其余语句进行写入,可是重复加读锁不受影响。可是当咱们加写锁的时候,既不能够读也不能够写。一样在使用 unlock tables 以后能够解除锁定。

另一种表级锁是 MDL 锁(metadata lock) MDL 锁不须要显示的使用,在访问一个表的时候自动就被加上了。 MDL 锁是用来保证读写正确性的,当咱们对一个表在作 增删改查操做的时候都会被加上 MDL 读锁。当要进行 ddl 的时候须要加 MDL 写锁。

MDL 读锁与读锁之间不互斥,所以咱们能够多个线程进程对一个表进行增删改查。

MDL 读写锁之间互斥,用来保证表结构变动的安全性。所以若是有两个线程同时要给同一个表加字段,其中一个要等另一个执行完成以后再开始执行。

 

下面咱们来看一个比较有表明性的场景 MDL 读锁写锁互斥致使表没法读写被死锁。

session A: 开始一个事务,而后查询 t 表,这会给 t 表加上 MDL 读锁。(注意该事务被打开后就一直没有结束)

session B: 查询一个 t 表。这里应该是 autoocommit 会自动成功。

session C: 修改表 ddl 会加 MDL 写锁,和 session A 的读锁互斥。这个时候就锁住了表。

session D: 因为 session C 形成了写锁阻塞,因此后面全部的请求都会被锁住。

若是该表查询频繁,并且客户端有重试的机制,那么这个数据库的查询线程会很快被打满。

可能在进行 web 开发的同窗会常常遇到相似的状况。好比我在 ipython 里面打开了一个数据库某个表的链接,而后我一直没有 commit 。就可能形成该表在加写锁的时候阻塞后面全部的操做。

这种事情很是常见。

 

那么咱们如何安全的给小表加字段,首先咱们应该解决长事务或者脚本事务的问题,由于他们会一直挂读锁不结束。在 MySQL 的 information_schema 中的 innodb_trx 中能够查询到执行中的长事务,可是比较麻烦的是这个看不到很短的事务。可是每每进行 sleep 的短事务也可能由于一直没有 commit 而致使上面的状况出现。

这个时候就须要把对应表的 sleep 进程 kill 掉使其恢复正常。

 

行级锁 

先来看个描述两阶段锁的例子:

事务 A 会持有两条记录的行锁,而且只会在 commit 以后才会释放。

在 InnoDB 事务中,行锁是在须要的时候加上,可是并非不须要就马上释放,而是等事务结束以后才会释放。这个就是两阶段锁协议。

知道了这个设定咱们应该在长事务中把影响并发度的锁尽可能日后放。下面的这一段的介绍比较复杂,我以为 丁奇 讲得仍是比较清楚的因此直接引用原文了。

 

假设你负责实现一个电影票在线交易业务,顾客 A 要在影院 B 购买电影票。咱们简化一点,这个业务须要涉及到如下操做:

1. 从顾客 A 帐户余额中扣除电影票价;

2. 给影院 B 的帐户余额增长这张电影票价;

3. 记录一条交易日志。

也就是说,要完成这个交易,咱们须要 update 两条记录,并 insert 一条记录。固然,为了保证交易的原子性,咱们要把这三个操做放在一个事务中。那么,你会怎样安排这三个语句在事务中的顺序呢?

试想若是同时有另一个顾客 C 要在影院 B 买票,那么这两个事务冲突的部分就是语句 2 了。由于它们要更新同一个影院帐户的余额,须要修改同一行数据。

根据两阶段锁协议,不论你怎样安排语句顺序,全部的操做须要的行锁都是在事务提交的时候才释放的。因此,若是你把语句 2 安排在最后,好比按照 三、一、2 这样的顺序,那么影院帐户余额这一行的锁时间就最少。这就最大程度地减小了事务之间的锁等待,提高了并发度。

 

死锁和死锁检测 

若是出现下面的不慎操做就会发生死锁。

事务 A 开启事务,而且拿了 id = 1 的行锁。

事务 B 开启事务,拿到 id = 2 的行锁。

事务 A 试图去拿 id = 2 的行锁被 block。

事务 B 试图去拿 id = 1 的行锁被 block。

解决死锁 MySQL 目前有两种策略,第二种策略的参数我没有在 MySQL 5.6 版本中找到,在 MySQL 5.7 中找到。

1. 一种策略是,直接进入等待,直到超时。这个超时时间能够经过参数 innodb_lock_wait_timeout 来设置。这个参数默认是 50s。

2. 另外一种策略是,发起死锁检测,发现死锁后,主动回滚死锁链条中的某一个事务,让其余事务得以继续执行。将参数 innodb_deadlock_detect 设置为 on,表示开启这个逻辑。

很显然,等待 50 s 失效在现实业务中是不切实际的。确定会形成高并发的业务大量的阻塞和 500 。因此看上去咱们能够依赖第二种办法?

可是第二种办法也有反作用。

死锁检测会对每一个新来的被堵住的线程,都判断会不会因为本身的加入致使了死锁,这是一个时间复杂度是 O(n) 的操做。假设有 1000 个并发线程要同时更新同一行,那么死锁检测操做就是 100 万这个量级的。虽然最终检测的结果是没有死锁,可是这期间要消耗大量的 CPU 资源。所以,你就会看到 CPU 利用率很高,可是每秒却执行不了几个事务。

解决这个的方法是

1. 若是咱们能确保业务中就是不会存在死锁的逻辑,那么咱们能够关闭死锁检测。

2. 咱们控制并发度,不让某些业务更新这么快。对客户端的并发控制下来以后,死锁检测的效率是高的,也能够解决这个问题。

 

 

Reference:

本读书笔记皆来自发布在极客时间的 林晓斌(丁奇)的 MySQL 实战45讲:

极客时间版权全部: https://time.geekbang.org/ 版权全部: 

https://time.geekbang.org/column/article/69862

https://time.geekbang.org/column/article/70215

相关文章
相关标签/搜索