MySQL锁
1、基本概念
锁是计算机协调多个进程或线程并发访问某一资源的机制。在数据库中的数据也是一种供多个用户使用的共享资源,
当多个用户并发地存取数据时,在数据库加锁的目的可以保证数据库数据的一致性。
- 锁的类型
在数据库中,对数据的操作其实只有两种,即读和写,所以可用共享锁和互斥锁实现,即共享锁间之间是兼容的,而互斥锁间不兼容。
- 共享锁(读锁):允许事务读
- 互斥锁(写锁):允许事务删除或者更新一行数据
共享锁代表了读操作、互斥锁代表了写操作,所以我们可以在数据库中并行读,但是只能串行写,只有这样才能保证不会发生线程竞争,实现线程安全。
- 锁的粒度
我们按照锁的粒度可以分为全局锁、表锁和行锁,不同的存储引擎拥有的锁粒度是不同的
在下面的文章中,我们按照锁粒度的划分分别学习全局锁、表级锁和行锁
2、全局锁
2.1基本概念
从名称上就可以看出,全局锁是对整个数据库实例加锁。MySQL提供了一个加全局读锁的方法,即Flush tables with read lock (FTWRL)。当对数据库使用这个命令时,数据库将会处于只读状态,之后的其它线程的语句:如数据更新语句(数据的增删改)DML,数据的定义语句(表结构的修改)DDL均会被阻塞。
2.2使用场景
全局锁的典型使用场景是做数据库逻辑备份,也就是将整个库中的所有表都select出来存成文本。在备份过程中的整个库要处于只读状态,这会存在以下问题
- 如果你在主库上备份,那么在备份期间都不能执行更新,业务基本上就能停止
- 如果你在从库上备份,那么备份期间从库不能执行主库同步过来的binlog,会导致主从延迟。
如果选用的引擎不支持事务(如MyISAM),在对数据库做逻辑备份时只能选择FTWRL。如果引擎支持事务,并且支持可重复读的隔离级别(如InnoDB),我们就可以用一致性视图来做备份,MySQL自带的逻辑备份工具是mysqldump。当mysqldump使用参数-single-transaction时,导入数据之前就可以启动一个事务,来确保拿到一致性视图。在MVCC的支持下,导入数据的过程中是可以正常更新的。
2.3lock/unlock命令
- 全局锁加锁命令
flush tables with read lock
复制代码
- 释放全局锁的命令
unlock tables
复制代码
断开连接也能释放全局锁
3、表级锁
MySQL的表级锁有两种:表锁,元数据锁(meta data lock MDL),
3.1表锁
表锁的语法是:lock tables ... read/write,可以利用unlock tables主动释放锁,也可以在连接断开时自动释放。需要注意的是,lock tables除了限制其它线程的读写,也会限制自身接下来的操作对象
举例
如果在某个线程 A 中执行 lock tables t1 read, t2 write这个语句,则其他线程写 t1、读写 t2 的语句都会被阻塞。与此同时,线程 A 在执行 unlock tables 之前,也只能执行读 t1、读写 t2 的操作。连写 t1 都不允许,自然也不能访问其他表。
这种操作整个表的影响很大,所以对于使用InnoDB引擎下,一般不适用lock tables命令控制并发。
3.2MDL
MDL全称是metadata lock,即元数据锁,它的作用主要是为了维护表中数据的一致性,即用于解决DDL操作与DML操作的一致性。
- 解决的问题
其主要解决了2个问题,一个是事务隔离问题,比如在可重复隔离级别下,会话A在2次查询期间,会话B对表结构做了修改,两次查询结果就会不一致,无法满足可重复读的要求;另外一个是数据复制的问题,比如会话A执行了多条更新语句期间,另外一个会话B做了表结构变更并且先提交,就会导致slave在重做时,先重做alter,再重做update时就会出现复制错误的现象。
元数据锁是server层的锁,表级锁,每执行一条DML、DDL语句时都会申请MDL锁,DML操作需要MDL读锁,DDL操作需要MDL写锁(MDL加锁过程是系统自动控制,无法直接干预,读读共享,读写互斥,写写互斥),申请MDL锁的操作会形成一个队列,队列中写锁获取优先级高于读锁。
NOTE:一旦出现写锁等待,不但当前操作会被阻塞,同时还会阻塞后续该表的所有操作(读操作当然也会被阻塞)。事务一旦申请到MDL锁后,直到事务执行完才会将锁释放。
MDL锁一旦发生可能会对数据库的业务带来其它的影响,因为后续对该表的访问都会被阻塞,造成连接积压。那么我们该如何避免MDL锁的发生呢?
- MDL锁的优化
- 避免长事务
- 在DDL操作中设定等待时间
4、行锁
行锁每次锁定的是一行数据,行级锁定不是MySQL自己实现锁定的方式,是由存储引擎实现的(InnoDB)自己实现的。
InnoDB实现了以下两种类型的行锁
- 共享锁(读锁)
允许一个事务去读一行,阻止其他事务获得相同数据集的排他锁。若事务T对数据对象A加上共享锁,则事务T可以读A但不能修改A,而其他事务只能再对对象A加共享锁,而不能加排他锁,直到事务T释放A上的共享锁。这保证了其他事务可以读A,但在事务T释放A上的S锁之前不能对A做任何修改。
- 排他锁(写锁)
允许获取排他锁的事务更新数据,阻止其他事务取得相同的数据集共享读锁和排他写锁。若事务T对数据对象A加上排他锁,事务T可以读A也可以修改A,其他事务不能再对A加任何锁,直到T释放A上的锁
NOTE:排他锁指的是一个事务在一行数据加上排他锁后,其他事务不能再在其上加其他的锁。mysql InnoDB引擎默认的修改数据语句:update,delete,insert都会自动给涉及到的数据加上排他锁,select语句默认不会加任何锁类型,如果加排他锁可以使用select …for update语句,加共享锁可以使用select … lock in share mode语句。所以加过排他锁的数据行在其他事务种是不能修改数据的,也不能通过for update和lock in share mode锁的方式查询数据,但可以直接通过select …from…查询数据,因为普通查询没有任何锁机制。
为了使得表锁和行锁共存,实现多粒度锁机制,InnoDB存在了两种内部使用的的意向锁
- 意向共享锁
事务打算给数据行共享锁,事务在给一个数据行加共享锁前必须先取得该表的意向共享锁。
- 意向排他锁
事务打算给数据行加排他锁,事务在给一个数据行加排他锁前必须先取得该表的意向排他锁
NOTE:意向锁是InnoDB自动加的,不需用户干预。对于UPDATE、DELETE和INSERT语句,InnoDB会自动给涉及数据集加排他锁;对于普通SELECT语句,InnoDB不会加任何锁。
- 死锁
当并发系统中不同线程出现循环资源依赖,涉及的线程都在等待别的线程释放资源时,就会导致这几个线程都进入无限等待的状态,称为死锁
事务 A 在等待事务 B 释放 id=2 的行锁,而事务 B 在等待事务 A 释放 id=1 的行锁。 事务 A 和事务 B 在互相等待对方的资源释放,就是进入了死锁状态。
解除死锁的策略
- 设置超时时间参数innodb_lock_wait_timeout,
- 将参数 innodb_deadlock_detect 设置为 on,发起死锁检测,当发现死锁后,主动回滚死锁链条中的某一个事务,让其它事务得以继续执行。
- 间隙锁(Next-key)
当我们用范围条件而不是相等条件检索数据,并请求共享或排他锁时,InnoDB会给符合条件的已有数据记录的 索引项加锁。对于键值在条件范围内但并不存在的记录叫做“间隙(GAP)”,InnoDB也会对这个“间隙”加锁,这种锁机制就是所谓的间隙锁 (Next-Key锁)。
使用间隙锁的目的是防止幻读