MySQL锁机制
锁是计算机协调多个进程或线程并发访问某一资源的机制。锁保证数据并发访问的一致性、有效性;锁冲突也是影响数据库并发访问性能的一个重要因素。锁是MySQL
在服务器层和存储引擎层的的并发控制。
加锁是消耗资源的,锁的各种操作,包括获得锁、检测锁是否是否已解除、释放锁等。
锁机制
共享锁与排他锁
- 共享锁(读锁):简称
s锁
,当一个事务为数据加上读锁之后,其他事务只能对该数据加读锁,而不能对数据加写锁,直到所有的读锁释放之后其他事务才能对其进行加写锁。共享锁的特性主要是为了支持并发的读取数据,读取数据的时候不支持修改,避免出现重复读的问题 - 排他锁(写锁):简称
X锁
,当一个事务为数据加上写锁之后,**其他事务不能再为数据加任何锁,直到该锁释放之后,其他事务才能对数据进行加锁,排他锁的目的是在数据修改的时候,不允许其他事务修改,也不允许其他事务读取,避免了出现脏数据和脏读的问题。
粒度锁
MySQL
不同的存储引擎支持不同的锁机制,所有的存储引擎都以自己的方式显现了锁机制,服务器层完全不了解存储引擎中的锁实现:
MyISAM
和MEMORY
存储引擎采用的是表级锁(table-level locking
)InnoDB
存储引擎既支持行级锁(row-level locking
),也支持表级锁,但默认情况下是采用行级锁。BDB
存储引擎采用的是页面锁(page-level locking
),但也支持表级锁
默认情况下,表锁和行锁都是自动获得的,不需要额外的命令。
但是在有的情况下,用户需要明确地进行锁表或者进行事务的控制,以便确保整个事务的完整性,这样就需要使用事务控制和锁定语句来完成。
不同粒度锁的比较
- 表级锁:指上锁的时候锁住整个表,当下一个事务访问该表的时候,必须等前一个事务释放了锁才能进行对表的访问。
特点:开销小,加锁快;不会出现死锁;锁定粒度大,发生锁冲突的概率最高,并发度最低。这些存储引擎通过总是一次性同时获取所有需要的锁以及总是按相同的顺序获取表锁来避免死锁。表级锁更适合于以查询为主,并发用户少,只有少量按索引条件更新数据的应用,如Web
应用。 - 行级锁:上锁的时候锁住的是表的某一行或多行记录,其他事务访问同一张表时,只有被锁住的记录不能访问,其他的记录可正常访问。
特点:开销大,加锁慢;会出现死锁;锁定粒度最小,发生锁冲突的概率最低,并发度也最高。最大程度的支持并发,同时也带来了最大的锁开销。在InnoDB
中,除单个SQL
组成的事务外,锁是逐步获得的,这就决定了在InnoDB
中发生死锁是可能的。行级锁只在存储引擎层实现,而Mysql服务器层没有实现。行级锁更适合于有大量按索引条件并发更新少量不同数据,同时又有并发查询的应用,如一些在线事务处理(OLTP
)系统 - 页面锁:
mysql
中锁定粒度介于行级锁和表级锁中间的一种锁。表级锁速度快,但冲突多,行级锁冲突少,但速度慢。所以取了折衷的页级锁,一次锁定相邻的一组记录。
特点:开销和加锁时间界于表锁和行锁之间;会出现死锁;锁定粒度界于表锁和行锁之间,并发度一般。
MyISAM表锁
MyISAM表级锁模式
- 表共享读锁(
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
就暂时将写请求的优先级降低,给读进程一定获得锁的机会。
MyISAM加表锁方法
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
表中有没有空洞,都允许在表尾并发插入记录。
查询表级锁争用情况
可以通过检查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行级锁和表级锁
InnoDB锁模式
InnoDB
实现了以下两种类型的行锁 :
- 共享锁(
S
):允许一个事务去读一行,阻止其他事务获得相同数据集的排他锁。 - 排他锁(
X
):允许获得排他锁的事务更新数据,阻止其他事务取得相同数据集的共享读锁和排他写锁。
为了允许行锁和表锁共存,实现多粒度锁机制,InnoDB
还有两种内部使用的意向锁(Intention Locks
),这两种意向锁都是表锁:
- 意向共享锁(
IS
):事务打算给数据行加行共享锁,事务在给一个数据行加共享锁前必须先取得该表的IS
锁。 - 意向排他锁(
IX
):事务打算给数据行加行排他锁,事务在给一个数据行加排他锁前必须先取得该表的IX
锁。
锁模式的兼容情况:
(如果一个事务请求的锁模式与当前的锁兼容,InnoDB就将请求的锁授予该事务;反之,如果两者不兼容,该事务就要等待锁释放。)
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 for update:
在执行这个select
查询语句的时候,会将对应的索引访问条目进行上排他锁(X锁
),也就是说这个语句对应的锁就相当于update
带来的效果。
select *** for update的使用场景:为了让自己查到的数据确保是最新数据,并且查到后的数据只允许自己来修改的时候,需要用到for update
子句。
select lock in share mode : in share mode子句的作用就是将查找到的数据加上一个share锁,这个就是表示其他的事务只能对这些数据进行简单的select操作,并不能够进行DML操作。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。
InnoDB行锁实现方式
InnoDB
行锁是通过给索引上的索引项加锁来实现的,这一点MySQL
与Oracle
不同,后者是通过在数据块中对相应数据行加锁来实现的。InnoDB
这种行锁实现特点意味着:只有通过索引条件检索数据,InnoDB
才使用行级锁,否则,InnoDB
将使用表锁!- 不论是使用主键索引、唯一索引或普通索引,
InnoDB
都会使用行锁来对数据加锁。 - 只有执行计划真正使用了索引,才能使用行锁:即便在条件中使用了索引字段,但是否使用索引来检索数据是由MySQL通过判断不同执行计划的代价来决定的,如果MySQL认为全表扫描效率更高,比如对一些很小的表,它就不会使用索引,这种情况下InnoDB将使用表锁,而不是行锁。因此,在分析锁冲突时,
别忘了检查SQL
的执行计划(可以通过explain
检查SQL
的执行计划),以确认是否真正使用了索引。(更多阅读:MySQL索引总结) - 由于MySQL的行锁是针对索引加的锁,不是针对记录加的锁,所以虽然多个session是访问不同行的记录,但是如果是使用相同的索引键, 是会出现锁冲突的(后使用这些索引的session需要等待先使用索引的session释放锁后,才能获取锁)。 应用设计的时候要注意这一点。
InnoDB的间隙锁
当我们用范围条件而不是相等条件检索数据,并请求共享或排他锁时,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的恢复机制要求:在一个事务未提交前,其他并发事务不能插入满足其锁定条件的任何记录,也就是不允许出现幻读。
InnoDB在不同隔离级别下的一致性读及锁的差异
锁和多版本数据(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_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)
LOCK TABLES和UNLOCK TABLES
MySQL也支持lock tables和unlock tables,这都是在服务器层(MySQL Server层)实现的,和存储引擎无关,它们有自己的用途,并不能替代事务处理(除了禁用了autocommint后可以使用,其他情况不建议使用):
- 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 WRITE, 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
总是一次获得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
存储引擎支持“并发插入”))
死锁(Deadlock Free)
- 死锁产生:
- 死锁是指两个或多个事务在同一资源上相互占用,并请求锁定对方占用的资源,从而导致恶性循环。
- 当事务试图以不同的顺序锁定资源时,就可能产生死锁。多个事务同时锁定同一个资源时也可能会产生死锁。
- 锁的行为和顺序和存储引擎相关。以同样的顺序执行语句,有些存储引擎会产生死锁有些不会——死锁有双重原因:真正的数据冲突;存储引擎的实现方式。
- 检测死锁: 数据库系统实现了各种死锁检测和死锁超时的机制。
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只在COMMITTED READ(读提交)和REPEATABLE READ(可重复读)两种隔离级别下工作
- 对于一些特定的事务,可以使用表锁来提高处理速度或减少死锁的可能
乐观锁、悲观锁
- 乐观锁(Optimistic Lock) :假设不会发生并发冲突,只在提交操作时检查是否违反数据完整性。乐观锁不能解决脏读的问题。
乐观锁,顾名思义,就是很乐观,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,可以使用版本号等机制。乐观锁适用于多读的应用类型,这样可以提高吞吐量,像数据库如果提供类似于write_condition机制的其实都是提供的乐观锁。
- 悲观锁(Pessimistic Lock) :假定会发生并发冲突,屏蔽一切可能违反数据完整性的操作。
悲观锁,顾名思义,就是很悲观,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会block直到它拿到锁。传统的关系型数据库里边就用到了很多这种锁机制,比如行锁,表锁等,读锁,写锁等,都是在做操作之前先上锁。