MySQL锁机制
一、概述
锁是计算机协调多个进程或线程并发访问某一资源的机制。锁保证数据并发访问的一致性、有效性;锁冲突也是影响数据库并发访问性能的一个重要因素。锁是MySQL在服务器层和存储引擎层的的并发控制。
加锁是消耗资源的,锁的各种操作,包括获得锁、检测锁是否是否已解除、释放锁等。
二、锁机制
2.1 粒度锁
MySQL不同的存储引擎支持不同的锁机制,所有的存储引擎都以自己的方式显现了锁机制,服务器层完全不了解存储引擎中的锁实现:
MyISAM和MEMORY存储引擎采用的是表级锁(table-level locking)InnoDB存储引擎既支持行级锁(row-level locking),也支持表级锁,但默认情况下是采用行级锁。BDB存储引擎采用的是页面锁(page-level locking),但也支持表级锁
默认情况下,表锁和行锁都是自动获得的,不需要额外的命令。
但是在有的情况下,用户需要明确地进行锁表或者进行事务的控制,以便确保整个事务的完整性,这样就需要使用事务控制和锁定语句来完成。
2.2 不同粒度锁的比较
- 表级锁:指上锁的时候锁住整个表,当下一个事务访问该表的时候,必须等前一个事务释放了锁才能进行对表的访问。
特点:开销小,加锁快;不会出现死锁;锁定粒度大,发生锁冲突的概率最高,并发度最低。这些存储引擎通过总是一次性同时获取所有需要的锁以及总是按相同的顺序获取表锁来避免死锁。表级锁更适合于以查询为主,并发用户少,只有少量按索引条件更新数据的应用,如Web应用。 - 行级锁:上锁的时候锁住的是表的某一行或多行记录,其他事务访问同一张表时,只有被锁住的记录不能访问,其他的记录可正常访问。
特点:开销大,加锁慢;会出现死锁;锁定粒度最小,发生锁冲突的概率最低,并发度也最高。最大程度的支持并发,同时也带来了最大的锁开销。在InnoDB中,除单个SQL组成的事务外,锁是逐步获得的,这就决定了在InnoDB中发生死锁是可能的。行级锁只在存储引擎层实现,而Mysql服务器层没有实现。行级锁更适合于有大量按索引条件并发更新少量不同数据,同时又有并发查询的应用,如一些在线事务处理(OLTP)系统 - 页面锁:
mysql中锁定粒度介于行级锁和表级锁中间的一种锁。表级锁速度快,但冲突多,行级锁冲突少,但速度慢。所以取了折衷的页级锁,一次锁定相邻的一组记录。
特点:开销和加锁时间界于表锁和行锁之间;会出现死锁;锁定粒度界于表锁和行锁之间,并发度一般。
三、MyISAM表锁
3.1 表级锁模式
- 表共享读锁(
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就暂时将写请求的优先级降低,给读进程一定获得锁的机会。
3.2 加表锁方法
MyISAM在执行查询语句(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表中有没有空洞,都允许在表尾并发插入记录。
3.3 查询表级锁争用情况
可以通过检查table_locks_waited和table_locks_immediate状态变量来分析系统上的表锁的争夺,如果Table_locks_waited的值比较高,则说明存在着较严重的表级锁争用情况:
mysql> SHOW STATUS LIKE 'Table%';
+-----------------------+---------+
| Variable_name | Value |
+-----------------------+---------+
| Table_locks_immediate | 1151552 |
| Table_locks_waited | 15324 |
+-----------------------+---------+
四、InnoDB行锁和表锁
4.1 锁模式
InnoDB实现了以下两种类型的行锁 :
- 共享锁(
S):允许一个事务去读一行,阻止其他事务获得相同数据集的排他锁。 - 排他锁(
X):允许获得排他锁的事务更新数据,阻止其他事务取得相同数据集的共享读锁和排他写锁。
为了允许行锁和表锁共存,实现多粒度锁机制,InnoDB还有两种内部使用的意向锁(Intention Locks),这两种意向锁都是表锁:
- 意向共享锁(
IS):事务打算给数据行加行共享锁,事务在给一个数据行加共享锁前必须先取得该表的IS锁。 - 意向排他锁(
IX):事务打算给数据行加行排他锁,事务在给一个数据行加排他锁前必须先取得该表的IX锁。
锁模式的兼容情况:

(如果一个事务请求的锁模式与当前的锁兼容,InnoDB就将请求的锁授予该事务;反之,如果两者不兼容,该事务就要等待锁释放。)
4.2 加锁方法
4.2.1 加锁分类
- 对于普通
SELECT语句,InnoDB不会加任何锁; - 对于
UPDATE、DELETE和INSERT语句,InnoDB会自动给涉及数据集加排他锁(X); - 意向锁是
InnoDB自动加的,不需用户干预。 - 隐式锁定
InnoDB在事务执行过程中,使用两阶段锁协议:- 随时都可以执行锁定,
InnoDB会根据隔离级别在需要的时候自动加锁; - 锁只有在执行
commit或者rollback的时候才会释放,并且所有的锁都是在同一时刻被释放。
- 随时都可以执行锁定,
- 显式加锁
事务可以通过以下语句显式给记录集加共享锁或排他锁:- 共享锁(
S):SELECT * FROM table_name WHERE ... LOCK IN SHARE MODE。其他session仍然可以查询记录,并也可以对该记录加share mode的共享锁。但是如果当前事务需要对该记录进行更新操作,则很有可能造成死锁。 - 排他锁(
X):SELECT * FROM table_name WHERE ... FOR UPDATE。其他session可以查询该记录,但是不能对该记录加共享锁或排他锁,而是等待获得锁
- 共享锁(
4.2.2 显式锁定
select ... lock in share mode //共享锁
select ... for update //排他锁
select for update:在执行这个select查询语句的时候,会将对应的索引访问条目进行上排他锁(X锁),也就是说这个语句对应的锁就相当于update带来的效果。
select lock in share mode:in share mode子句的作用就是将查找到的数据加上一个share锁,这个就是表示其他的事务只能对这些数据进行简单的select操作,并不能够进行DML操作。
使用场景:
select for update:为了让自己查到的数据确保是最新数据,并且查到后的数据只允许自己来修改的时候,需要用到for update子句。
select lock in share mode:为了确保自己查到的数据没有被其他的事务正在修改,也就是说确保查到的数据是最新的数据,并且不允许其他人来修改数据。但是自己不一定能够修改数据,因为有可能其他的事务也对这些数据使用了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。
4.3 行锁实现方式
InnoDB行锁是通过给索引上的索引项加锁来实现的,这一点MySQL与Oracle不同,后者是通过在数据块中对相应数据行加锁来实现的。InnoDB这种行锁实现特点意味着:只有通过索引条件检索数据,InnoDB才使用行级锁,否则,InnoDB将使用表锁!- 不论是使用主键索引、唯一索引或普通索引,
InnoDB都会使用行锁来对数据加锁。 - 只有执行计划真正使用了索引,才能使用行锁:即便在条件中使用了索引字段,但是否使用索引来检索数据是由
MySQL通过判断不同执行计划的代价来决定的,如果MySQL认为全表扫描效率更高,比如对一些很小的表,它就不会使用索引,这种情况下InnoDB将使用表锁,而不是行锁。因此,在分析锁冲突时,别忘了检查SQL的执行计划(可以通过explain检查SQL的执行计划),以确认是否真正使用了索引。(更多阅读:MySQL索引总结) - 由于
MySQL的行锁是针对索引加的锁,不是针对记录加的锁,所以虽然多个session是访问不同行的记录,但是如果是使用相同的索引键,是会出现锁冲突的(后使用这些索引的session需要等待先使用索引的session释放锁后,才能获取锁)。应用设计的时候要注意这一点。
4.4 间隙锁
当我们用范围条件而不是相等条件检索数据,并请求共享或排他锁时,InnoDB会给符合条件的已有数据记录的索引项加锁;对于键值在条件范围内但并不存在的记录,叫做“间隙(GAP)”,InnoDB也会对这个“间隙”加锁,这种锁机制就是所谓的间隙锁(Next-Key锁)。
很显然,在使用范围条件检索并锁定记录时,InnoDB这种加锁机制会阻塞符合条件范围内键值的并发插入,这往往会造成严重的锁等待。因此,在实际应用开发中,尤其是并发插入比较多的应用,我们要尽量优化业务逻辑,尽量使用相等条件来访问更新数据,避免使用范围条件。
InnoDB使用间隙锁的目的:
- 防止幻读,以满足相关隔离级别的要求;
- 满足恢复和复制的需要:
MySQL通过BINLOG录入执行成功的INSERT、UPDATE、DELETE等更新数据的SQL语句,并由此实现MySQL数据库的恢复和主从复制。MySQL的恢复机制(复制其实就是在Slave Mysql不断做基于BINLOG的恢复)有以下特点:
MySQL的恢复是SQL语句级的,也就是重新执行BINLOG中的SQL语句。MySQL的Binlog是按照事务提交的先后顺序记录的,恢复也是按这个顺序进行的。
由此可见,MySQL的恢复机制要求:在一个事务未提交前,其他并发事务不能插入满足其锁定条件的任何记录,也就是不允许出现幻读。
4.5 不同隔离级别锁的差异
锁和多版本数据(MVCC)是InnoDB实现一致性读和ISO/ANSI SQL92隔离级别的手段。
因此,在不同的隔离级别下,InnoDB处理SQL时采用的一致性读策略和需要的锁是不同的:


对于许多SQL,隔离级别越高,InnoDB给记录集加的锁就越严格(尤其是使用范围条件的时候),产生锁冲突的可能性也就越高,从而对并发性事务处理性能的影响也就越大。
因此,我们在应用中,应该尽量使用较低的隔离级别,以减少锁争用的机率。实际上,通过优化事务逻辑,大部分应用使用Read Commited隔离级别就足够了。对于一些确实需要更高隔离级别的事务,可以通过在程序中动态改变隔离级别的方式满足需求。
set session transaction isolation level repeatable read
set session transaction isolation level serializable
4.6 获取行锁争用情况
可以通过检查InnoDB_row_lock状态变量来分析系统上的行锁的争夺情况:
mysql> show status like 'innodb_row_lock%';
+-------------------------------+-------+
| Variable_name | Value |
+-------------------------------+-------+
| InnoDB_row_lock_current_waits | 0 |
| InnoDB_row_lock_time | 0 |
| InnoDB_row_lock_time_avg | 0 |
| InnoDB_row_lock_time_max | 0 |
| InnoDB_row_lock_waits | 0 |
+-------------------------------+-------+
5 rows in set (0.01 sec)
五、表锁与全局锁
MySQL也支持lock tables和unlock tables,这都是在服务器层(MySQL Server层)实现的,和存储引擎无关,它们有自己的用途,并不能替代事务处理(除了禁用了autocommint后可以使用,其他情况不建议使用):
LOCK TABLES可以锁定用于当前线程的表。如果表被其他线程锁定,则当前线程会等待,直到可以获取所有锁定为止。UNLOCK TABLES可以释放当前线程获得的任何锁定。当前线程执行另一个LOCK TABLES时,或当与服务器的连接被关闭时,所有由当前线程锁定的表被隐含地解锁
5.1 表锁语法
- 在用
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;
5.2 使用表锁的场景
给表显示加表级锁(InnoDB表和MyISAM都可以),一般是为了在一定程度模拟事务操作,实现对某一时间点多个表的一致性读取。(与MyISAM默认的表锁行为类似)
在用LOCK TABLES给表显式加表锁时,必须同时取得所有涉及到表的锁,并且MySQL不支持锁升级。也就是说,在执行LOCK TABLES后,只能访问显式加锁的这些表,不能访问未加锁的表;同时,如果加的是读锁,那么只能执行查询操作,而不能执行更新操作。
其实,在MyISAM自动加锁(表锁)的情况下也大致如此,MyISAM总是一次获得SQL语句所需要的全部锁,这也正是MyISAM表不会出现死锁(Deadlock Free)的原因。
例如,有一个订单表orders,其中记录有各订单的总金额total,同时还有一个订单明细表order_detail,其中记录有各订单每一产品的金额小计subtotal,假设我们需要检查这两个表的金额合计是否相符,可能就需要执行如下两条SQL:
Select sum(total) from orders;
Select sum(subtotal) from order_detail;
这时,如果不先给两个表加锁,就可能产生错误的结果,因为第一条语句执行过程中,
order_detail 表可能已经发生了改变。因此,正确的方法应该是:
Lock tables orders read local, order_detail read local;
Select sum(total) from orders;
Select sum(subtotal) from order_detail;
Unlock tables;
(在LOCK TABLES时加了“local”选项,其作用就是允许当你持有表的读锁时,其他用户可以在满足MyISAM表并发插入条件的情况下,在表尾并发插入记录(MyISAM存储引擎支持“并发插入”))
5.3 全局锁
全局锁就是对整个数据库实例加锁,加上全局锁后,整个数据库将处于只读状态,后续的DML语句、DDL语句,以及更新操作的事务提交语句都将被阻塞
全局锁的典型的使用场景是做全库的数据备份,需要对所有的表进行锁定,从而获取一致性视图,保证数据的一致性和完整性
数据备份
接下来我们演示一下数据库的数据备份操作
第一步:获取全局锁
flush tables with read lock;
第二步:使用mysqldump工具做数据库的备份(注意,是在Linux终端中运行)
mysqldump -u root -p123456 > /tmp/blog.sql
运行指令后会有一个警告,因为我们将密码显式地展现出来了
mysqldump: [Warning] Using a password on the command line interface can be insecure.
第三步:释放全局锁
unlock tables;
使用全局锁造成的问题
数据库中加全局锁,是一个比较重的操作,存在以下问题:
- 如果在主库上备份,那么在备份期间都不能执行更新,业务基本上就得停止
- 如果在从库上备份,那么在备份期间从库不能执行主库同步过来的二进制日志(
binlog),会导致主从延迟现象的发生
在InnoDB引擎中,我们可以在备份时加上参数--single-transaction参数来完成不加全局锁的一致性数据备份
mysqldump --single-transaction -u root -p123456 > /tmp/blog.sql
六、乐观锁、悲观锁
- 乐观锁(
Optimistic Lock) :假设不会发生并发冲突,只在提交操作时检查是否违反数据完整性。乐观锁不能解决脏读的问题。
乐观锁,顾名思义,就是很乐观,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,可以使用版本号等机制。乐观锁适用于多读的应用类型,这样可以提高吞吐量,像数据库如果提供类似于write_condition机制的其实都是提供的乐观锁。
- 悲观锁(
Pessimistic Lock) :假定会发生并发冲突,屏蔽一切可能违反数据完整性的操作。
悲观锁,顾名思义,就是很悲观,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会block直到它拿到锁。传统的关系型数据库里边就用到了很多这种锁机制,比如行锁,表锁等,读锁,写锁等,都是在做操作之前先上锁。
七、优化锁性能的建议
- 尽量使用较低的隔离级别;
- 精心设计索引,并尽量使用索引访问数据,使加锁更精确,从而减少锁冲突的机会
- 选择合理的事务大小,小事务发生锁冲突的几率也更小
- 给记录集显示加锁时,最好一次性请求足够级别的锁。比如要修改数据的话,最好直接申请排他锁,而不是先申请共享锁,修改时再请求排他锁,这样容易产生死锁
- 不同的程序访问一组表时,应尽量约定以相同的顺序访问各表,对一个表而言,尽可能以固定的顺序存取表中的行。这样可以大大减少死锁的机会
- 尽量用相等条件访问数据,这样可以避免间隙锁对并发插入的影响
- 不要申请超过实际需要的锁级别
- 除非必须,查询时不要显示加锁。
MySQL的MVCC可以实现事务中的查询不用加锁,优化事务性能;MVCC只在COMMITTED READ(读提交)和REPEATABLE READ(可重复读)两种隔离级别下工作 - 对于一些特定的事务,可以使用表锁来提高处理速度或减少死锁的可能

浙公网安备 33010602011771号