根据加锁的范围,MySQL锁可以分为 :

  • 全局锁
  • 表级锁
  • 行锁

全局锁(FTWRL)

全局锁就是对整个数据库实例加锁,使用 Flush tables with read lock (FTWRL)实现 ,作用是让整个库只读。

全局锁的使用场景 : 全库逻辑备份。

全局锁的缺点:加锁期间,业务停摆(只能读数据而不能更新数据)。

那为什么在备份的时候一定要加全局锁?

答 : 假设有一个网购网站,里面有人账户余额为100元,花60元买了一本书,还剩40元 。备份顺序是先备份余额表,再备份购买商品表。如果不加锁,备份结果可能就是该用户余额仍然为100元,商品却多了一本书。

不是有事务隔离可以保证可重复读吗?

答:InnoDB 存储引擎默认的事务隔离级别正是可重复读,因此可以采用这种方式来备份数据库。但是,如果备份数据库的工具是 mysqldump,是不支持事务的,在备份数据库时就要使用全局锁的方法。(这往往是 DBA 要求业务开发人员使用 InnoDB 替代 MyISAM 的原因之一。)

为什么不 set global readonly=true呢? 也是全局只读

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

表级锁

表锁

表锁的语法是 lock tables tableA read/write

read是共享锁,write是独占锁。

  • 共享锁:(S锁)满足读读共享,读写互斥。
  • 独占锁:(X锁)满足写写互斥、读写互斥。

即如果线程A执行lock tables tableA read,说明线程A和其他线程对tableA均只读。

如果线程A执行lock tables tableA write,说明线程A对tableA只读,其他线程对tableA都不能读写。

表锁不但会影响其他线程,还会本线程的操作对象(本线程指加锁的线程)

举例 ,如果线程A执行 lock tables tableA read

那么A线程在解锁之前,A线程有且只有对tableA有读的权利,连读别的表的权利都没有;

当然,其他线程也只能读A,不能改A, 但是其他线程有读写其他表的权利。

举例 ,如果线程A执行 lock tables tableA read

表锁缺点:表锁的颗粒度太大,会影响并发性能。

元数据锁MDL(metadata lock)

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

  • 当对一个表做增删改查操作的时候,加 MDL 读锁

    即如果A线程在对表A做CRUD,有B线程想给表A添加字段,就会被阻塞。但是读锁之间不互斥,其他线程也可以对表A做CRUD。

  • 当要对表做结构变更操作的时候,加 MDL 写锁

    即如果A线程对表A添加字段,那么其他线程做CRUD也会被阻塞。读写锁、写锁之间互斥,此时其他线程加字段也会被阻塞。

如果一个查询正在遍历一个表中的数据,而执行期间另一个线程对这个表结构做变更,删了一列,那么查询线程拿到的结果跟表结构对不上,肯定是不行的。这就是MDL的意义所在。

MDL锁是系统默认加上去的,在事务完成后才释放因此长事务可能会导致线程堵塞。(线程A\B同时加读锁,不冲突,且未提交。线程C加写锁,与读锁冲突,被堵塞。那么有一个写锁阻塞后,由于写锁获取优先级高于读锁,后续的所有读写请求都会阻塞)

如何安全的给表加字段?

答:首先我们要解决长事务,事务不提交,就会一直占着 MDL 锁。在 MySQL 的 information_schema 库的 innodb_trx 表中,你可以查到当前执行中的事务。如果你要做 表结构 变更的表刚好有长事务在执行,要考虑先暂停,或者 kill 掉那个长事务。

行锁

MySQL 的行锁是在引擎层由各个引擎自己实现的。但并不是所有的引擎都支持行锁,比如 MyISAM 引擎就不支持行锁。InnoDB 是支持行锁的,这也是 MyISAM 被 InnoDB 替代的重要原因之一。所以,在说 MySQL 是怎么加行级锁的时候,其实是在说 InnoDB 引擎是怎么加行级锁的。

行锁就是针对数据表中行记录的锁。比如事务 A 更新了一行,而这时候事务 B 也要更新同一行,则必须等事务 A 的操作完成后才能进行更新。

行锁写法:

  • select ... lock in share mode; //加共享锁
  • select ... for update; //加独占锁

两阶段锁协议 : 在 InnoDB 事务中,行锁是在需要的时候才加上的,但并不是不需要了就立刻释放,而是要等到事务结束时才释放。(因此要加上begin;commit;否则直接就完成事务了)

因此如果你的事务中需要锁多个行,要把最可能造成锁冲突、最可能影响并发度的锁尽量往后放。

举例 :对于一个买电影票事务而言,会有以下操作 :

  1. 从顾客 A 账户余额中扣除电影票价;
  2. 给影院 B 的账户余额增加这张电影票价;
  3. 记录一条交易日志。

如何安排这三个操作在事务中的顺序?

答: 试想如果同时有另外一个顾客 C 要在影院 B 买票,那么这两个事务冲突的部分就是语句 2 了。因为它们要更新同一个影院账户的余额,需要修改同一行数据。因此把2安排在最后即可。

死锁

死锁 : 当并发系统中不同线程出现循环资源依赖,涉及的线程都在等待别的线程释放资源时,就会导致这几个线程都进入无限等待的状态,称为死锁。

下面两个事务就会造成死锁 :

事务A 事务B
begin; begin;
update t set k = k + 1 where id = 1; (1)
update t set k = k + 1 where id = 2; (2)
update t set k = k + 1 where id = 1; (3)
update t set k = k + 1 where id = 1; (4)

由于(3)在等(2)释放id = 2的那行的锁,而(4)在等(1)释放id = 1的锁,两个事务互相等待,都没法提交,就形成死锁。

死锁解决

  • 方法一 : 直接进入等待,直到超时。。。。
  • 方法二 : 死锁检测。

死锁检测

发起死锁检测: 发现死锁后,主动回滚死锁链条中的某一个事务,让其他事务得以继续执行。

将参数 innodb_deadlock_detect 设置为 on,表示开启死锁检测。(默认就是on)

缺点 : 死锁检测要耗费大量的 CPU 资源。