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的锁机制与PHP文件锁处理高并发简单思路

Mysql的锁机制与PHP文件锁处理高并发简单思路

mysql 锁机制

mysql锁机制详解

MySQL锁机制

MySQL InnoDB锁机制