MySQL锁机制

Posted EileenChang

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了MySQL锁机制相关的知识,希望对你有一定的参考价值。

文章目录


概述

  锁是计算机协调多个进程或线程并发访问某一资源的机制。在应用程序中,数据是多个用户共享的资源。如何保证数据并发访问的一致性、有效性是所有数据库必须解决的一个问题,锁冲突也是影响数据库并发访问性能的一个重要因素。在mysql中,锁和存储引擎是强相关的,不同的存储引擎有不同的锁机制。

1 锁的粒度

  从锁的粒度来说,mysql的锁分为行锁、页面锁和表锁。不同的存储引擎支持不同粒度的锁。

  • 表锁:开销小,加锁快,不会产生死锁;但是锁粒度大,发生锁冲突的概率高,并发度低。
  • 行锁:开销大,加锁慢,会产生死锁;优点是锁粒度小,发生冲突的概率低,并发度高。
  • 页面锁:开销和加锁时间介于表锁和行锁之间,也会产生死锁;锁的粒度介于表锁和行锁之间,并发度一般。

  InnoDB存储引擎既支持行锁,也支持表锁,它默认情况下是使用行锁。MyISAM和MEMORY存储引擎使用的是表锁。BDB存储引擎默认使用页面锁,同时也支持表锁。

2 锁的实现

2.1 MyISAM锁的实现

2.1.1 MySQL表锁

  MySQL的表级锁有两种模式:表共享读锁(Table Read Lock)表独占写锁(Table Write Lock)。对MyISAM表的读操作,不会阻塞其他用户对同一表的读请求,但会阻塞对同一表的写请求;对 MyISAM表的写操作,则会阻塞其他用户对同一表的读和写操作;MyISAM表的读操作与写操作之间,以及写操作之间是串行的!
  默认情况下,写锁比读锁具有更高的优先级。当一个锁释放时,这个锁会优先分配给写锁等待队列中的请求,然后再分配给读锁等待队列中的请求。这也正是MyISAM表不太适合于有大量更新操作和查询操作应用的原因,因为,大量的更新操作会造成查询操作很难获得读锁,从而可能永远阻塞。同时,一些慢查询会导致写操作阻塞,应用中应尽量避免出现慢查询操作。

2.1.2 MyISAM加锁方式

  • 自动加锁:MyISAM在执行查询语句前,会自动给涉及的表加共享读锁,在执行更新操作前,会自动给涉及的表加独占写锁,这个过程并不需要用户干预。
  • 显式加锁,除了自动加锁以外,用户还可以直接用LOCK TABLE命令给MyISAM 表显式加锁。

2.1.3 写阻塞读

Session ASession B
对表student加独占写锁:
lock table student write;
Session A对student表的读写操作均可执行:
select * from student;
update student set name = ‘John’ where id = 1;
Sesson B对student表的读操作被阻塞:
select * from student;
Session A释放锁:
unlock tables;
Session B被阻塞的查询可以继续执行并返回结果

2.1.4 读阻塞写

Session ASession B
对student表加共享读锁:
lock table student read;
Session A可以读取student表:
select * from student;
Session B可以读取student表:
select * from student;
Session A不能查询没有锁定的表:
select * from person;
报错:Table ‘person’ was not locked with LOCK TABLES
Session B可以查询或者更新未锁定的表
select * from person;
insert into person values(1, ‘Tom’);
Session A不能插入或者更新student表:
insert into student values(6, ‘Sunny’);
报错:Table ‘student’ was locked with a READ lock and can’t be updated
Session B插入student表会阻塞:
insert into student values(6, ‘Kaly’);

2.2 InnoDB锁的实现

2.2.1 InnoDB行锁

  • 共享锁(S):又称读锁,允许一个事务去读一行,阻止其他事务获得相同数据集的排他锁。若事务T对数据对象A加上S锁,则事务T可以读A但不能修改A,其他事务只能再对A加S锁,而不能加X锁,直到T释放A上的S锁。这保证了其他事务可以读A,但在T释放A上的S锁之前不能对A做任何修改。
  • 排他锁(X):又称写锁,允许获取排他锁的事务更新数据,阻止其他事务取得相同的数据集共享锁和排他锁。若事务T对数据对象A加上X锁,事务T可以读A也可以修改A,其他事务不能再对A加任何锁,直到T释放A上的锁。

2.2.2 行锁加锁方式

  • 对于更新语句,InnoDB引会自动给涉及到的数据加上排他锁。
  • select语句默认不会加任何锁类型。
  • 如果查询语句要加共享锁,可以使用select … lock in share mode语句,其他事务仍然可以查询记录(普通的select语句),并也可以对该记录加共享锁。但是如果当前事务需要对该记录进行更新操作,则很有可能造成死锁。共享锁可以确保当前事务查到的数据是最新的数据,并且不允许其他事务来修改数据。但是自己不一定能够修改数据,因为有可能其他的事务也对这些数据加了S锁。
  • 如果查询语句要加排他锁,可以使用select … for update语句,加共享锁可以使用select … lock in share mode语句。其他事务可以查询该记录(普通的select语句),但是不能对该记录加共享锁或排他锁,而是等待释放锁。排他锁确保当前事务查到的数据是最新数据,并且查到后的数据只允许自己来修改。

2.2.3 行锁实现方式

  • InnoDB 行锁是通过给索引上的索引项加锁来实现的,这一点MySQL与Oracle不同,后者是通过在数据块中对相应数据行加锁来实现的。InnoDB 这种行锁实现特点意味着,只有通过索引条件检索数据,InnoDB 才使用行级锁,否则将退化为表锁
  • 不论是使用主键索引、唯一索引或普通索引,InnoDB 都会使用行锁来对数据加锁。
  • 只有执行计划真正使用了索引,才能使用行锁。即便在条件中使用了索引字段,但是否使用索引来检索数据是由MySQL通过判断不同执行计划的代价来决定的。如果MySQL认为全表扫描效率更高,比如对一些很小的表,它就不会使用索引,这种情况下InnoDB将使用表锁,而不是行锁。因此,在分析锁冲突时,别忘了检查SQL的执行计划,以确认是否真正使用了索引。
  • 由于MySQL的行锁是针对索引加的锁,不是针对记录加的锁,所以即使多个事务访问不同行的记录,但是如果是使用相同的索引键,还是会出现锁冲突的。

2.2.4 意向锁

  为了允许行锁和表锁共存,实现多粒度锁机制,InnoDB还有两种内部使用的意向锁(Intention Locks),这两种意向锁都是表锁:

  • 意向共享锁(IS):事务打算给数据行加行共享锁,事务在给一个数据行加共享锁前必须先取得该表的IS锁。
  • 意向排他锁(IX):事务打算给数据行加行排他锁,事务在给一个数据行加排他锁前必须先取得该表的IX锁。

2.2.5 间隙锁(Gap Locks)

  间隙锁是指在某个索引项的两个索引值之间的数据集(where条件为between and)加的锁,或者是对某个索引值之前的数据集(where条件为<)或某个索引值之后的数据集(where条件为>)加的锁。比如,SELECT c1 FROM t WHERE c1 BETWEEN 10 and 20 FOR UPDATE,将会阻止其他事务将数据插入到c1列的10到20的范围内。

2.2.6 临键锁(Next-Key Locks)

  临键锁是行锁(Record Locks)以及“where条件为<”的间隙锁的结合。

2.2.7 自增锁(AUTO-INC Locks)

  自增锁是一种特殊的表锁,当一个事务使用自增属性插入表数据时,会使用自增锁。

3 死锁

3.1 MyISAM死锁

  在自动加锁的情况下,MyISAM总是一次获得SQL语句所需要的全部锁,所以MyISAM不会出现死锁。

3.2 InnoDB死锁

  避免死锁:

  • 为了在单个InnoDB表上执行多个并发写入操作时避免死锁,可以在事务开始时一次性获取所有需要修改的记录的锁。
  • 在事务中,如果要更新记录,应该直接申请足够级别的锁,即排他锁,而不应先申请共享锁、更新时再申请排他锁,因为这时候当用户再申请排他锁时,其他事务可能又已经获得了相同记录的共享锁,从而造成锁冲突,甚至死锁。
  • 如果事务需要修改或锁定多个表,则应在每个事务中以相同的顺序使用加锁语句。 在应用中,如果不同的程序会并发存取多个表,应尽量约定以相同的顺序来访问表,这样可以大大降低产生死锁的机会。
      如果出现死锁,可以用 SHOW INNODB STATUS 命令来确定最后一个死锁产生的原因。返回结果中包括死锁相关事务的详细信息,如引发死锁的 SQL 语句,事务已经获得的锁,正在等待什么锁,以及被回滚的事务等。据此可以分析死锁产生的原因和改进措施。

4 锁监控

4.1 表锁监控

  MyISAM或InnoDB可以通过检查table_locks_waited和table_locks_immediate状态变量来分析系统上的表锁定争夺,如果Table_locks_waited值很大,意味着存在比较严重的表锁争用情况。

mysql> show status like '%table_locks%';
+-----------------------+-------+
| Variable_name         | Value |
+-----------------------+-------+
| Table_locks_immediate | 100   |
| Table_locks_waited    | 0     |
+-----------------------+-------+
2 rows in set (0.00 sec)

4.2 行锁监控

  InnoDB可以通过检查InnoDB_row_lock状态变量来分析系统上的行锁的争夺情况,如果InnoDB_row_lock_waits和InnoDB_row_lock_time_avg的值比较高,说明行锁争用激烈。

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 锁优化

  • 尽量使用较低的隔离级别。
  • 精心设计索引,并尽量使用索引访问数据,使加锁更精确,从而减少锁冲突的机会。
  • 选择合理的事务大小,小事务发生锁冲突的几率也更小。
  • 给记录集显式加锁时,最好一次性请求足够级别的锁。比如要修改数据的话,最好直接申请排他锁,而不是先申请共享锁,修改时再请求排他锁,这样容易产生死锁。
  • 不同的程序访问一组表时,应尽量约定以相同的顺序访问各表,对一个表而言,尽可能以固定的顺序存取表中的行,这样可以大大减少死锁的机会。
  • 尽量用相等条件访问数据,这样可以避免间隙锁对并发插入的影响。
  • 不要申请超过实际需要的锁级别。
  • 除非必须,查询时不要显示加锁, MySQL的MVCC了实现普通查询不用加锁。

以上是关于MySQL锁机制的主要内容,如果未能解决你的问题,请参考以下文章

MySQL - 全局锁表级锁行级锁元数据锁自增锁意向锁共享锁独占锁记录锁间隙锁临键锁死锁

MySQL 进阶 锁 -- MySQL锁概述MySQL锁的分类:全局锁(数据备份)表级锁(表共享读锁表独占写锁元数据锁意向锁)行级锁(行锁间隙锁临键锁)

MySQL锁机制

MySQL锁机制

MySQL锁机制

mysql插入意向锁测试