深入浅出Mysql - 优化篇(锁)

深入浅出Mysql - 优化篇(锁)

锁是计算机协调多个进程或线程并发访问某一资源的机制。在数据库中,除传统的计算资源(如CPU、RAM、I/O等)的争用以外,数据也是一种供许多用户共享的资源。如何保证数据并发访问的一致性、有效性是所有数据库必须解决的一个问题,锁冲突也是影响数据库并发访问性能的一个重要因素。从这个角度来说,锁对数据库而言显得尤其重要,也更加复杂。

Mysql锁概述

相对其他数据库而言,MySQL的锁机制比较简单,其最显著的特点是不同的存储引擎支持不同的锁机制。比如,MylSAM和MEMORY存储引擎采用的是表级锁(table-level locking);BDB存储弓障采用的是页面锁(page-level locking),但也支持表级锁;InnoDB存储引擎既支持行级锁(row-level locking),也支持表级锁,但默认情况下是采用行级锁。

  • 表级锁:开销小,加锁快;不会出现死锁;锁定粒度大,发生锁冲突的概率最高,并烦最低。
  • 行级锁:开帰大,加锁慢;会出现死锁;锁定牲度最小,发生锁彳突的概率最低并发度也最高。
  • 页面锁:开销和加锁时间界于表锁和行锁之间;会出现死锁;锁定牲度界于表锁和行锁之间,并发度一般。

Myisam 表锁

MylSAM存储引擎只支持表锁,这也是MySQL开始几个版本中唯一支持的锁类型。随着应用对事务完整性和并发性要求的不断提高,MySQL才开始开发基于事务的存储引擎,后来?慢慢出现了支持页锁的BDB存储引擎和支持行锁的InnoDB存储引撃。但是MylSAM的表锁依然是使用最为广泛的锁类型。本节将详细介绍MylSAM表锁的使用。

查询表级锁的争用情况
mysql> show status like 'table%';
+-----------------------+-----------+
| Variable_name         | Value     |
+-----------------------+-----------+
| Table_locks_immediate | 268670717 |
| Table_locks_waited    | 16        |
+-----------------------+-----------+
2 rows in set (0.03 sec)

如果Table_locks_waited的值比较高,则说明存在着较严重的表级锁争用情况。

Mysql表级锁的锁模式

MySQL的表级锁有两种模式:表共享读锁(Table Read Lock)和表独占写锁(Table Write Lock )。

在这里插入图片描述

可见,对MylSAM表的读操作,不会阻塞其他用户对同一表的读请求,但会阻塞对同一?表的写请求;对MylSAM表的写操作,则会阻塞其他用户对同一表的读和写操作;MylSAM表的读操作与写操作之间,以及写操作之间是串行的!当一个线程获得对一个表的写锁后,只有持有锁的线程可以对表进行更新操作。其他线程的读、?写操作都会等待,直到锁被释放为止。

在这里插入图片描述

如何加表锁

MylSAM在执行查询语句(SELECT)前,会自动给涉及的所有表加读锁,在执行更新操
作(UPDATE、DELETE. INSERT等)前,会自动给涉及的表加写锁,这个过程并不需要用户
干预,因此,用户一般不需要直接用LOCK TABLE命令给MylSAM表显式加锁。在本书的示
例中,显式加锁基本上都是为了方便说明问题,并非必须如此。

给MylSAM表显式加锁,一般是为了在一定程度模拟事务操作,实现对某一时间点多个?表的一致性读取。例如,有一个订单表orders,其中记录有各订单的总金额total,同时还有一个订单明细表order_detaU,其中记录有各订单每一产品的金额W、计subtotal,假设我们需要检?查这两个表的金额各计是否相符,可能就需要执行如下两条SQL语句。

lock tables fa_order read local ;
select sum(actual_amount) form fa_order;
unlock tables;

上面的例子在LOCK TABLES时加了 "local 选项,其作用就是在满足MylSAM表
并发插入条件的精况下,允许其他用户在表尾并发插入记录,有关MylSAM表的并发插入问
题,在后面的章节中还会进一步介绍。

在用LOCKTABLES给表显式加表锁时,必须同时取得所有涉及表的锁,并且MySQL不支持锁升级.也就是说,在执行LOCK TABLES后,只能访问威式加锁的这些表,不能访问未加锁的表;同时,如果加的是读锁,那么只能执行查询操作,而不能执行更新操作.在白动加锁的情况下也是如此,MylSAM总是一次获得SQL语句所需要的全部锁.这也正是MylSAM表不会出现死锁(DeadlockFree)的原因。

一个session使用LOCK TABLE命令给表film.text加了读锁,这个session可以查询锁定表中的记录,但更新或访问其他表都会提示错误;高时,另外一个session可以查询表中的记录,但更新就会出现锁等待。

并发插入

上文提到过MylSAM表的读和写是申行的,但这是就总体而言的。在一定条件下,MylSAM表也支持查询和插入操作的并发进行。

MylSAM存储引擎有一个系统变量concurrent.insert,专门用以控制其并发插入的行为,其值分别可以为0、1或2。

  • 当concurrent_insert设置为0时,不允许并发插入。

  • 当concurrentjnsert设置为1时,如果MylSAM表中没有空洞,MylSAM允许在一个进程读表的同时,另一个进程从表尾插入记录.这也是MySQL的默认设置。

  • 当concurrentjnsert设置为2时,无论MylSAM表中有没有空洞,都允许在表尾并发插入记录。

session_l获得了一个表的READ LOCAL锁,该线程可以对表进行查询操作,但不能对表进行更新操屈;其他的线程(session_1),虽然不能对表进行删除和更新操作,但却可以对该表进行并发插入操作,这里假设该表串间不存在空洞。

可以利用MylSAM存储引擎的并发插入特性来解决应用中对同一表查询和插入的锁争用。例如,将concurrent_insert系统变量设为2,总是允许并发插入;同时,通过定期在系统空闲时段执行OPTIMIZE TABLE语句来整理空间碎片,收回因删除记录而产生的中间空洞。

Myisam的锁调度

前面讲过,MylSAM存储引撃的读锁和写锁是互斥的,读写操作是串行的。那么,一个进程请求某个MylSAM表的读锁,同时另一个进程也请求同一表的写锁,MySQL如何处理呢?答案是写进程先获得锁。不仅如此,即使读请求先到锁等待队列,写请求后到,写锁也会插到读锁请求之前!这是因为MySQL认为写请求_般比读请求要重要。这也正是MylSAM表不太适合于有大量更新操作和查询操作应用的原因,因为,大量的更新操作会造成查询操作很难获得读锁,从而可能永远阻塞。

InnoDB锁问题

InnoDB与MylSAM的最大不同有两点:一是支持事务(TRANSACTION);二是采用了行级锁。

事务及其ACID属性:

  • 原子性(Atomicity):亨务是一个原子操作单元,其对数据的修改,要么全都执行,要么全都不执行。

  • 一致性(Consistent):在亨务开始和完成时,数据都必须保持一致状态.这意味着所有相关的数据规则都必须应用于亨务的修改,以保持数据的完整性;事务结束时,所有的内部数据结构(如B树索引或双向链表)也都必须是正确的。

  • 隔髙性(Isolation):数据库系统提供一定的隔髙机制,保证亨务在不受外部并发操作?影响的“独立”环境执行.这意味着亨务处理过程中的中间状态对外部是不可见的,反之亦然。

  • 持久性(Durable):亨务完成之后,它对于数据的修改是永久性的,即使出现系统故障也能够保持。

并发处理带来的问题

更新丢失(Lost Update):当两个或多个事务选择同一行,然后基于最初选定的值更新该行时,由于每个亨务都不知道其他事务的存在,就会发生丢失更新问题一最后的更新夜盖了由其他事务所做的更新。例如,两个编辑人员制作了同一文档的电子副本每个编辑人员独立地更改其副本,然后保存更改后的副本,这样就覆葢了原始文档.最后保存其更改副本的编辑人员覆盖另一个编辑人员所做的更改.如果在一个编辑人员完成并提交亨务之前,另一个编辑人员不能访问同一文件,则可避免此问题。

脏读(Dirty Reads): 一个亨务正在对一条记录做修改,在这个亨务完成并提交前,这条记录的数据就处于不一致状态;这时,另一个事务也来读取同一条记录,如果不加控制,第二个亨务读取了这些“脏”数据,并据此做进一步的处理,就会产生未提交的数据依赖关系.这种现象被形象地叫做“脏读”。

不可重复读(Non-RepeatableReads): 一个亨务在读取某些数据后的某个时间,再次读取以前读过的数据,却发现其读出的数据巳经发生了改变或某些记录已经被删除了!这种现象就叫做“不可重复读。

幻读(Phantom Reads): 一个事务按相同的查询条件重新读取以纺检索过的数据,却?发现其他亨务插入了满足其查询条件的新数据,这种现象就称为“幻读”。

事务的隔离级别

在上面讲到的并发事务处理带来的问题中,“更新丢失”通常是应该完全避免的。但防止?更新丢失,并不能单靠数据库事务控制器来解决,需要应用程序对要更新的数据加必要的锁来解决,因此,防止更新丢失应该是应用的责任。

“脏读”、“不可重复读”和“幻读”,其实都是数据库读一致性问题,必须由数据库提供一?定的事务隔髙机制来解决。数据库实现事务隔离的方式,基本上可分为以下两种。

  • 一种是在读取数据前,对其加锁,阻止其他事务对敷据进行修改。

  • 另一种是不用加任何锁,通过一定机制生成一个数据请求时间点的一致性数据快照(Snapshot),并用这个快照来提供一定级别(语句级或亨务级)的一致性读取.从用户的角度来看,好像是数据库可以提供同一数据的多个版本,因此,这种技术叫做数据多版本井发控制?(MultiVbrsion Concurrency Control,简称MVCC或MCC),也经常称为多版本数据库。

数据库的事务隔离越严格,并发副作用越小,但付出的代价也就越大,因为専务隔离实质?上就是使事务在一定程度上“串行化”进行,这显然与“并发”是矛盾的。同时,不同的应用对读一致性和事务隔离程度的要求也是不同的,比如许多应用对“不可重复读”和“幻读”并?不敏感,可能更关心数据并发访问的能力。

获取Innodb 行锁争用情况

mysql> show status like 'innodb_row_%';
+-------------------------------+--------------+
| 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            |
| Innodb_rows_deleted           | 5094969      |
| Innodb_rows_inserted          | 81058340     |
| Innodb_rows_read              | 109973483305 |
| Innodb_rows_updated           | 2721990      |
+-------------------------------+--------------+
9 rows in set (1.46 sec)

如果发现锁争用比较严重,如 InnoDB_row_lock_waits 和 InnoDB_row_lock_time_avg 的值比较高,可以通过查询infbrmatig_scheml数窟库皐相关的表来查专锁福况,竜者通过设置?InnoDBMonitors来进一步观察发生反冲突的表、数据行等,并分析锁争用的原因。

Innodb的行锁模式及加锁方式

  • 共享锁(S)允许一个亨务去读一行,阻止其他事务获得相同数据集的排他锁。

  • 排他锁(X):允许获得排他锁的亨务更断数据,阻止其他亨务取得相同数据集的共享读锁和排他写锁。

另外,为了允许行锁和表锁共在,实现多粒度锁机制,innoDB还有两种内部使用的意向锁(Intentioii Locks),这两种意向锁都是表锁。

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

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

如果一个事务请求的锁模式与当前的锁兼容,innoDB就将请求的锁授予该事务;反之,?如果两者不兼容,该事务就要等待锁释放。意向锁是InnoDB自动加的,不需用户干预。对于UPDATE、DELETE和INSERT语句,InnoDB会自动给涉及数据集加排他锁(X);对于普通SELECT语句,hmoDB不会加任何锁;事务可以通过以下语句显示给记录集加共享锁或排他锁。

共享锁(S): SELECT * FROM table_name WHERE... LOCK IN SHARE MODE
用咐锁(X): SELECT * FROM tablc_namc WHERE... FOR UPDATE.

用SELECT… IN SHARE MODE获得共享锁,主要用在需要数据依存关系时来确认某行记录是否存在,并确保没有人对这个记录进行UPDATE或者DELETE操作。但是如果当前事务也需要对该记录进行更新操作,则很有可能造成死锁,对于锁定行记录后需要进行更新操作的赫,应该稣 SELECT… FOR UPDATE方式获得排他锁。

Innodb行锁实现方式

InnoDB行锁是通过给索引上的索引项加锁来实现的,如果没有索引,InnoDB将通过隐蒙?的聚簇索引来对记录加锁。

  • Record lock:对索引项加倾。
  • Gap lock:对索引项之间的“间隙第一条记录前的 响隙”为I后一条记录后的间BT加锁。
  • Next-keylock:前两种的馆合,对记录及其前面的间隙加做。

ImoDB这种行锁实现特点意味着:如果不通过索引条件检索数据,那么IimoDB将对表中?的所有记录加锁,实际效果跟表锁一样!

在实际应用中,要特别注意ImoDB行锁的这一特性,否则可能导致大量的锁冲突,从而影响并发性能。

在不通过索引条件查询时,InnoDB会锁定表中的所有记录。

看起来sessional只给一行加了排他锁,但session^在请求其他行的排他锁时,却出现了锁等待!原因就是星没有索引的情况下,hmoDB会嗣有记录都加锁。

由于MySQL的行锁是针对索引加的锁,不是针对记录加的锁,所以虽然是访问不?同行的记录,但是如果是使用相同的索引键,是会出现锁冲突的。

在这里插入图片描述

当表有多个索引的时候,不同的事务可以使用不同的索引锁定不同的行,不论是使用主缝索引、唯一索引或普通索引,IimoDB都会使用行锁来对数据加锁。

在这里插入图片描述

Next-Key 锁

当我们用范围条件而不是相等条件检索数据,并请求共享或排他锁时,Innodb会给符合条件的已有数据记录的索引项加锁;对于键值在条件范围内但并不存在的记录,叫做“间隙(GAP)", InnoDB也会对这个“间隙”加锁,这种锁机制就是所谓的Next-Key锁。

InnoDB使用Next-Key锁的目的,一方面是为了防止幻读,以满足相关隔离级别的要求,?对于上面的例子,要是不使用间隙锁,如果其他事务插入了 empid大于100的任何记录,那么?本事务如果再次执行上述语句,就会发生幻读;另一方面,是为了满足其恢复和复制的需要。

很显然,在使用范围条件检索并锁定记录时,InnoDB 这种加锁机制会阻塞符合条件范围?内键值的并发插入,这往往会造成严重的锁等待。因此,在实际应用开发中,尤其是并发插入比较多的应用,我们要尽量优化业务逻辑,尽量使用相等条件来访问更新数据,避免使用范围条件。

还要特别说明的是,InnoDB除了通过范围条件加锁时使用Next-Key锁外,如果使用相等?条件请求给一个不存在的记录加锁,IimoDB也会使用Next-Key锁!

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章