MySQL的锁
1、MySQL的锁的基本介绍
锁是计算机协调多个进程或线程并发访问某一资源的机制。在数据库中,除传统的计算资源(如CPU、RAM、I/O等)的争用以外,数据也是一种供许多用户共享的资源。如何保证数据并发访问的一致性、有效性是所有数据库必须解决的一个问题,锁冲突也是影响数据库并发访问性能的一个重要因素。从这个角度来说,锁对数据库而言显得尤其重要,也更加复杂。
相对其他数据库而言,MySQL的锁机制比较简单,其最显著的特点是不同的存储引擎支持不同的锁机制。
(InnoDB 存储引擎(MySQL 默认存储引擎,默认可重复读隔离级别)中)数据库事务的锁是执行某语句时就会加锁,而且只有当整个事务完成(提交或回滚)后,锁(排他行锁、共享行锁等等)才会被释放。例如,有一个事务 T1,在其中执行了UPDATE users SET user_email = 'new_email@example.com' WHERE user_id = 1001
。当这条UPDATE
语句执行后,排他行锁就会锁住user_id = 1001
对应的行。即使UPDATE
语句执行完成,只要事务 T1 还没有提交或者回滚,这个排他行锁就会一直存在。
1.1、数据库锁的分类
MySQL 的锁按照范围可以分为全局锁(对整个数据库实例进行加锁)、表锁、行锁,其中行锁是由数据库引擎实现的,并不是所有的引擎都提供行锁,MyISAM 就不支持行锁。
从对数据操作的类型:
- 共享锁(读锁):针对同一行数据,多个读操作可以同时进行而不会互相影响,但会阻塞写操作。
- 排它锁(写锁):当前写操作没有完成前,它会阻断其他会话的写和读。
共享锁会阻塞涉及到的数据的写,但是不会堵塞读;而排它锁则会把涉及到的数据的读和写都堵塞。也就是共享锁和排它锁都会阻塞涉及到的数据的写操作。
从对数据操作的粒度分:
- 表级锁:开销小,加锁快;锁定粒度大,发生锁冲突的概率最高,并发度最低。
- 行级锁:开销大,加锁慢;锁定粒度最小,发生锁冲突的概率最低,并发度也最高。
- 页面锁:开销和加锁时间界于表锁和行锁之间;锁定粒度界于表锁和行锁之间,并发度一般
2、表级锁(共享锁、排它锁)
2.1、表级锁的分类
2.1.1、共享表锁(读锁)
假设有会话 A 和 会话 B,会话 A 对 mylock 表加了读锁,则:
- 会话A :可以查该表的数据,但是无法增删改该表的数据,并且也无法查询其他表的数据,即使其他表未被锁定也无法查询,直到对该锁表进行释放锁。
- 会话B :可以查询该锁表的数据,也可以查询、增删改其他表的数据。但是在对已锁的数据进行增删改时,会一直阻塞,导致SQL无法执行结束,直到该表锁被释放后,SQL会自动执行结束。
已锁定表 | 其他表 | ||
会话A(加锁的会话) | 可查;但无法增删改 | 无法增删改查(会报错) | |
其他会话 | 可查;但无法增删改(会一直阻塞) | 可增删改查 |
也就是加共享锁的会话可以查锁表数据,但加锁会话无法增删改该表,也无法查询其他表。其他的会话可以查询任何表的数据,但其他会话对锁表进行增删改时也会阻塞,直到锁表的锁被释放。
总结:共享锁会阻塞写,不会阻塞读。
2.1.2、排它表锁(写锁)
假设有会话 A 和 会话 B,会话 A 对 mylock 表加了写锁,则:
- 会话A :可以查该表的数据,也可以对该锁定表进行增删改,但是无法查询其他表的数据,即使其他表未被锁定也无法查询,直到对已锁定表进行释放锁。
- 会话B :对已锁定表进行增删改查都会一直阻塞,直到该表锁被释放。可以对其他表进行增删改查。
已锁定表 | 其他表 | |
会话A(加锁的会话) | 可增删改查 | 无法增删改查(会报错) |
其他会话 | 无法增删改查(会一直阻塞) | 可增删改查 |
也就是加排它锁的会话可以增删改查锁定表的数据,但加锁会话无法查询其他表。其他的会话对锁定表进行增删改查时会一直阻塞,直到锁表的锁被释放,但可以增删改查其他表的数据。
总结:写锁会阻塞其他会话的读和写。
2.2、表级锁之MyISAM 引擎表的读写操作和锁
MyISAM 存储引擎在执行查询语句(select)时,会自动给涉及的所有表都加上共享锁,在执行增删改操作前,会自动给涉及到的所有表都加上排它锁。
所以对 MyISAM 表进行操作,会有以下情况:
- 对 MyISAM 表进行读操作时(会自动加共享锁):不会阻塞其他进程对同一表的读请求,但会阻塞对同一表的写请求。只有当读锁释放后,才会执行其它进程的写操作。
- 对 MyISAM 表进行写操作时〈会自动加排它锁):会阻塞其他进程对同一表的读和写操作,只有当写锁释放后,才会执行其它进程对该表的读写操作。
MyISAM 的读写锁调度是写优先,这也是 MyISAM 不适合做写为主的表的引擎的原因。因为写操作会自动加写锁,加锁后其他线程就没办法做任何操作,大量的写操作会使查询很难得到锁,从而造成永远阻塞。
2.3、表级锁的相关SQL语句
2.3.1、查看被锁定的表
可以通过以下命令来查看哪些表被锁定:
show open tables;
结果示例如下:
结果说明如下:
- Database:含有该表的数据库
- Table:表名称
- In_use:表当前被查询使用的次数。如果该数为零,则表是打开的,即当前没有锁表。如果该列的值大于 0,则表示表正在被使用,有可能被锁住。
- Name_locked:表名称是否被锁定。名称锁定用于取消表或对表进行重命名等操作
在 MySQL 的 InnoDB 引擎中,information_schema.INNODB_LOCKS
表存储了当前 InnoDB 存储引擎中存在的锁信息。可以通过查询这个表来确定表是否被锁以及被何种锁锁住。
SELECT * FROM information_schema.INNODB_LOCKS
通过查询结果可以看到哪些表被锁住,并且可以看到锁的类型(如行锁、表锁)、锁定的事务 ID 等信息。
2.3.2、给表加表级锁
可以使用以下命令给表加锁:
lock table 表名 read(write), 表名2 read(write), 其他;
-- 示例
lock table lock_table_test read, lock_table_test2 write;
示例:
先创建一个表 mylock:
create table mylock (
id int not null primary key auto_increment,
name varchar(20) default ''
) engine myisam;
insert into mylock(name) values('a');
insert into mylock(name) values('b');
insert into mylock(name) values('c');
insert into mylock(name) values('d');
insert into mylock(name) values('e');
然后给该表加读锁:
lock table mylock read;
使用 show open tables; 来查看表锁情况:
可以看到,mydbtest.mylock 表已被锁定。
2.3.3、释放表锁
释放所有表的锁:
unlock tables;
2.3.4、分析表锁
我们可以通过以下命令行来分析表锁的情况:
show status like 'table_locks%';
结果示例:
可以通过检查 table_locks_waited(主要分析该值) 和 table_locks_immediate 状态变量来分析系统上的表锁定,说明如下:
- table_locks_waited:出现表级锁定争用而发生等待的次数(也可以说是不能立即获取锁的次数,每触发一次需等待锁的SQL时该值就加1),此值高则说明存在着较严重的表级锁争用情况。
- table_locks_immediate:产生表级锁定的次数,表示可以立即获取锁的查询次数,每立即获取锁值加1。
3、行级锁
MyISAM 引擎支持表级锁,但不支持行级锁,InnoDB 引擎支持行级锁和表级锁。在默认的可重复读隔离级别下,InnoDB 会尽量使用行级锁(而不是表级锁)和 MVCC(多版本并发控制)机制来保证事务的隔离性。
在 MySQL 的 InnoDB 存储引擎中,行级锁的使用通常与索引密切相关。当通过索引条件来查询和修改数据时,InnoDB 更倾向于使用行级锁。这是因为索引能够帮助数据库快速定位到要操作的行,从而精确地对这些行加锁。例如,在一个employees
表中有employee_id
索引,当执行UPDATE employees SET salary = salary * 1.1 WHERE employee_id = 1001
这样的更新操作时,InnoDB 可以通过employee_id
索引快速找到employee_id = 1001
对应的行,并对该行加排他行锁(X 锁),其他事务对这一行的读写操作(不符合 MVCC 机制下的可读条件)就会被阻塞。
当查询或修改数据的条件没有使用索引时,InnoDB 也有可能会升级为表锁。这是因为如果没有索引来定位行,为了保证操作的一致性和正确性,数据库可能会选择更粗粒度的锁,即表锁。例如,在employees
表中,如果执行UPDATE employees SET department = 'New Dept' WHERE age > 30
,假设age
列没有索引,InnoDB 可能会对整个employees
表加表锁。这样做的原因是,在没有索引的情况下,数据库难以确定哪些行满足条件,通过加表锁可以防止其他事务对表进行不兼容的操作,确保当前事务能够正确地完成更新操作。不过,这种行为也可能因 MySQL 的版本、配置参数以及具体的操作场景而有所不同。
示例:
下面建一张表,并且建立索引,因为只有通过索引来检索数据,InnoDB 才会使用行级表。
CREATE TABLE test_lock (a INT(11),b VARCHAR(16))ENGINE=INNODB; INSERT INTO test_lock VALUES(1,'b2'); INSERT INTO test_lock VALUES(3,'3'); INSERT INTO test_lock VALUES(4, '4000'); INSERT INTO test_lock VALUES(5,'5000'); INSERT INTO test_lock VALUES(6, '6000'); INSERT INTO test_lock VALUES(7,'7000'); INSERT INTO test_lock VALUES(8, '8000'); INSERT INTO test_lock VALUES(9,'9000'); INSERT INTO test_lock VALUES(1,'b1'); -- 建立索引列 CREATE INDEX test_lock_a_ind ON test_lock(a); CREATE INDEX test_lock_b_ind ON test_lock(b);
建立两个会话,并且设置不自动提交事务(这样才能模拟出在一个事务内的情况,否则事务立马提交就看不出效果了),先在 A 会话中对某一行数据进行修改但不提交,然后在 B 会话中也对同一行数据进行修改,如下:
此时,会话 B 的修改会一直阻塞,直到 A 提交事务 commit,会话 B 的进程才能执行成功,否则会一直阻塞直到超时报错,这就是加了行级锁。
当然,如果会话 B 修改其他行的数据则是不会受到影响的,因为行级锁只会锁定指定行的数据。而且如果只是查询则没问题,比如A只是查询,则不会锁行,或者A修改B查询,则B的查询不会受到影响。
3.1、行级锁的分类
定义与原理:共享行锁用于对一行数据进行读取操作时的并发控制。当一个事务对某一行数据加上共享行锁后,允许其他事务也对这一行加共享行锁来读取数据。这是基于并发读取数据的需求,多个事务可以同时读取同一行数据而不会相互干扰,只要它们都只进行读取操作。
应用场景和示例:以一个在线图书阅读平台为例,多个用户可能同时查看某一本书的详细信息(如书名、作者、简介等)。假设这些信息存储在books
表中的一行数据中,当用户 A 的事务对这行数据加共享行锁进行读取时,用户 B 的事务也可以对同一行数据加共享行锁来读取相同的信息。这样可以满足多个用户同时获取图书信息的需求,并且在没有写操作介入时,不会产生冲突。但是,如果有一个事务想要对这行数据进行修改(如更新图书简介),则需要获取排他行锁,此时该事务会被阻塞,直到所有的共享行锁都被释放。
- 排他行锁(Exclusive Row Lock,X)
定义与原理:排他行锁用于对一行数据进行写操作(如插入、更新、删除)时的并发控制。当一个事务对某一行数据加上排他行锁后,只有该事务能够对这一行进行读写操作,其他事务不能对这一行加共享行锁进行读取,也不能加排他行锁进行写入,直到持有排他行锁的事务完成并释放锁。这种锁机制确保了在对数据进行修改时的独占性,防止其他事务对正在修改的数据进行干扰,从而保证数据的准确性和完整性。
应用场景和示例:在一个银行账户管理系统中,当一个事务需要更新某个账户的余额时,它会对存储该账户余额的行数据加上排他行锁。例如,事务 A 要从账户 1 向账户 2 转账,事务 A 会对账户 1 和账户 2 余额所在的行分别加上排他行锁。在事务 A 完成转账操作(包括更新两个账户的余额并提交事务)之前,其他事务(如同时进行的另一个转账事务或者查询账户余额的事务)对这两个账户余额行的读写操作都会被阻塞。这样可以确保转账操作的原子性,即要么全部完成,要么全部不做,避免数据不一致的情况出现。- 意向共享行锁(Intention Shared Row Lock,IS)
定义与原理:意向共享行锁是一种表级的锁意向表示,该锁是为了提高效率用的。当一个事务对表中的一行或多行加上共享行锁时,会在表级自动加上意向共享行锁。它的主要作用是为了提高数据库在处理更高层次(表级)锁冲突检查时的效率。通过在表级设置意向共享行锁,数据库在进行表级锁操作(如另一个事务试图对整个表加排他表锁)时,可以快速判断表内是否有行已经被加上共享行锁,而不需要逐行检查。
应用场景和示例:假设在一个电商系统的库存管理模块中,有一个事务需要查询多个商品的库存信息行。事务在对这些商品库存信息行加上共享行锁的同时,会在inventory
表级加上意向共享行锁。这样,当其他事务想要对整个inventory
表进行排他操作(如更新所有库存信息)时,数据库通过检查表级的意向共享行锁,就可以快速判断出表内有行正在被读取,从而避免了可能的锁冲突,提高了系统的并发性能和锁管理效率。
-
意向排他行锁(Intention Exclusive Row Lock,IX)
定义与原理:意向排他行锁同样是一种表级的锁意向表示,该锁是为了提高效率用的。当一个事务对表中的一行或多行加上排他行锁时,会在表级自动加上意向排他行锁。它的主要作用是在更高层次上表明表内有行正在进行排他操作,方便数据库快速进行锁冲突检查。当其他事务尝试对整个表进行不兼容操作(如加共享锁或排他锁)时,数据库可以通过检查表级的意向排他行锁及时发现并阻止,从而避免复杂的逐行检查过程。
应用场景和示例:在一个订单处理系统中,当事务 A 对订单表中的某一订单行进行更新操作(如修改订单状态)时,会对该订单行加上排他行锁,并在订单表级加上意向排他行锁。此时,如果另一个事务 B 想要对整个订单表进行读取操作(加共享锁)或者进行其他修改操作(加排他锁),数据库通过检查表级的意向排他行锁,就可以快速知道表内有行正在进行排他操作,从而避免了可能的锁冲突,确保数据的一致性和操作的正确性。
3.2、索引失效行锁变表锁
InnoDB 行锁是通过给索引上的索引项加锁来实现的,只有通过索引条件来查询修改数据(select xxx where 索引字段),InnoDB才使用行级锁,否则,InnoDB将使用表锁!行级锁都是基于索引的,如果一条SQL语句用不到索引是不会使用行级锁的,会使用表级锁。
比如,上面的例子当中,如果是修改 a 的值,并且条件不加引号,如条件语句为 where b = 1000,由于 b 是 varchar 类型,而此时不使用引号引起来会导致 mysql 底层自动进行类型转换,相当于发生了计算,由此索引会失效。当没有使用到索引时,此时 InnoDB 表不会使用行级锁,而是使用表锁。
如下:
上面 A 会话中由于 SQL 语句中的条件发生了类型转换,导致索引失效,行锁变表锁,所以 B 会话的修改会一直阻塞,直到 A 会话提交事务 B 的修改才能执行结束。
3.3、间隙锁(Next-Key锁)
当我们用范围条件而不是相等条件来修改数据,InnoDB 会给符合范围条件的所有数据记录的索引项加锁,那些值在条件范围内但并不存在在表里的数据,叫做“间隙(GAP)”,InnoDB 也会对这些 “间隙” 加锁,这种锁机制就是所谓的间隙锁(Next-Key锁)。可以理解是一个范围锁。
比如,A会话对某个范围条件的数据进行修改,但 A 还未提交事务,此时 B 会话想新增一条数据,但刚好这条数据符合 A 修改语句的条件范围,则 B 的新增会一直阻塞,因此此时 InnoDB 对这些范围数据加了间隙锁。
如下:
3.4、如何分析行锁
可通过以下命令查看数据库中行锁的情况:
show status like 'innodb_row_lock%';
说明如下:
- Innodb_row_lock_current_waits:当前正在等待锁定的数量
- Innodb_row_lock_time:从系统启动到现在锁定总时间长度。
- Innodb_row_lock_time_avg:每次等待所花平均时长。
- Innodb_row_lock_time_max:从系统启动到现在等待最长的一次所花的时间
- Innodb_row_lock_waits:系统启动后到现在总共等待的次数
当等待的次数很高,而且每次等待的时长也不小的时候,我们就需要分析系统中为什么会有如此多的等待,然后根据分析结果着手制定优化计划。
4、MySQL 默认事务的锁
mysql 默认事务隔离(Repeatable Read)下,增删改查操作对应的锁机制:
4.1、SELECT ... FROM
1)SELECT ... FROM ...
- 该语句是一个快照读,通过读取数据库的一个快照,不会加任何锁,除非将隔离级别设置成了 SERIALIZABLE 。
2)SELECT ... FROM ... LOCK IN SHARE MODE
- 如果使用到索引并且条件是范围,则在所有索引扫描范围的索引记录上加上共享的间隙锁(next key锁)。如果是唯一索引,只需要在相应记录上加排他行锁。这些被共享锁锁住的行无法进行update/delete。
- 允许其它事务对这些记录再加SHARE锁
3)SELECT ... FROM ... FOR UPDATE
- 如果使用到索引并且条件是范围,则在所有索引扫描范围的索引记录上加上排他的next key锁。如果是唯一索引,只需要在相应记录上加排他行锁
4.2、UPDATE ... WHERE ...
- 对要更新的行加上排他行锁,的next key锁。
- 如果涉及范围更新,则会对索引间隙加上间隙锁。
4.3、DELETE FROM ... WHERE ...
- 语句在所有索引扫描范围的索引记录上加上排他的next key锁。如果是唯一索引,只需要在相应记录上加排他行锁
4.4、INSERT
- 在插入的记录上加一把排他行锁。
总结:
隔离级别越高,越能保证数据的完整性和一致性,但是对并发性能的影响也越大,鱼和熊掌不可兼得啊。对于多数应用程序,可以优先考虑把数据库系统的隔离级别设为Read Committed,它能够避免脏读取,而且具有较好的并发性能。尽管它会导致不可重复读、幻读这些并发问题,在可能出现这类问题的个别场合,可以由应用程序采用悲观锁或乐观锁来控制。
可参考:https://www.cnblogs.com/frankyou/p/9603784.html
5、数据库的事务隔离和锁的关系
事务隔离级别是为了解决多并发时查到的数据不一致的问题,而锁是为了阻塞其他事务对同一数据进行修改。一个是为查,一个是为改。
6、多并发事务产生死锁
6.1、分析和示例
6.2、如何预防死锁