什么是锁

锁的相关概念

锁被设计出来的的初衷是为了处理并发访问,两个请求访问相同资源时候,为了防止发生冲突,创建一个锁文件,当下个请求到达时,如果锁文件存在,就排队等待锁所释放后进行访问,从而更加合理的控制资源的访问.

在 MySql 中锁大致分为3类,分别是,全局锁、表级锁和行锁。

全局锁

顾名思义,全局锁就是对整个数据库加锁,MySql 提供了一个加全局读锁的方法,命令是 Flush tables with read lock (FTWRL),执行此命令后,整个库会处于一个只读状态,更新,删除,插入包括表结构的修改都会被阻塞,相反 unlock tables; 就是释放全局锁.

全局锁的主要应用场景主要是,做全库的逻辑备份

但是这样子会给业务带来极大的问题(业务停摆,从库的话无法同步binlog,造成主从延迟).

还有就是,在客户端断开连接后,全局锁会自动释放.

表级锁

表级锁主要有两种,表锁和 MDL(元数据锁) 两种. MDL 是自动加锁的,不需要显式启动.而表锁则需要手动启动.

表锁

表锁用于给某个单表加锁,锁的类型根据读写分为,读锁和写锁.在 MyIsam 时代,表级锁就是最小粒度的锁, 也是最常用的处理并发的方式;

表锁的语法是 lock tables tableName read/write,和全局锁一样,可以使用unlock tables;主动释放锁,或者客户端断开的时候会自动释放锁.

对于加锁的 A线程 来说,只能读 加了读锁的表, 能读写加了 写锁 的表,不能访问其他表.
对于其他 B线程来说, 不能写 加了读锁的表, 不能 读写 加了 写锁的表.可以访问其他表

例如:
线程 A 中执行 lock tables table1 read, table2 write; 这个语句,则其他线程写 table1、读写 table2 的语句都会被阻塞。同时,线程 A 在执行 unlock tables 之前,也只能执行读 t1、读写 t2 的操作,不能读写其他表。

在还没有出现更加细粒度的锁的时候,表锁是最常用的处理并发的方法,而对于 InnoDB 这种支持行锁的引擎,一般不使用 lock tables 命令来控制并发,毕竟锁住整个表的影响面还是太大。

MDL(metadata lock)元数据锁

MDL 是并发情况下维护数据一致性,在表上有事务时,不可以对元数据进行写入操作,并且这个操作是在 service 层实现的. 主要的作用时防止 DDL 和 DML 并发的冲突.
MDL 不需要显示使用,在访问一个表的时候会自动加上,MDL 的所用是,保证读写的正确性,你们想想一下如果查询正在遍历一个表的数据,,执行的时候,另一个线程对这个表的结构做了变更,删除了一列,那么查询线程拿到的结果和表结构对不上,肯定是不行的.

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

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

所以当对线上数据库结构发生变化时,可能会锁住线上的查询和更新,那么应该如何安全的给小表加索引呢?

首先,我们要进行修改表之前,先看一下表上有没有长事务,事务如果不提交的话,会一直占用 MDL 锁,我们可以稍后执行 DDL ,或者先 kill 这个长事务;

如果这个表是个热点表,kill 可能不太管用了,我们可以在 alter 上设定一个等待时间,一段时间内拿不到 MDL 锁可以先放弃,不要阻塞后面的语句.

行锁

行锁是有引擎实现的,不是所有的引擎都支持行锁,不支持行锁意味着,要进行并发控制只能使用表锁,同一张表上,同时只能有一个更新在执行,这会严重影响到并发度,而 InnoDb 是支持行锁的,这是 MyISAM 被 InnoDB 代替的主要原因之一.

两阶段锁

事务A 事务B
begin;
update t set c = c+1 where id=1;
update t set c = c+1 where id=2;
begin;
update t set c = c+ 2 where id =1;
commit

当事务A执行完两条sql, 而未提交时,它具有什么锁呢?锁什么时候释放呢?

我们可以试一下:

事务B 的语句会被阻塞,只有当事务A commit,事务B,才能继续执行.

知道了这个答案,不难猜出事务 A 持有的两个记录的行锁,都是在 commit 的时候才释放的。

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

所以,如果我们在事务中有多个语句,需要锁住多个行,要把最可能造成冲突,最能影响并发的锁尽量往后放

死锁和死锁检测

当你发现 CPU 消耗接近100%,但是数据库每秒执行了不到100 个事务,你叫要看一下是否出现死锁了;

发生死锁

如图,事务A 在等着事务B id =2 的行锁释放,而事务B 在等着事务A id=1的行锁释放,他们都在相互等着对方释放锁,就造成了死锁.

当死锁出现后,有两种方式

  • 直接进入等待,直到超时。
  • 发起死锁检测, 发现死锁后,主动回滚死锁链条中的某一个事务,让其他事务得以继续执行。将参数 innodb_deadlock_detect 设置为 on,表示开启这个逻辑。

超时时间可以通过 innodb_lock_wait_timeout 参数设置,默认 50s, 也就是说,按照第一种策略,第一个被锁住的线程要过 50s 才会超时退出,然后其他线程才有可能继续执行.

所以,正常情况下我们还是要采用第二种策略,即:主动死锁检测,而且 innodb_deadlock_detect 的默认值本身就是 on。主动死锁检测在发生死锁的时候,是能够快速发现并进行处理的,但是它也是有额外负担的。

每当一个事务被锁的时候,就要看看它所依赖的线程有没有被别人锁住,如此循环,最后判断是否出现了循环等待。假设有 1000 个并发线程要同时更新同一行,那么死锁检测操作就是 100 万次左右的。虽然最终检测的结果可能是没有死锁,但是这期间要消耗大量的 CPU 资源.这样就会导致 CPU 利用率很高,但却执行不了几个事务。

如果我们要解决这种热点行更新导致的性能问题呢?

  • 我们必须在业务层去保证一定不会出现死锁,然后临时把死锁检测关掉.
  • 控制并发度,比如同一行同时最多只有 10 个线程在更新,那么死锁检测的成本很会很高.
  • 把一行变为 “多行”, 以影院余额为例,把影院的账户余额改为 10 个记录,影院的账户总额等于这 10 个记录的值的总和。这样每次要给影院账户加金额的时候,随机选其中一条记录来加。这样每次冲突概率变成原来的 1/10.
  • DML(data manipulation language):它们是SELECT、UPDATE、INSERT、DELETE,用来对数据库里的数据进行操作的语言
  • DDL(data definition language): DDL比DML要多,主要的命令有CREATE、ALTER、DROP等,DDL主要是用在定义或改变表(TABLE)的结构,数据类型,表之间的链接和约束等初始化工作上

本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!