19.高性能MySQL --- 锁的调试

原文链接: https://book.douban.com/subject/3766465/

 

1.服务器级别的锁等待
	锁等待可能发生在服务器级别或存储引擎级别。

	1.表锁
		表可以被显式的读锁和写锁进行锁定。这些锁有许多变种,例如本地读锁。除了这些显式的锁外,查询过程中还有隐式的锁。

		表锁既可以是显式的也可以是隐式的。显式的锁用 lock tables 创建。例如,如果在mysql会话中执行下列命令,将 sakila.film 上获取一个显式的
	  锁。
	  lock tables sakila.film read;//读锁,可以查询,但插入会挂起
	  show full processlist => State: Waiting for table level lock

	  lock tables sakila.film write;//写锁,查询就会挂起
	  show full processlist =>  State: Waiting for table metadata lock

	  在mysql 服务器代码中只有一个线程会进入此状态:当一个线程持有该锁后,其他线程只能不断的尝试获取。因此,如果看到这样的信息,你就知道线程等待一个
	mysql服务器中的锁,而不是存储引擎的。
	  
	  然后,显式锁并不是阻塞这样一个操作的唯一类型的锁。服务器在查询过程中会隐式的锁住表。用一个长时间运行的查询可以很容易的展示这一点,长时间的查询
	可以通过sleep()函数轻松创建。
	  select sleep(30) from a limit 1;
	  当这个查询运行时,如果你再次尝试锁 a,操作会因隐式锁而挂起,就如同显式锁一样。
	  lock tables a write;
	  show full processlist => State: Waiting for table metadata lock	
	  在本例中,select查询的隐式锁阻塞了 lock tables 中所请求的显式写锁。另外,隐式锁也会互相阻塞。
	  你可能想知道隐式锁和显式锁的差异。从内部来说,它们有相同的结构,由相同的mysql服务器代码来控制。从外部来说,你可以通过lock tables 和 unlock tables
    来控制显式锁。
      然而,当涉及非 MyISAM 存储引擎时,它们之间有一个非常重要的区别。当创建显式锁时,它会按照你的指令来做,但隐式锁就比较隐蔽并'有魔幻性'。服务器会在需要时
    自动的创建和释放隐式锁,并将它们传递给存储引擎。存储引擎感知后,可能会‘转换’这些锁。例如,InnoDB有这样的相关规则:对一个给定的服务器级别的表锁,innodb
    应该为其创建特定类型的innodb表锁。这也使得操作人很难理解innodb幕后到底做了什么。


    找出谁持有锁:
    	如果你看到许多进程出于Locked状态,问题可能出在对MyISAM护着其他类似存储引擎的高并发访问。这会阻塞你执行人工操作,例如给表增加索引等。如果一个update
      查询进入队列并等待MyISAM表锁,此时就连select也不会被允许执行。
        某些场景下,可以清楚的看到几个连接长时间持有某个锁,此时需要将它们杀死(或者劝告用户不要阻塞这些连接的工作)。但是如何找出那个连接呢?
        目前没有sql命令可以显示哪个线程持有阻塞你的查询的表锁。如果运行show processlist,你会看到等待锁的进程,而不是哪个进程持有锁。幸运的是,有一个debug
      命令可以打印关于锁的信息到服务器的错误日志中,你可以使用mysqladmin 工具来运行这个命令。
      mysqladmin debug
        在错误日志的输出中包含了许多的调试信息,在接近尾部可以看到下面的信息。我们是创建这些输出的:在一个连接中锁住表,然后在另外一个连接中尝试再次对它加锁。
        Thread database.table_name          Locked/Waiting        Lock_type
		6       test.a                      Locked - read         Read lock without concurrent inserts
		7       test.a                      Waiting - write       Concurrent insert lock
		可以看到线程7正在等待线程6持有的锁。

	2.全局锁
		可以通过 flush tables with read lock 或设置 read_only=1 来获取单个全局读锁。它与任何表锁都冲突。

		mysql 服务器还实现了一个全局读锁,可以如下获取该锁。
		flush tables with read lock;
		如果此时在另外一个会话中尝试再锁这个表,结果会像之前一样被挂起:
		lock tables a write;

		如何判断这个查询正在等待全局读锁而不是一个表级别的锁?请看 show processlist 的输出。
		show full processlist => State: Waiting for global read lock
		注意查询的状态是 State: Waiting for global read lock 。这说明查询正在等待一个全局读锁而不是表级别的锁。
		mysql 没有提供查处谁持有全局锁的方法。

	3.命名锁
		命名锁是表锁的一种,服务器在重命名或删除一个表时创建。

		命名锁是一种表锁:服务器在重命名或者删除一个表时创建。命名锁与普通的表锁相冲突,无论是隐式的还是显式的。例如,如果和之前一样使用lock tables,
	  然后在另外一个会话中尝试对此表重命名,查询会挂起,但这次不会是处于Locked状态。
	    rename table sakila.film2 to sakila.film;
	    和前面一样,从进程列表中获取锁的进程,其状态是 Waiting for table。
	    State: Waiting for table metadata lock
	    也可以在 show open tables 输出中看到命名锁的影响。

MariaDB [test]> show open tables;
+----------+-------+--------+-------------+
| Database | Table | In_use | Name_locked |
+----------+-------+--------+-------------+
| test     | film_text|      1 |           1 |
| test     | film     |      1 |           1 |
| test     | film2    |      1 |           1 |
+----------+-------+--------+-------------+	  
		  注意2个名字(原名和新名)都被锁住了.film_text因为 film 上有个指向它的触发器而被锁住,这也解释了另外一种锁,它们可以暗自的将自己放置到预期
		之外的地方。查询film,触发器会使你悄悄的接触 film_text,因而隐式的锁住它。触发器实际上不需要因重命名触发,确实如此,因此从技术上将并不需要
		锁,但事实是:mysql的锁有时可能并不具有你所期望的锁粒度。
		  mysql 并没有提供任何一种方法来查明谁拥有命名锁,但这通常不是问题,因为它们一般持有非常短的时间。当有冲突时,一般是由于命名锁在等待一个普通的
		表锁,而这通过先前展示的 mysqladmin debug 可以看到。

	4.字符锁
		你可以用 GET_LOCK()以及相关的函数在服务器级别内锁住和释放任意一个字符串。

		在服务器中实现的最后一种锁是用户锁,它基本是一个命名互斥量。你需要指定锁的名称字符串,以及等待超时秒数。
		select GET_LOCK('my_lock', 100);
		指令成功后,这个线程就在命名互斥量上持有一把锁。如果另外一个线程此时尝试锁相同的字符串,它会挂起直到超时。这次进程列表显示了一个不同的进程状态。
		select GET_LOCK('my_lock', 100);
		show full processlist => State: User lock
		User lock 状态是这种类型的锁独有的。mysql 并没有提供查明谁拥有用户锁的方法。

2.InnoDB 中的锁等待
	服务器级的锁要比存储引擎级别的锁容易调试很多。各个存储引擎的锁互不相同,并且存储引擎可能不提供任何方法来查看内部的锁。
	innodb 在 show engine innodb status 的输出中暴露了一些锁信息。如果事务正在等待某个锁,这个锁会显示在 show engine innodb status 输出的
  TRANSACTIONS 部分中。例如,如果一个会话中执行下面的命令,将需要表中第一行的写锁。

  set autocommit=0;
  begin;
  select * from aa limit 1 for update;

    如果在另外一个会话中运行相同的命令,查询将会因第一个会话在那一行获取的锁而阻塞。可以在show engine innodb status 中看到影响。

------------
TRANSACTIONS
------------
Trx id counter D16
Purge done for trx's n:o < D13 undo n:o < 0
History list length 3
LIST OF TRANSACTIONS FOR EACH SESSION:
---TRANSACTION D0B, not started
MySQL thread id 22, OS thread handle 0x7fa770194700, query id 153 localhost root
---TRANSACTION D15, ACTIVE 14 sec starting index read
mysql tables in use 1, locked 1
LOCK WAIT 2 lock struct(s), heap size 376, 1 row lock(s)
MySQL thread id 7, OS thread handle 0x7fa775a76700, query id 152 localhost root Sending data
select * from aa limit 1 for update
------- TRX HAS BEEN WAITING 14 SEC FOR THIS LOCK TO BE GRANTED:
RECORD LOCKS space id 0 page no 307 n bits 72 index `PRIMARY` of table `test`.`aa` trx id D15 lock_mode X waiting
------------------
---TRANSACTION D13, ACTIVE 287 sec
2 lock struct(s), heap size 376, 1 row lock(s)
MySQL thread id 6, OS thread handle 0x7fa7759e2700, query id 154 localhost root
show engine innodb status

	显示线程 id 7,在等待该表的 PRIMARY 索引的 307页上的一个排它锁(lock_mode X)。最终,锁等待超时,查询返回一个错误。
	ERROR 1205 (HY000): Lock wait timeout exceeded; try restarting transaction
	不幸的是,由于看不到谁拥有锁,因此很难确定哪个事务导致这个问题。不过往往可以通过查看哪个事务打开了非常长的一段时间来有根据的猜测;还
  有另外一种方法,可以激活innodb锁监控器,它最多可以显示每个事务中拥有的10把锁。为了激活该监控器,需要在innodb存储引擎中创建一个特殊名字
  的表。
    create table innodb_lock_monitor(a int) engine=innodb;
    发起这个查询后,innodb开始定时的(这个时间间隔可以变化,但通常是每分钟几次)打印 show engine innnodb status 的一个略微加强的版本
  的输出到标准输出中。在大多数系统中,这个输出被重定向到服务器的错误日志中,你可以检查它以查看哪个事务应该拥有那把锁。若想停掉监控器,删除
  表即可。

------------
TRANSACTIONS
------------
LOCK WAIT 2 lock struct(s), heap size 376, 1 row lock(s)
MySQL thread id 7, OS thread handle 0x7fa775a76700, query id 166 localhost root Sending data
select * from aa limit 1 for update
------- TRX HAS BEEN WAITING 16 SEC FOR THIS LOCK TO BE GRANTED:
RECORD LOCKS space id 0 page no 307 n bits 72 index `PRIMARY` of table `test`.`aa` trx id D19 lock_mode X waiting
------------------
TABLE LOCK table `test`.`aa` trx id D19 lock mode IX
RECORD LOCKS space id 0 page no 307 n bits 72 index `PRIMARY` of table `test`.`aa` trx id D19 lock_mode X waiting
---TRANSACTION D17, ACTIVE 341 sec
2 lock struct(s), heap size 376, 1 row lock(s)
MySQL thread id 6, OS thread handle 0x7fa7759e2700, query id 167 localhost root
Trx read view will not see trx with id >= D19, sees < D18
TABLE LOCK table `test`.`aa` trx id D17 lock mode IX
RECORD LOCKS space id 0 page no 307 n bits 72 index `PRIMARY` of table `test`.`aa` trx id D17 lock_mode X
	
	第3行显示的是mysql线程id,跟进程列表里id列的值是一样的。第5行显示了改事务在表里有一个显示的独占表锁(IX)。第6~8行显示了索引里的锁。
  我们删除了第8行的信息,因为它导出了这个锁定的记录,显得非常累赘。第9~11行显示了主键上相应的锁(for update 锁必须锁住整行,而不仅仅
  是索引)。
    出于种种原因,锁监控器并不是最理想的。它的主要问题在于是锁信息非常冗长,因为导出了别锁定记录的十六进制和ascii格式。它会填满错误日志。
  并且还会很轻易的移除固定长度的 show engine innodb status;输出结果。这意味着你可能无法查看在那一段之后的其他输出信息。innodb对每个
  事务打印锁的数量有硬编码限制,即每个事务只能打印出10个持有的锁,超过10个就无法输出,这意味着你可能看不到需要的锁信息。这还不算完,即使
  要查找的东西确实在里面,也难以把它从锁的输出信息里定位出来。
    有2样东西能够使锁的信息更加有用。第一样是本书作者为innodb和mysql服务器编写的一个补丁,包含在 Percona Server 和 MariaDB 中。这个
  补丁会移除输出结果里那冗长的记录导出信息,默认会把锁信息包含到 show engine innodb status 的输出中(因而锁监控器就无需激活了),还会
  增加动态可设置服务器变量来控制冗长的输出信息,以及每个事务能打印出的锁信息的个数。
    第二个可选用的方法是使用 innotop 来解析和格式化输出结果。它的lock模式能够显示锁信息,并通过连接和表优美的聚合在一起,因而能很快的看出
  哪一个事务持有特定表的锁。

3.使用 INFORMATION_SCHEMA 表
	使用 show engine innodb status 来查看锁绝对是比较老派的做法,现在innodb有 information_schema 来显露它的事务和锁。
	show variables like 'innodb_version';

19.高性能MySQL --- 锁的调试_第1张图片

19.高性能MySQL --- 锁的调试_第2张图片

19.高性能MySQL --- 锁的调试_第3张图片

19.高性能MySQL --- 锁的调试_第4张图片

19.高性能MySQL --- 锁的调试_第5张图片

19.高性能MySQL --- 锁的调试_第6张图片

19.高性能MySQL --- 锁的调试_第7张图片

19.高性能MySQL --- 锁的调试_第8张图片

19.高性能MySQL --- 锁的调试_第9张图片

19.高性能MySQL --- 锁的调试_第10张图片

19.高性能MySQL --- 锁的调试_第11张图片

19.高性能MySQL --- 锁的调试_第12张图片

19.高性能MySQL --- 锁的调试_第13张图片

19.高性能MySQL --- 锁的调试_第14张图片

19.高性能MySQL --- 锁的调试_第15张图片

19.高性能MySQL --- 锁的调试_第16张图片

19.高性能MySQL --- 锁的调试_第17张图片

19.高性能MySQL --- 锁的调试_第18张图片

 

 

 

你可能感兴趣的:(高性能MySQL)