[toc]
前言
锁是MySQL在服务器层和存储引擎的并发控制
加锁是消耗资源的,锁的各种操作,包括获得锁,检测锁是否已解除,释放锁等。
锁机制
MySQL不同的存储引擎支持不同的锁机制,所有的存储引擎都以自己的方式显现了锁机制,服务器层完全不了解存储引擎中的锁实现:
- MyISAM和MEMORY存储引擎采用的是表级锁(table-level locking)
- BDB存储引擎采用的是页面锁(page-level locking),但是也支持表级锁
- InnoDB存储印尼去即支持行级锁(row-level locking),也支持表级锁,但默认情况下是采用行级锁。
默认情况向下,表锁和行锁都是自动获得的,不需要额外的命令。
但是在有的情况下,用户需要明确进行锁表或者进行事务控制,以便确保整个事务的完整性,这样就需要使用事务控制和锁定语句来完成。
不同粒度锁的比较
- 表级锁:开销小,加锁块,不会出现死锁,锁粒度大,发生锁冲突的概率最高,并发度最低。
- 这些存储引擎通过总是一次性同时获取所有需要的锁已经总是按相同的顺序获取表锁来避免死锁
- 表锁更适合以查询为主,并发用户少,只有少量按索引条件更新数据的应用
- 行级锁:开销大,加锁慢,会出现死锁,锁粒度最小,发生锁冲突的概率最低,并发程度也是最高
- 最大程度下支持并发,同时也带来了最大的锁开销
- 在InnoDB中,除单个SQL组成的事务外,锁是逐步获得的,这就决定了在InnoDB中发生死锁的可能。
- 行级锁只在存储引擎层实现,而MySQL服务器层没有实现,行级锁更适合有大量按索引条件并发更新少量数据,同时又有并发查询的应用。
- 页面锁:开销和加锁时间介于表锁和行锁只在,会出现死锁,锁定的力度介于表锁和行锁之间,并发程度一般。
MyISAM表锁
MyISAM表级锁模式:
- 表共享锁(Table Read Lock):不会阻塞其他用户同一张表的读请求,但会阻塞对同一表写请求。
- 表独占锁(Table Write Lock):会阻塞其他用户对同一表的读写操作
MyISAM表的读操作与写操作之间,以及写操作之间是串行的,当一个线程获得对一个表的写锁后,只有持有锁的线程可以对表进行更新操作,其他线程的读,写操作都会等待,知道释放锁为止。
默认情况下,写锁比读锁具有更高的优先级:当一个锁释放时,这个锁会优先给写锁队列中等候的获取锁的请求,然后在给读锁队列中等待获取锁的请求
这也正是MyISAM表不适合有大量更新操作和查询操作的应用,因为大量的更新操作会造成查询操作很难获取锁,从而可能永远阻塞,同时,一些需要长时间运行的查询操作,也会使写线程饿死,应用中应尽量避免出现长时间查询操作(有可能的情况下可以通过使用中间表等措施对SQL语句做一定的分解,使每一步查询都能在较短时间完成,从而减少锁冲突,如果复杂查询不可避免,应尽量安排在数据库空闲时间段执行,比如一些定期统计可以安排在夜间执行)。
MyISAM加锁方法
MyISAM在执行查询语句(SELECT)前,会自动给涉及的表加读锁,在执行更新操作(UPDATE、DELETE、INSERT)前,会自动给涉及的表加写锁,这个过程并不需要用户的干预,因此用户一般不需要直接使用LOCK TABLE命令给MyISAM表显示加锁。
在自动加锁的情况下,MyISAM总是一次获得SQL语句需要的全部锁,这也正是MyISAM表不会出现死锁的原因。
MyISAM存储引擎支持并发插入,以减少给定表的读和写操作之间的争用:
如果MyISAM表在数据文件中间没有空缺块,则行始终插入数据文件插入数据文件的末尾,在这种情况下,你 可以自由混合并发使用MyISAM表的INSERT和SELECT语句而不需要加锁可以在其他线程进行读操作,同时将行插入到MyISAM表中,文件中间有空闲块可能是从表格中间删除或更新的行产生的,如果文件中间有空缺块,则并发插入会被禁用,但是当所有空闲块都填充有新数据时,他会自动重新启用,要控制此行为,可以使用MySQL的concurrent_insert系统变量。
如果使用LOCK TABLES显示获取表锁,则可以请求READ LOCAL锁而不是READ锁,以便在锁定表时,其他会话可以使用并发插入。
- 当concurrent_insert设置为0时,不允许并发插入
- 当concurretn_insert设置为1时,如果MyISAM表中没有空洞(即表中间没有被删除的行),MyISAM运行在一个线程读表的同时,另一个线程从表尾插入记录,这也是MySQL的默认设置
- 当concurrent_insert设置为2时,无论MyISAM有没有空洞,都允许在表尾并发插入记录。
查询表级锁争用情况
可以通过检查table_locks_waited和tabl_locks_immediate状态变量来分析系统上的表锁的争夺,如果table_locks_waited的值比较高,则说明存在这比较严重的表级锁争用情况。
InnoDB行级锁和表级锁
InnoDB锁模式
InnoDB实现了以下两种类型的行锁:
- 共享锁(S):允许一个事务去读一行,阻止其他事务获得相同数据集的排它锁
- 排它锁(X):允许获得排它锁的事务更新数据,阻止其他事务取得相同数据集的共享读锁和排它写锁
为了允许行锁和表锁共存,实现多粒度锁机制,InnoDB还有两种内部使用的意向锁(Intentionn Locks),这两种意向锁都是表锁:
- 意向共享锁(IS):事务打算给数据行加行共享锁,事务在给一个数据行加共享锁前必须先取得该表的IS锁
- 意向排它锁(IX):事务打算给数据行加行排它锁,事务在给一个数据行加排它锁必须先获取该表的IX锁。
锁模式的兼容情况:
如果一个事务请求的锁模式与当前的锁兼容,InnoDB就将请求的锁授予该事务;反之如果两者不兼容,该事物就要等待锁释放。
InnoDB加锁方法
- 意向锁是InnoDB自动加的,不需要用户干预
- 对于UPDATE、DELETE和INSERT语句,InnoDB会自动给涉及数据集加排它锁(X)
- 对于普通SELETE语句InnoDB不会加任何锁,事务可以通过以下语句显示给记录集添加共享锁或排它锁:
- 共享锁(S):
select * from table_name where ... LOCK IN SHARE
MODE。其他事务仍然可以查询记录,并也可以对该记录加share mode的共享锁,但是如果当前事务需要对该记录进行更新操作,则很有可能造成死锁。 - 排它锁(X):
select * from table_name where ... for update
。其他事务可以查询该记录,但是不能对该记录加共享锁和排它锁,而是等待获的锁。
- 共享锁(S):
- 隐式锁定
InnoDB在事务执行过程中,使用两种段锁协议:
随时都可以执行锁定,InnoDB会根据隔离级别在需要的时候自动加锁。
锁只有在执行commit或者rollback的时候才会释放,并且所有锁同一时刻释放
- 显式锁定:
select ... lock in share mode //共享锁
select ... for update //排它锁
select for update:在执行这个select查询语句的时候,会将对应的索引访问条目加上排它锁(X锁),也就是说这个语句对应的锁就相当于update带来的效果。
所使用场景:为了让自己查到的数据确保是最新数据,并且查到后的数据只允许自己来修改的时候,需要用到for update字句。
select lock in share mode:inshare mode子句的作用就是将查找到的数据加上一个share锁,这个就是表示其他事务只能对这些数据进行简单的select操作,并不能进行DML操作。
使用场景:为了确保自己查到的数据没用被其他的事务正在修改,也就是说确保查到是最新数据,并不允许其他人修改数据,但是自己不一定能够修改数据,因为有可能其他事物也对这些数据使用了in share mode当方式上了S锁。
性能影响:select for update语句,相当于一个update语句,在业务繁忙的时候,如果事务没有及时commit或者rollback可能会造成其他事务长时间的等待,从而数据库的并发使用效率。select lock in share mode语句时一个给查询数据上了一个共享锁(S锁)功能,它允许其他事务也对改数据上S锁,但是不能够允许对改数据进行修改,如果不及时commit或者rollback也可能造成大量的事务等待。
for update和lock in share mode区别:
前一个上的是排它锁(X锁),一旦一个事务获取了这个锁,其他事务是没法在这些数据上执行for update;后者是共享锁,多个事务可以同时对相同数据执行lock in share mode。
InnoDB行锁的实现方式
- InnoDB行锁是通过给索引上的索引项来实现的,这一点MySQL与Oracle不同,后者是通过在数据块对相应数据行加锁实现的,InnoDB这种行锁的实现特点意味着:只有通过索引条件检索数据,InnoDB才使用行级锁,否则,InnoDB将使用表锁。
- 不论是使用主键索引、唯一索引或者普通索引,InnoDB都会使用行锁来对数据加锁
- 只有执行计划真正使用了索引,才会使用行锁,即便在条件中使用了索引字段,但是否使用索引来检索孙书记是由MySQL通过判断不同的执行计划的代价来决定的,如果MySQL认为全表扫描效率更高,比如对一些很小的表,就不会使用索引,这种情况下InnoDB将使用表锁,而不是行锁,因此分析所冲突时,别忘了检查SQL的执行计划(可以通过explain检查SQL的执行计划),以确认是否真正使用了索引
- 由于MySQL的行锁是针对索引加锁,不是针对记录加锁,所以虽然多个session是访问不同行的记录,但是如果是使用相同的索引建,会出现锁冲突(后使用这些索引的session需要等待先使用索引的session释放锁后,才能获取锁),应用设计的时候要注意这一点。
InnoDB的间隙锁
当我们范围条件而不是相等条件检索输送机,并请求共享或排它锁时,InnoDB会给符合条件的已有数据记录的索引项加锁;对于键值在条件范围内但不存在的记录,叫做间隙(GAP),InnoDB也会对这个间隙加锁,这种锁机制就是所谓的间隙锁(Next-Key锁)。
很显然,在使用范围条件检索并锁定记录时,InnoDB这种锁机制会阻塞符合条件范围内键值的并发插入,这往往会造成严重的锁等待,因此,在实际开发中,尤其是并发插入比较多的应用,我们要尽量优化业务逻辑,尽量使用相等条件来访问更新呢数据,避免使用范围条件。
InnoDB使用间隙锁的目的:
- 防止幻读,以满足相关隔离级别的要求
- 满足恢复和复制的需要
MySQL通过BINLOG录入执行成功的INSERT、UPDATE、DELETE等更新数据的SQL语句,并由次实现数据库的恢复和主从复制。MySQL的恢复机制(复制其实就是在Slave MySQL不断做基于BINLOG恢复)有以下特点:
- MySQL的恢复是SQL语句级的,也就是重新执行BINLOG的中的SQL语句
- MySQL的BINLOG是按照事务提交的先后顺序记录的,恢复也是按这个顺序执行的。
由此可见,MySQL的恢复机制的要求,在一个事务未体检前,其他并发事务不能插入满足其锁定条件的任何记录,也就是不允许出现幻读。
InnoDB在不同隔离级别下的一致性读及锁的差异:
锁和多版本数据(MVCC)是InnoDB实现一致性读和ISO/ANSI SQL92隔离级别的手段。
因此在不同的隔离级别下,InnoDB处理SQL时采用的一致性读策略和需要的锁是不同的:
对于许多SQL,隔离级别越高,InnoDB给记录集加的锁就越严格(尤其是使用范围条件的时候),产生锁冲突的可能性也就越高,从而对并发性事务处理性能的影响也就越大。
因此,我们在应用中,应该尽量使用较低的隔离级别,以减少锁竞争的几率。
LOCK TABLES 和UNLOCK TABLES
MySQL也支持lock tables和un lock tables,这都是在服务器层(MySQL Service层)实现的,和存储引擎无关,他们有自己的用途,并不能替代事务的处理。
- LOCK TABLES可以锁定用于当前线程的表,如果表被其他线程锁定,则当前线程会等待,直到可以获取所有锁定为止。
- UNLOCK TABLES可以释放当前线程获得的任何锁定,当前线程执行另一个LOCK TABLES时,或当与服务器的链接被关闭时,所有由当前线程锁定的表被隐含的解锁。
LOCK TABLES语法:
- 在用LOCK TABLES对InnoDB表加锁时要注意,要将AUTOCOMMIT设为0,否则MYSQL不会给表加锁
- 事务结束前,不要用UNLOCK TABLES释放表锁,因为UNLOCK TABLES隐含的提交事务。
- COMMIT或ROLLBACK并不能释放用LOCK TABLES加的表级锁,必须用UNLOCK TABLES释放表级锁。
正确的方式见如下语句:
例如,如果需要写表t1并从表t读,可以按照如下去做:
set autocommit=0;
lock tables t1 where ,t2 read,...;
[do something with tables t1 and t2 here];
commit;
unlock tables;
使用LOCK TABLES的场景
给表显示加表级锁(InnoDB表和MyISAM都可以),一般为了在一定程度模拟事务操作,实现对某一时间点多个表的一致性读取。(与MyISAM默认的表锁行为类似)
在用LOCK TABLES给表显示加表锁时,必须同时获取所有涉及到表的锁,并且MySQL不支持锁升级,也就是说,在执行LOCK TABLES后,只能访问显示加锁的这些表,不能访问未加锁的表;童年故事如果加的是读锁,那么只能执行查询操作,而不能执行更新操作。
其实,在MyISAM自动加锁(表锁)的情况下也大致如此,MyISAM总是一次获得SQLy语句所需要的全部锁,这正是MyISAM表不会出现死锁的原因。
死锁
- 死锁产生:
- 死锁是指两个或多个事务在同一资源上相互占用,并请求锁定对方占用的资源,从而倒追恶性循环
- 当事务试图以不同顺序锁定资源时,就可能产生死锁,多个事务同时锁定同一个资源时也可能产生死锁
- 锁的行为和顺序和存储引擎相关,以同样的顺序执行语句,有些存储引擎会产生死锁有些不会,死锁的双重原因:真正的数据冲突,存储引擎的实现的方式。
- 检测死锁:数据库系统实现了各种死锁的检测和死锁的超时机制,InnoDB存储引擎检测到死锁的循环依赖并立即返回一个错误
- 死锁恢复:死锁发生以后,只有部分或完全回滚其中一个事务,才能打破死锁,InnoDB目标处理死锁的方法是,将持有最少行级排它锁的事务进行回滚,所以事务型应用程序在设计时必须考虑如果处理死锁,多数情况下只需要重新执行因死锁回滚的事务即可。
- 外部锁的死锁检测:发生死锁后,InnoDB一般都会自动检测到,并使一个事务释放锁并回退,另一个事务获得锁,继续完成事务,但涉及外部锁,或涉及表锁的情况下,InnoDB并不能完全自动检测到死锁,这需要通过设置锁等待超时参数innodb_lock_wait_timeout来解决
- 死锁影响性能:死锁会影响性能而不是会产生严重的错误,因为InnoDB会自动检测死锁状况并回滚其中一个受影响的事务,在高并发系统上,当许多线程等待同一个锁时,死锁检测可能导致速度变慢,有时当发生死锁时,禁用死锁检测(使用innodb_deadlock_detect配置选项)可能会更有效,这是可以依赖innodb_lock_wait_timeout设置进行事务回滚。
MyISAM避免死锁:
在自动加锁的情况下,MyISAM总是一次获得SQL语句所需要的全部锁,所以MyISAM表不会出现死锁。
InnoDB避免死锁
- 为了在当个InnoDB表上执行多个并发写入操作时避免死锁,可以在是事务开始时通过为预期要修改的每个行使用select for update语句来获取必要的锁,即使这些行的更改语句是在之后才执行的。
- 在事务中,如果要更新记录,应该直接申请足够级别的锁,即排它锁,而不应该先申请共享锁,更新是在在申请排它锁,因为这时候用户在申请排它锁时,其他事务可能又已经获得了相同记录的共享锁,从而造成锁冲突,甚至死锁
- 如果事务需要修改或锁定多个表,则应在每个事务中以相同的顺序使用加锁语句,在应用如果不同的程序会并发存取多个表,应尽量预定以相同的顺序来访问表,这样可以大大降低产生死锁的机会
- 通过select lock in share mode获取行的读锁后,如果当前事务需要对该记录进行更新操作,则很有可能造成死锁
- 改变事务的隔离级别
如果出现死锁,可以用show innodb status命令来确定最后一个死锁产生的原因,返回结果中包括死锁相关事务的详细信息,如果引发死锁的SQL语句,事务已经获得的锁,正在等待什么锁,以及被回滚的事务等,据此可以分析死锁产生的原因和改进措施。
一些优化锁性能的建议
- 尽量使用较低的隔离级别
- 精心设计索引,并尽量使用索引访问数据,使加锁更精确,从而减少锁冲突的机会
- 选择合理的事务大小,小事务发生所冲突的几率更小
- 给记录集显示加锁时,最后一次性请求足够的锁级别,比如要修改数据,最后直接申请排它锁,额而不是先申请共享锁,修改时候在申请排它锁
- 不同程序访问一组表时,应尽量约定以相同的顺序访问各表,对一个表而言,尽可能以固定的顺序存取表中的行,这样可以大大减少死锁的机会
- 尽量用相等条件访问数据,这样可以避免间隙锁对并发插入的影响。
- 不要申请超过时间需要的锁级别
- 除非必须,查询时不要显示加锁,MySQL的MVCC可以实现事务中查询不用加速搜,优化事务性能,MVCC只在commited read(读提交),和repeatable read(可重复度)两种隔离级别下工作
- 对于一些特定的事务,可以使用表锁来提高处理速度或减少死锁的可能。
乐观锁和悲观锁
乐观锁
乐观苏:假设不会发生并发冲突,只在提交操作时检查是否违反数据完整性,乐观锁不能解决脏读问题。
乐观锁,顾名思义,就是很乐观,每次去拿数据的时候都会认为别人不好修改数据,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,可以使用版本号的机制,乐观锁适用于读多的应用,这样可以提供吞吐量,像数据库如果提供类似于write_condition机制的其实都是提供乐观锁。
悲观锁
假定会发生并发冲突,屏蔽一切可能违反数据完整性的操作。
悲观锁,就是很悲观,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会加上锁,这样别人想拿这个数据就会block直到拿到锁,传统的关系数据库里面就用到了这种锁机制,比如行锁,表锁,读锁,写锁等,都是在操作之前先上锁。