加锁是实现数据库并发控制的一个很是重要的技术。当事务在对某个数据对象进行操做前,先向系统发出请求,对其加锁。加锁后事务就对该数据对象有了必定的控制,在该事务释放锁以前,其余的事务不能对此数据对象进行更新操做。html
锁是计算机协调多个进程或线程并发访问某一资源的机制。mysql
锁保证数据并发访问的一致性、有效性;算法
锁冲突也是影响数据库并发访问性能的一个重要因素。sql
锁是Mysql在服务器层和存储引擎层的的并发控制。数据库
数据库是一个多用户使用的共享资源。当多个用户并发地存取数据时,在数据库中就会产生多个事务同时存取同一数据的状况。若对并发操做不加控制就可能会读取和存储不正确的数据,破坏数据库的一致性。安全
锁是用于管理对公共资源的并发控制。也就是说在并发的状况下,会出现资源竞争,因此须要加锁。bash
加锁解决了 多用户环境下保证数据库完整性和一致性。服务器
Lock的对象是事务,用来锁定的是数据库中的对象,如表、页、行。而且通常lock的对象仅在事务commit或rollback后进行释放(不一样事务隔离级别释放的时间可能不一样)。session
共享锁||读锁||S 锁(share lock):其余事务能够读,但不能写。容许一个事务去读一行,阻止其余事务得到相同数据集的排他锁。数据结构
排他锁||写锁||X 锁(exclusive) :其余事务不能读取,也不能写。容许得到排他锁的事务更新数据,阻止其余事务取得相同数据集的共享读锁和排他写锁。
类型细分:
意向共享锁(IS Lock/intent share lock)
意向排他锁||互斥锁(IX Lock/intent exclusive lock)
悲观锁||保守锁(pessimistic locking):假定会发生并发冲突,屏蔽一切可能违反数据完整性的操做。
悲观锁是数据库层面加锁,都会阻塞去等待锁。
乐观锁(optimistic locking):假设不会发生并发冲突,只在提交操做时检查是否违反数据完整性。
乐观锁是一种思想,具体实现是,表中有一个版本字段,第一次读的时候,获取到这个字段。处理完业务逻辑开始更新的时候,须要再次查看该字段的值是否和第一次的同样。若是同样更新,反之拒绝。之因此叫乐观,由于这个模式没有从数据库加锁,等到更新的时候再判断是否能够更新。
缺点:并发很高的时候,多了不少无用的重试。乐观锁,不能解决脏读的问题。
锁的开销是较为昂贵的,锁策略其实就是保证了线程安全的同时获取最大的性能之间的平衡策略。
行锁:即只容许事务读一行数据。行锁的粒度实在每一条行数据,固然也带来了最大开销,可是行锁能够最大限度的支持并发处理。
开销大,加锁慢;会出现死锁;锁定粒度最小,发生锁冲突的几率最低,并发度也最高。
最大程度的支持并发,同时也带来了最大的锁开销。
行级锁更适合于有大量按索引条件并发更新少许不一样数据,同时又有并发查询的应用,如一些在线事务处理(OLTP)系统
在 InnoDB 中,除单个 SQL 组成的事务外,锁是逐步得到的,这就决定了在 InnoDB 中发生死锁是可能的。
行级锁只在存储引擎层实现,而Mysql服务器层没有实现。
表锁:容许事务在行级上的锁和表级上的锁同时存在。锁定整个表,开销最小,可是也阻塞了整个表。
开销小,加锁快;不会出现死锁;锁定粒度大,发生锁冲突的几率最高,并发度最低。
这些存储引擎经过老是一次性同时获取全部须要的锁以及老是按相同的顺序获取表锁来避免死锁。
表级锁更适合于以查询为主,并发用户少,只有少许按索引条件更新数据的应用,如Web 应用。
若一个用户正在执行写操做,会获取排他的“写锁”,这可能会锁定整个表,阻塞其余用户的读、写操做;
若一个用户正在执行读操做,会先获取共享锁“读锁”,这个锁运行其余读锁并发的对这个表进行读取,互不干扰。只要没有写锁的进入,读锁能够是并发读取统一资源的。
Mysql的表级别锁分为两类:元数据锁(Metadata Lock,MDL)、表锁。
元数据锁(MDL) 不须要显式使用,在访问一个表的时候会被自动加上。这个特性须要MySQL5.5版本以上才会支持,
当对一个表作增删改查的时候,该表会被加MDL读锁
当对表作结构变动的时候,加MDL写锁
读锁之间不互斥,因此能够多线程多同一张表进行增删改查。
读写锁、写锁之间是互斥的,为了保证表结构变动的安全性,因此若是要多线程对同一个表加字段等表结构操做,就会变成串行化,须要进行锁等待。
MDL的写锁优先级比MDL读锁的优先级,可是能够设置max_write_lock_count系统变量来改变这种状况,当写锁请求超过这个变量设置的数后,MDL读锁的优先级会比MDL写锁的优先级高。(默认状况下,这个数字会很大,因此不用担忧写锁的优先级降低)
MDL的锁释放必需要等到事务结束才会释放
页面锁(page-level locking)
页级锁定是 MySQL 中比较独特的一种锁定级别,在其余数据库管理软件中也并非太常见。
页面锁开销和加锁时间界于表锁和行锁之间;会出现死锁;锁定粒度界于表锁和行锁之间,并发度通常。
页级锁定的特色是锁定颗粒度介于行级锁定与表级锁之间,因此获取锁定所须要的资源开销,以及所能提供的并发处理能力也一样是介于上面两者之间。另外,页级锁定和行级锁定同样,会发生死锁。
在数据库实现资源锁定的过程当中,随着锁定资源颗粒度的减少,锁定相同数据量的数据所须要消耗的内存数量是愈来愈多的,实现算法也会愈来愈复杂。
不过,随着锁定资源颗粒度的减少,应用程序的访问请求遇到锁等待的可能性也会随之下降,系统总体并发度也随之提高。
使用页级锁定的主要是 BerkeleyDB 存储引擎。
MySQL 提供全局锁来对整个数据库实例加锁。
FLUSH TABLES WITH READ LOCK
这条语句通常都是用来备份的,当执行这条语句后,数据库全部打开的表都会被关闭,而且使用全局读锁锁定数据库的全部表,同时,其余线程的更新语句(增删改),数据定义语句(建表,修改表结构)和更新类的事务提交都会被阻塞。
在mysql 8.0 之后,对于备份,mysql能够直接使用备份锁。
LOCK INSTANCE FOR BACKUP UNLOCK INSTANCE
这个锁的做用范围更广,这个锁会阻止文件的建立,重命名,删除,包括 REPAIR TABLE TRUNCATE TABLE, OPTIMIZE TABLE操做以及帐户的管理都会被阻塞。固然这些操做对于内存临时表来讲是能够执行的,为何内存表不受这些限制呢?由于内存表不须要备份,因此也就不必知足这些条件。
全部的存储引擎都以本身的方式显现了锁机制,服务器层彻底不了解存储引擎中的锁实现:
MyISAM、MEMORY、CSV存储引擎采用的是表级锁(table-level locking)
BDB(Berkeley DB) 存储引擎采用的是页面锁(page-level locking),但也支持表级锁
InnoDB 存储引擎既支持行级锁(row-level locking),也支持表级锁,但默认状况下是采用行级锁。
InnoDB行锁是经过给索引上的索引项加锁来实现的,InnoDB这种行锁实现特色意味着:只有经过索引条件检索数据,InnoDB才使用行级锁,不然,InnoDB将使用表锁!
行级锁都是基于索引的,若是一条SQL语句用不到索引是不会使用行级锁的,会使用表级锁。行级锁的缺点是:因为须要请求大量的锁资源,因此速度慢,内存消耗大。
默认状况下,表锁和行锁都是自动得到的, 不须要额外的命令。
可是在有的状况下, 用户须要明确地进行锁表或者进行事务的控制, 以便确保整个事务的完整性,这样就须要使用事务控制和锁定语句来完成。
InnoDB与MyISAM的最大不一样有两点:一是支持事务(TRANSACTION);二是采用了行级锁。
Innodb存储引擎因为实现了行级锁定,虽然在锁定机制的实现方面所带来的性能损耗可能比表级锁定会要更高一些,可是在总体并发处理能力方面要远远优于MyISAM的表级锁定的。当系统并发量较高的时候,Innodb的总体性能和MyISAM相比就会有比较明显的优点了。
可是,Innodb的行级锁定一样也有其脆弱的一面,当咱们使用不当的时候,可能会让Innodb的总体性能表现不只不能比MyISAM高,甚至可能会更差。
InnoDB 实现了如下两种类型的行锁:
共享锁(S-shared):容许一个事务去读一行,阻止其余事务得到相同数据集的排他锁。
排他锁(X-exclusive):容许得到排他锁的事务更新数据,阻止其余事务取得相同数据集的共享读锁和排他写锁。
为了支持在不一样粒度上进行加锁操做(容许行锁和表锁共存,实现多粒度锁机制),InnoDB 还有两种内部使用的意向锁(Intention Locks),这两种意向锁都是表锁:
意向共享锁(IS- intent share lock)事务想要得到一张表中某几行的共享锁
务打算给数据行加行共享锁,事务在给一个数据行加共享锁前必须先取得该表的 IS 锁。
意向排他锁(IX -intent exclusive lock)事务想要得到一张表中某几行的排他锁
事务打算给数据行加行排他锁,事务在给一个数据行加排他锁前必须先取得该表的 IX 锁。
因为InnoDB存储引擎支持的是行级别的锁,所以意向锁其实不会阻塞除全表扫之外的任何请求。故表级意向锁与行级锁的兼容性以下所示
若是一个事务请求的锁模式与当前的锁兼容, InnoDB 就将请求的锁授予该事务; 反之, 若是二者不兼容,该事务就要等待锁释放
若将上锁的对象当作一棵树,那么对最下层的对象上锁,也就是对最细粒度的对象进行上锁,那么首先须要对粗粒度的对象上锁。例上图,若是须要对页上的记录r进行上X锁,那么分别须要对数据库A、表、页上意向锁IX,最后对记录r上X锁。若其中任何一个部分致使等待,那么该操做须要等待粗粒度锁的完成。举例来讲,在对记录r加X锁以前,已经有事务对表1进行了S表锁,那么表1上已存在S锁,以后事务须要对记录r在表1上加上IX,因为不兼容,因此该事务须要等待表锁操做的完成。
innodb的意向锁主要用户多粒度的锁并存的状况。好比事务A要在一个表上加S锁,若是表中的一行已被事务B加了X锁,那么该锁的申请也应被阻塞。若是表中的数据不少,逐行检查锁标志的开销将很大,系统的性能将会受到影响。为了解决这个问题,能够在表级上引入新的锁类型来表示其所属行的加锁状况,这就引出了“意向锁”的概念。
举个例子,若是表中记录1亿,事务A把其中有几条记录上了行锁了,这时事务B须要给这个表加表级锁,若是没有意向锁的话,那就要去表中查找这一亿条记录是否上锁了。若是存在乎向锁,那么假如事务A在更新一条记录以前,先加意向锁,再加X锁,事务B先检查该表上是否存在乎向锁,存在的意向锁是否与本身准备加的锁冲突,若是有冲突,则等待直到事务A释放,而无须逐条记录去检测。事务B更新表时,其实无须知道到底哪一行被锁了,它只要知道反正有一行被锁了就好了。
主要做用是处理行锁和表锁之间的矛盾,可以显示“某个事务正在某一行上持有了锁,或者准备去持有锁”
行锁是加在索引上的
Innodb中的索引数据结构是 B+ 树,数据是有序排列的,从根节点到叶子节点一层层找到对应的数据。
普通索引,也叫作辅助索引,叶子节点存放的是主键值。主键上的索引叫作汇集索引,表里的每一条记录都存放在主键的叶子节点上。当经过辅助索引select 查询数据的时候,会先在辅助索引中找到对应的主键值,而后用主键值在汇集索引中找到该条记录。
举个例子,用name=Alice来查询的时候,会先找到对应的主键值是18 ,而后用18在下面的汇集索引中找到name=Alice的记录内容是 77 和 Alice。
表中每一行的数据,是组织存放在汇集索引中的,因此叫作索引组织表。
InnoDB 行锁是经过给索引上的索引项加锁来实现的,这一点 MySQL 与 Oracle 不一样,后者是经过在数据块中对相应数据行加锁来实现的。InnoDB 这种行锁实现特色意味着:只有经过索引条件检索数据,InnoDB 才使用行级锁,不然,InnoDB 将使用表锁!
不管是使用主键索引、惟一索引或普通索引,InnoDB 都会使用行锁来对数据加锁。
只有执行计划真正使用了索引,才能使用行锁:即使在条件中使用了索引字段,可是否使用索引来检索数据是由 MySQL 经过判断不一样执行计划的代价来决定的,若是 MySQL 认为全表扫描效率更高,好比对一些很小的表,它就不会使用索引,这种状况下 InnoDB 将使用表锁,而不是行锁。所以,在分析锁冲突时,别忘了检查 SQL 的执行计划(能够经过 explain 检查 SQL 的执行计划),以确认是否真正使用了索引。(更多阅读:MySQL索引总结)
因为 MySQL 的行锁是针对索引加的锁,不是针对记录加的锁,因此虽然多个session是访问不一样行的记录, 可是若是是使用相同的索引键, 是会出现锁冲突的(后使用这些索引的session须要等待先使用索引的session释放锁后,才能获取锁)。 应用设计的时候要注意这一点。
Record Lock(单行记录)
Gap Lock(间隙锁,锁定一个范围,但不包含锁定记录)
Next-Key Lock(Record Lock + Gap Lock,锁定一个范围,而且锁定记录自己, MySql 防止幻读,就是使用此锁实现)
记录锁、间隙锁、临键锁都是排它锁
事务加锁后锁住的只是表的某一条记录。
记录锁出现条件:精准条件命中,而且命中的条件字段是惟一索引;
例如:update user_info set name=’张三’ where id=1 ,这里的id是惟一索引。
Record Lock老是会去锁住索引记录,若是InnoDB存储引擎表在创建的时候没有设置任何一个索引,那么这时InnoDB存储引擎会使用隐式的主键来进行锁定。
记录锁的做用:加了记录锁以后能够避免数据在查询的时候被修改的重复读问题,也避免了在修改的事务未提交前被其余事务读取的脏读问题。
临键锁是INNODB的行锁默认算法,它是记录锁和间隙锁的组合,临键锁会把查询出来的记录锁住,同时也会把该范围查询内的全部间隙空间也会锁住,再之它会把相邻的下一个区间也会锁住。
临键锁出现条件:范围查询并命中,查询命中了索引。
好比下面表的数据执行 select * from user_info where id>1 and id<=13 for update ;
会锁住ID为 1,5,10的记录;同时会锁住,1至5,5至10,10至15的区间。
临键锁的做用:结合记录锁和间隙锁的特性,临键锁避免了在范围查询时出现脏读、重复读、幻读问题。加了临键锁以后,在范围区间内数据不容许被修改和插入。
Next-Key Lock是结合了Gap Lock和Record Lock的一种锁定算法,在Next-Key Lock算法下,InnoDB对于行的查询都是采用这种锁定算法。
除了Next-Key Locking,还有Previous-Key Locking技术。
当咱们用范围条件而不是相等条件检索数据,并请求共享或排他锁时,InnoDB会给符合条件的已有数据记录的索引项加锁;对于键值在条件范围内但并不存在的记录,叫作“间隙(GAP)”,InnoDB也会对这个“间隙”加锁,这种锁机制就是所谓的间隙锁。
很显然,在使用范围条件检索并锁定记录时,InnoDB这种加锁机制会阻塞符合条件范围内键值的并发插入,这每每会形成严重的锁等待。所以,在实际应用开发中,尤为是并发插入比较多的应用,咱们要尽可能优化业务逻辑,尽可能使用相等条件来访问更新数据,避免使用范围条件。
防止幻读,以知足相关隔离级别的要求;
知足恢复和复制的须要:
使用普通索引锁定;
使用多列惟一索引;
使用惟一索引锁定多行记录。
以上状况,都会产生间隙锁
推荐阅读《MySQL的锁机制 - 记录锁、间隙锁、临键锁》
MySQL 经过 BINLOG 录入执行成功的 INSERT、UPDATE、DELETE 等更新数据的 SQL 语句,并由此实现 MySQL 数据库的恢复和主从复制。MySQL 的恢复机制(复制其实就是在 Slave Mysql 不断作基于 BINLOG 的恢复)有如下特色:
一是 MySQL 的恢复是 SQL 语句级的,也就是从新执行 BINLOG 中的 SQL 语句。
二是 MySQL 的 Binlog 是按照事务提交的前后顺序记录的, 恢复也是按这个顺序进行的。
因而可知,MySQL 的恢复机制要求:在一个事务未提交前,其余并发事务不能插入知足其锁定条件的任何记录,也就是不容许出现幻读。
这张图里出现了三种锁
记录锁:单行记录上的锁
间隙锁:锁定记录之间的范围,但不包含记录自己。
Next Key Lock: 记录锁+ 间隙锁,锁定一个范围,包含记录自己。
不是全部索引都会加上Next-key Lock的,在查询的列是惟一索引(包含主键索引)的状况下,Next-key Lock会降级为Record Lock。
CREATE TABLE z (a INT,b INT,PRIMARY KEY(a),KEY(b));// a是主键索引,b是普通索引
INSERT INTO z select1,1;
INSERT INTO z select3,1;
INSERT INTO z select5,3;
INSERT INTO z select7,6;
INSERT INTO z select10,8;复制代码
这时候在会话A中执行 SELECT*FROM z WHERE b=3FOR UPDATE ,索引锁定以下:
这时候会话B执行的语句落在锁定范围内的都会进行waiting
SELECT * FROM z WHERE a =5 LOCK IN SHARE MODE;
INSERT INTO z SELECT 4,2;
INSERT INTO z SELECT 6,5;复制代码
用户能够经过如下两种方式来显示的关闭Gap Lock:
将事务的隔离级别设为 READ COMMITED。
将参数innodblocksunsafeforbinlog设置为1。
从上面的例子能够看出来,Gap Lock的做用是为了阻止多个事务将记录插入到同一个范围内,设计它的目的是用来解决Phontom Problem(幻读问题)。在MySQL默认的隔离级别(Repeatable Read)下,InnoDB就是使用它来解决幻读问题。
意向锁是 InnoDB 自动加的, 不需用户干预。
对于 UPDATE、 DELETE 和 INSERT 语句, InnoDB 会自动给涉及数据集加排他锁(X);
对于普通 SELECT 语句,InnoDB 不会加任何锁;
事务能够经过如下语句显式给记录集加共享锁或排他锁:
共享锁(S):SELECT * FROM table_name WHERE ... LOCK IN SHARE MODE。 其余 session 仍然能够查询记录,并也能够对该记录加 share mode 的共享锁。可是若是当前事务须要对该记录进行更新操做,则颇有可能形成死锁。
排他锁(X):SELECT * FROM table_name WHERE ... FOR UPDATE。其余 session 能够查询该记录,可是不能对该记录加共享锁或排他锁,而是等待得到锁
InnoDB在事务执行过程当中,使用两阶段锁协议:
随时均可以执行锁定,InnoDB会根据隔离级别在须要的时候自动加锁;
锁只有在执行commit或者rollback的时候才会释放,而且全部的锁都是在同一时刻被释放。
select ... lock in share mode //共享锁
select ... for update //排他锁复制代码
在执行这个 select 查询语句的时候,会将对应的索引访问条目进行上排他锁(X 锁),也就是说这个语句对应的锁就至关于update带来的效果。
select *** for update 的使用场景:为了让本身查到的数据确保是最新数据,而且查到后的数据只容许本身来修改的时候,须要用到 for update 子句。
经过锁住汇集索引中的节点来锁住这条记录(锁住id=10的索引,即锁住了这条记录)。
这里的name上加了惟一索引,惟一索引本质上是辅助索引,加了惟一约束。因此会先在辅助索引上找到name为d的索引记录,在辅助索引中加锁,而后查找汇集索引,锁住对应索引记录。
试想一下,若是有并发的另一个SQL,是直接经过主键索引id=30来更新,会先在汇集索引中请求加锁。若是只在辅助索引中加锁的话,两个并发SQL之间是互相感知不到的。
in share mode 子句的做用就是将查找到的数据加上一个 share 锁,这个就是表示其余的事务只能对这些数据进行简单的select 操做,并不可以进行 DML 操做。
select *** lock in share mode 使用场景:为了确保本身查到的数据没有被其余的事务正在修改,也就是说确保查到的数据是最新的数据,而且不容许其余人来修改数据。可是本身不必定可以修改数据,由于有可能其余的事务也对这些数据 使用了 in share mode 的方式上了 S 锁。
前一个上的是排他锁(X 锁),一旦一个事务获取了这个锁,其余的事务是无法在这些数据上执行 for update ;后一个是共享锁,多个事务能够同时的对相同数据执行 lock in share mode。
select for update 语句,至关于一个 update 语句。在业务繁忙的状况下,若是事务没有及时的commit或者rollback 可能会形成其余事务长时间的等待,从而影响数据库的并发使用效率。
select lock in share mode 语句是一个给查找的数据上一个共享锁(S 锁)的功能,它容许其余的事务也对该数据上S锁,可是不可以容许对该数据进行修改。若是不及时的commit 或者rollback 也可能会形成大量的事务等待。
默认是 MVCC 机制(“一致性非锁定读-consistent nonlocking read”)保证 RR 级别的隔离正确性,是不上锁的。
能够选择手动上锁:select xxxx for update (排他锁); select xxxx lock in share mode(共享锁),称之为“一致性锁定读”。
使用锁以后,就能在 RR 级别下,避免幻读。固然,默认的 MVCC 读,也能避免幻读。
既然 RR 可以防止幻读,那么,SERIALIZABLE 有啥用呢?防止丢失更新。
这个时候,咱们必须使用 SERIALIZABLE 级别进行串行读取。
最后,行锁的实现原理就是锁住汇集索引,若是你查询的时候,没有正确地击中索引,MySql 优化器将会抛弃行锁,使用表锁。
锁和多版本数据(MVCC)是 InnoDB 实现一致性读和 ISO/ANSI SQL92 隔离级别的手段。
所以,在不一样的隔离级别下,InnoDB 处理 SQL 时采用的一致性读策略和须要的锁是不一样的:
对于许多 SQL,隔离级别越高,InnoDB 给记录集加的锁就越严格(尤为是使用范围条件的时候),产生锁冲突的可能性也就越高,从而对并发性事务处理性能的 影响也就越大。
所以, 咱们在应用中, 应该尽可能使用较低的隔离级别, 以减小锁争用的机率。实际上,经过优化事务逻辑,大部分应用使用 Read Commited 隔离级别就足够了。对于一些确实须要更高隔离级别的事务, 能够经过在程序中执行 SET SESSION TRANSACTION ISOLATION LEVEL REPEATABLE READ 或 SET SESSION TRANSACTION ISOLATION LEVEL SERIALIZABLE 动态改变隔离级别的方式知足需求。
合理利用 InnoDB 的行级锁定,作到扬长避短
尽量让全部的数据检索都经过索引来完成,从而避免 InnoDB 由于没法经过索引键加锁而升级为表级锁定。
合理设计索引,让 InnoDB 在索引键上面加锁的时候尽量准确,尽量的缩小锁定范围,避免形成没必要要的锁定而影响其余 Query 的执行。
尽量减小基于范围的数据检索过滤条件,避免由于间隙锁带来的负面影响而锁定了不应锁定的记录。
尽可能控制事务的大小,减小锁定的资源量和锁定时间长度。
在业务环境容许的状况下,尽可能使用较低级别的事务隔离,以减小 MySQL 由于实现事务隔离级别所带来的附加成本。
对于 InnoDB 表,在绝大部分状况下都应该使用行级锁,由于事务和行锁每每是咱们之因此选择 InnoDB 表的理由。
事务须要更新大部分或所有数据,表又比较大,若是使用默认的行锁,不只这个事务执行效率低,并且可能形成其余事务长时间锁等待和锁冲突,这种状况下能够考虑使用表锁来提升该事务的执行速度。
事务涉及多个表,比较复杂,极可能引发死锁,形成大量事务回滚。这种状况也能够考虑一次性锁定事务涉及的表,从而避免死锁、减小数据库因事务回滚带来的开销。
在 InnoDB 下,使用表锁要注意如下两点:
使用 LOCK TABLES 虽然能够给 InnoDB 加表级锁,但必须说明的是,表锁不是由 InnoDB 存储引擎层管理的,而是由其上一层──MySQL Server 负责的。仅当 autocommit=0(不自动提交,默认是自动提交的)、InnoDB_table_locks=1(默认设置)时,InnoDB 层才能知道 MySQL 加的表锁,MySQL Server 也才能感知 InnoDB 加的行锁。这种状况下,InnoDB 才能自动识别涉及表级锁的死锁,不然,InnoDB 将没法自动检测并处理这种死锁。
在用 LOCK TABLES 对 InnoDB 表加锁时要注意,要将 AUTOCOMMIT 设为 0,不然 MySQL 不会给表加锁。事务结束前,不要用 UNLOCK TABLES 释放表锁,由于 UNLOCK TABLES 会隐含地提交事务。COMMIT 或 ROLLBACK 并不能释放用 LOCK TABLES 加的表级锁,必须用 UNLOCK TABLES 释放表锁。
正确的方式见以下语句,例如,若是须要写表 t1 并从表 t 读,能够按以下作:
SET AUTOCOMMIT=0;
LOCK TABLES t1 WRITE, t2 READ, ...;
[do something with tables t1 and t2 here];
COMMIT;
UNLOCK TABLES;复制代码
表共享读锁 (Table Read Lock):不会阻塞其余用户对同一表的读请求,但会阻塞对同一表的写请求;
表独占写锁 (Table Write Lock):会阻塞其余用户对同一表的读和写操做;
MyISAM 表的读操做与写操做之间,以及写操做之间是串行的。当一个线程得到对一个表的写锁后, 只有持有锁的线程能够对表进行更新操做。 其余线程的读、 写操做都会等待,直到锁被释放为止。
默认状况下,写锁比读锁具备更高的优先级:当一个锁释放时,这个锁会优先给写锁队列中等候的获取锁请求,而后再给读锁队列中等候的获取锁请求。 (This ensures that updates to a table are not “starved” even when there is heavy SELECT activity for the table. However, if there are many updates for a table, SELECT statements wait until there are no more updates.)。
这也正是 MyISAM 表不太适合于有大量更新操做和查询操做应用的缘由,由于,大量的更新操做会形成查询操做很难得到读锁,从而可能永远阻塞。同时,一些须要长时间运行的查询操做,也会使写线程“饿死” ,应用中应尽可能避免出现长时间运行的查询操做(在可能的状况下能够经过使用中间表等措施对SQL语句作必定的“分解” ,使每一步查询都能在较短期完成,从而减小锁冲突。若是复杂查询不可避免,应尽可能安排在数据库空闲时段执行,好比一些按期统计能够安排在夜间执行)。
能够设置改变读锁和写锁的优先级:
经过指定启动参数low-priority-updates,使MyISAM引擎默认给予读请求以优先的权利。
经过执行命令SET LOW_PRIORITY_UPDATES=1,使该链接发出的更新请求优先级下降。
经过指定INSERT、UPDATE、DELETE语句的LOW_PRIORITY属性,下降该语句的优先级。
给系统参数max_write_lock_count设置一个合适的值,当一个表的读锁达到这个值后,MySQL就暂时将写请求的优先级下降,给读进程必定得到锁的机会。
在执行查询语句(SELECT)前,会自动给涉及的表加读锁
在执行更新操做(UPDATE、DELETE、INSERT 等)前,会自动给涉及的表加写锁
这个过程并不须要用户干预,所以,用户通常不须要直接用 LOCK TABLE 命令给 MyISAM 表显式加锁。
在自动加锁的状况下,MyISAM 老是一次得到 SQL 语句所须要的所有锁,这也正是 MyISAM 表不会出现死锁(Deadlock Free)的缘由。
MyISAM存储引擎支持并发插入,以减小给定表的读和写操做之间的争用:
若是MyISAM表在数据文件中间没有空闲块,则行始终插入数据文件的末尾。 在这种状况下,你能够自由混合并发使用MyISAM表的INSERT和SELECT语句而不须要加锁——你能够在其余线程进行读操做的时候,同时将行插入到MyISAM表中。 文件中间的空闲块多是从表格中间删除或更新的行而产生的。 若是文件中间有空闲快,则并发插入会被禁用,可是当全部空闲块都填充有新数据时,它又会自动从新启用。 要控制此行为,可使用MySQL的concurrent_insert系统变量。
若是你使用LOCK TABLES显式获取表锁,则能够请求READ LOCAL锁而不是READ锁,以便在锁定表时,其余会话可使用并发插入。
当concurrent_insert设置为0时,不容许并发插入。
当concurrent_insert设置为1时,若是MyISAM表中没有空洞(即表的中间没有被删除的行),MyISAM容许在一个线程读表的同时,另外一个线程从表尾插入记录。这也是MySQL的默认设置。
当concurrent_insert设置为2时,不管MyISAM表中有没有空洞,都容许在表尾并发插入记录。
显示锁sql语句
共享读锁:lock table tableName read
独占写锁:lock table tableName write
同时加多锁:lock table t1 write,t2 read
批量解锁:unlock tables
惟一的办法就是让咱们的 Query 执行时间尽量的短
尽可能减小大的复杂 Query,将复杂 Query 分拆成几个小的 Query 分布进行。
尽量的创建足够高效的索引,让数据检索更迅速。
尽可能让 MyISAM 存储引擎的表只存放必要的信息,控制字段类型。
利用合适的机会优化 MyISAM 表数据文件。
MyISAM 存储引擎有一个控制是否打开 Concurrent Insert 功能的参数选项:
concurrent_insert=2,不管 MyISAM 表中有没有空洞,都容许在表尾并发插入记录。
concurrent_insert=1,若是 MyISAM 表中没有空洞(即表的中间没有被删除的行),MyISAM 容许在一个进程读表的同时,另外一个进程从表尾插入记录。这也是 MySQL 的默认设置。
concurrent_insert=0,不容许并发插入
能够利用 MyISAM 存储引擎的并发插入特性,来解决应用中对同一表查询和插入的锁争用。
例如,将 concurrent_insert 系统变量设为 2,老是容许并发插入;同时,经过按期在系统空闲时段执行 OPTIMIZE TABLE 语句来整理空间碎片,收回因删除记录而产生的中间空洞。
经过执行命令 SET LOW_PRIORITY_UPDATES=1,使该链接读比写的优先级高,若是咱们的系统是一个以读为主,能够设置此参数,若是以写为主,则不用设置。
经过指定 INSERT、UPDATE、DELETE 语句的 LOW_PRIORITY 属性,下降该语句的优先级。
MySQL 也提供了一种折中的办法来调节读写冲突,即给系统参数 max_write_lock_count 设置一个合适的值,当一个表的读锁达到这个值后,MySQL 就暂时将写请求的优先级下降,给读进程必定得到锁的机会。
须要长时间运行的查询操做,也会使写进程“饿死”,尽可能避免出现长时间运行的查询操做,不要总想用一条 SELECT 语句来解决问题,由于这种看似巧妙的 SQL 语句,每每比较复杂,执行时间较长。
多表级联。事务涉及多个表,比较复杂的关联查询,极可能引发死锁,形成大量事务回滚,这种状况若能一次性锁定事务涉及的表,从而能够避免死锁、减小数据库因事务回滚带来的开销。
死锁产生:
死锁是指两个或多个事务在同一资源上相互占用,并请求锁定对方占用的资源,从而致使恶性循环。
当事务试图以不一样的顺序锁定资源时,就可能产生死锁。多个事务同时锁定同一个资源时也可能会产生死锁。
锁的行为和顺序和存储引擎相关。以一样的顺序执行语句,有些存储引擎会产生死锁有些不会——死锁有双重缘由:真正的数据冲突;存储引擎的实现方式。
检测死锁:数据库系统实现了各类死锁检测和死锁超时的机制。InnoDB存储引擎能检测到死锁的循环依赖并当即返回一个错误。
死锁恢复:死锁发生之后,只有部分或彻底回滚其中一个事务,才能打破死锁,InnoDB目前处理死锁的方法是,将持有最少行级排他锁的事务进行回滚。因此事务型应用程序在设计时必须考虑如何处理死锁,多数状况下只须要从新执行因死锁回滚的事务便可。
外部锁的死锁检测:发生死锁后,InnoDB 通常都能自动检测到,并使一个事务释放锁并回退,另外一个事务得到锁,继续完成事务。但在涉及外部锁,或涉及表锁的状况下,InnoDB 并不能彻底自动检测到死锁, 这须要经过设置锁等待超时参数 innodb_lock_wait_timeout 来解决
死锁影响性能:死锁会影响性能而不是会产生严重错误,由于InnoDB会自动检测死锁情况并回滚其中一个受影响的事务。在高并发系统上,当许多线程等待同一个锁时,死锁检测可能致使速度变慢。 有时当发生死锁时,禁用死锁检测(使用innodb_deadlock_detect配置选项)可能会更有效,这时能够依赖innodb_lock_wait_timeout设置进行事务回滚。
在自动加锁的状况下,MyISAM 表不会出现死锁(MyISAM 老是一次得到 SQL 语句所须要的所有锁)。
为了在单个InnoDB表上执行多个并发写入操做时避免死锁,能够在事务开始时经过为预期要修改的每一个元祖(行)使用SELECT ... FOR UPDATE语句来获取必要的锁,即便这些行的更改语句是在以后才执行的。
在事务中,若是要更新记录,应该直接申请足够级别的锁,即排他锁,而不该先申请共享锁、更新时再申请排他锁,由于这时候当用户再申请排他锁时,其余事务可能又已经得到了相同记录的共享锁,从而形成锁冲突,甚至死锁
若是事务须要修改或锁定多个表,则应在每一个事务中以相同的顺序使用加锁语句。 在应用中,若是不一样的程序会并发存取多个表,应尽可能约定以相同的顺序来访问表,这样能够大大下降产生死锁的机会
经过SELECT ... LOCK IN SHARE MODE获取行的读锁后,若是当前事务再须要对该记录进行更新操做,则颇有可能形成死锁。
改变事务隔离级别,如下降隔离级别(若是业务容许,将隔离级别调低也是较好的选择,好比将隔离级别从RR调整为RC,能够避免掉不少由于gap锁形成的死锁)
为表添加合理的索引。能够看到若是不走索引将会为表的每一行记录添加上锁,死锁的几率大大增大。
若是出现死锁,能够用 SHOW INNODB STATUS 命令来肯定最后一个死锁产生的缘由。返回结果中包括死锁相关事务的详细信息,如引起死锁的 SQL 语句,事务已经得到的锁,正在等待什么锁,以及被回滚的事务等。据此能够分析死锁产生的缘由和改进措施。
下面两条简单的SQL,他们加什么锁?
select * from t1 where id = 10
delete from t1 where id = 10
若是要分析加锁状况,必须还要知道如下的一些前提,前提不一样,加锁处理的方式也不一样
id列是否是主键?
当前系统的隔离级别是什么?
id列若是不是主键,那么id列上有索引吗?
id列上若是有二级索引,那么这个索引是惟一索引吗?
两个SQL的执行计划是什么?索引扫描?全表扫描?
根据上述状况,有如下几种组合
id列是主键,RC隔离级别
id列是二级惟一索引,RC隔离级别
id列是二级非惟一索引,RC隔离级别
id列上没有索引,RC隔离级别
id列是主键,RR隔离级别
id列是二级惟一索引,RR隔离级别
id列是二级非惟一索引,RR隔离级别
id列上没有索引,RR隔离级别
Serializable隔离级别
排列组合尚未列举彻底,可是看起来,已经不少了。真的有必要这么复杂吗?事实上,要分析加锁,就是须要这么复杂。可是从另外一个角度来讲,只要你选定了一种组合,SQL须要加哪些锁,其实也就肯定了。接下来挑几个比较经典的组合
这个组合,是最简单,最容易分析的组合。id是主键,Read Committed隔离级别,给定SQL:delete from t1 where id = 10; 只须要将主键上,id = 10的记录加上X锁便可。以下图1:
结论:id是主键时,此SQL只须要在id=10这条记录上加X锁便可。
这个组合,id不是主键,而是一个Unique的二级索引键值。那么在RC隔离级别下,delete from t1 where id = 10; 须要加什么锁呢?见下图2:
id是unique索引,而主键是name列。此时,加锁的状况因为组合一有所不一样。因为id是unique索引,所以delete语句会选择走id列的索引进行where条件的过滤,在找到id=10的记录后,首先会将unique索引上的id=10索引记录加上X锁,同时,会根据读取到的name列,回主键索引(聚簇索引),而后将聚簇索引上的name = ‘d’ 对应的主键索引项加X锁。
结论:若id列是unique列,其上有unique索引。那么SQL须要加两个X锁,一个对应于id unique索引上的id = 10的记录,另外一把锁对应于聚簇索引上的[name='d',id=10]的记录、
相对于组合1、二,组合三又发生了变化,隔离级别仍旧是RC不变,可是id列上的约束又下降了,id列再也不惟一,只有一个普通的索引。假设delete from t1 where id = 10; 语句,仍旧选择id列上的索引进行过滤where条件,那么此时会持有哪些锁?一样见下图3:根据此图,能够看到,首先,id列索引上,知足id = 10查询条件的记录,均已加锁。同时,这些记录对应的主键索引上的记录也都加上了锁。与组合二惟一的区别在于,组合二最多只有一个知足等值查询的记录,而组合三会将全部知足查询条件的记录都加锁。
结论:若id列上有非惟一索引,那么对应的全部知足SQL查询条件的记录,都会被加锁。同时,这些记录在主键索引上的记录,也会被加锁。
还记得前面提到的MySQL的四种隔离级别的区别吗?RC隔离级别容许幻读,而RR隔离级别,不容许存在幻读。可是在组合5、组合六中,加锁行为又是与RC下的加锁行为彻底一致。那么RR隔离级别下,
组合七,Repeatable Read隔离级别,id上有一个非惟一索引,执行delete from t1 where id = 10; 假设选择id列上的索引进行条件过滤,最后的加锁行为,是怎么样的呢?一样看下图1:
结论:Repeatable Read隔离级别下,id列上有一个非惟一索引,对应SQL:delete from t1 where id = 10; 首先,经过id索引定位到第一条知足查询条件的记录,加记录上的X锁,加GAP上的GAP锁,而后加主键聚簇索引上的记录X锁,而后返回;而后读取下一条,重复进行。直至进行到第一条不知足条件的记录[11,f],此时,不须要加记录X锁,可是仍旧须要加GAP锁,最后返回结束。
何时会取得gap lock或nextkey lock 这和隔离级别有关,只在REPEATABLE READ或以上的隔离级别下的特定操做才会取得gap lock或nextkey lock。
相对于前面三个组合,这是一个比较特殊的状况。id列上没有索引,where id = 10;这个过滤条件,无法经过索引进行过滤,那么只能走全表扫描作过滤。对应于这个组合,SQL会加什么锁?或者是换句话说,全表扫描时,会加什么锁?这个答案也有不少:有人说会在表上加X锁;有人说会将聚簇索引上,选择出来的id = 10;的记录加上X锁。那么实际状况呢?请看下图2:
因为id列上没有索引,所以只能走聚簇索引,进行所有扫描。从图中能够看到,知足删除条件的记录有两条,可是,聚簇索引上全部的记录,都被加上了X锁。不管记录是否知足条件,所有被加上X锁。既不是加表锁,也不是在知足条件的记录上加行锁。
有人可能会问?为何不是只在知足条件的记录上加锁呢?这是因为MySQL的实现决定的。若是一个条件没法经过索引快速过滤,那么存储引擎层面就会将全部记录加锁后返回,而后由MySQL Server层进行过滤。所以也就把全部的记录,都锁上了。
结论:若id列上没有索引,SQL会走聚簇索引的全扫描进行过滤,因为过滤是由MySQL Server层面进行的。所以每条记录,不管是否知足条件,都会被加上X锁。可是,为了效率考量,MySQL作了优化,对于不知足条件的记录,会在判断后放锁,最终持有的,是知足条件的记录上的锁,可是不知足条件的记录上的加锁/放锁动做不会省略。同时,优化也违背了2PL的约束。
组合八,Repeatable Read隔离级别下的最后一种状况,id列上没有索引。此时SQL:delete from t1 where id = 10; 没有其余的路径能够选择,只能进行全表扫描。最终的加锁状况,图3所示:
结论:在Repeatable Read隔离级别下,若是进行全表扫描的当前读,那么会锁上表中的全部记录,同时会锁上聚簇索引内的全部GAP,杜绝全部的并发 更新/删除/插入 操做。固然,也能够经过触发semi-consistent read,来缓解加锁开销与并发影响,可是semi-consistent read自己也会带来其余问题,不建议使用。
上面的四个组合,都是在Read Committed隔离级别下的加锁行为,接下来的四个组合,是在Repeatable Read隔离级别下的加锁行为。
组合五,id列是主键列,Repeatable Read隔离级别,针对delete from t1 where id = 10; 这条SQL,加锁与组合一:[id主键,Read Committed]一致。
与组合五相似,组合六的加锁,与组合二:[id惟一索引,Read Committed]一致。两个X锁,id惟一索引知足条件的记录上一个,对应的聚簇索引上的记录一个。
针对前面提到的简单的SQL,最后一个状况:Serializable隔离级别。对于SQL2:delete from t1 where id = 10; 来讲,Serializable隔离级别与Repeatable Read隔离级别彻底一致,所以不作介绍。
Serializable隔离级别,影响的是SQL1:select * from t1 where id = 10; 这条SQL,在RC,RR隔离级别下,都是快照读,不加锁。可是在Serializable隔离级别,SQL1会加读锁,也就是说快照读不复存在,MVCC并发控制降级为Lock-Based CC。
结论:在MySQL/InnoDB中,所谓的读不加锁,并不适用于全部的状况,而是隔离级别相关的。Serializable隔离级别,读不加锁就再也不成立,全部的读操做,都是当前读。
这种状况很好理解,事务A和事务B操做两张表,但出现循环等待锁状况。
这种状况比较常见,以前遇到两个job在执行数据批量更新时,jobA处理的的id列表为[1,2,3,4],而job处理的id列表为[8,9,10,4,2],这样就形成了死锁。
这种状况比较隐晦,事务A在执行时,除了在二级索引加锁外,还会在聚簇索引上加锁,在聚簇索引上加锁的顺序是[1,4,2,3,5],而事务B执行时,只在聚簇索引上加锁,加锁顺序是[1,2,3,4,5],这样就形成了死锁的可能性。
innodb在RR级别下,以下的状况也会产生死锁,比较隐晦。不清楚的同窗能够自行根据上节的gap锁原理分析下。
结论:在MySQL/InnoDB中,所谓的读不加锁,并不适用于全部的状况,而是隔离级别相关的。Serializable隔离级别,读不加锁就再也不成立,全部的读操做,都是当前读。
尽可能使用较低的隔离级别;
精心设计索引, 并尽可能使用索引访问数据, 使加锁更精确, 从而减小锁冲突的机会
选择合理的事务大小,小事务发生锁冲突的概率也更小
给记录集显示加锁时,最好一次性请求足够级别的锁。好比要修改数据的话,最好直接申请排他锁,而不是先申请共享锁,修改时再请求排他锁,这样容易产生死锁
不一样的程序访问一组表时,应尽可能约定以相同的顺序访问各表,对一个表而言,尽量以固定的顺序存取表中的行。这样能够大大减小死锁的机会
尽可能用相等条件访问数据,这样能够避免间隙锁对并发插入的影响
不要申请超过实际须要的锁级别
除非必须,查询时不要显示加锁。 MySQL的MVCC能够实现事务中的查询不用加锁,优化事务性能;MVCC只在COMMITTED READ(读提交)和REPEATABLE READ(可重复读)两种隔离级别下工做
对于一些特定的事务,可使用表锁来提升处理速度或减小死锁的可能
相关连接:
MySQL锁总结 zhuanlan.zhihu.com/p/29150809
MySQL锁机制——你想知道的都在这了! zhuanlan.zhihu.com/p/75673270
详解mysql的各类锁(表锁、行锁、共享锁、意向共享锁、记录锁、间隙锁、临键锁) zhuanlan.zhihu.com/p/52312376
对于MySQL你必需要了解的锁知识 zhuanlan.zhihu.com/p/62525459
mysql锁机制总结,以及优化建议 zhuanlan.zhihu.com/p/70889229
深刻理解MySQL――锁、事务与并发控制 这才是正确的! zhuanlan.zhihu.com/p/36060546
深刻理解MySQL锁 zhuanlan.zhihu.com/p/8355298
MySQL(Innodb)索引的原理 zhuanlan.zhihu.com/p/62018452
数据库两大神器【索引和锁】 zhuanlan.zhihu.com/p/40396971
深刻理解MySQL锁 zhuanlan.zhihu.com/p/83552985
原文:再谈mysql锁机制及原理-锁的诠释 - mysql - 周陆军的我的网站 修改更新只在原文,文有不妥之处,请留言告知。