MySQL管理与优化(16):锁问题

锁问题

MySQL锁概述:

  • MyISAM, MEMORY支持表级锁BDB支持页面锁和表级锁InnoDB支持行级锁
  • 三种锁的特性:

      1.表级锁:开销小,加锁快;不会出现死锁;锁定粒度大,发生锁冲突的概率最高,并发度最低。

      2.行级锁:开销大,加锁慢;会出现死锁;锁定粒度最小,发生锁冲突的概率最低,并发度最高。

      3.页面锁:开锁和加锁时间界于表锁和行锁之间;会出现死锁;锁定粒度界于表锁和行锁之间,并发度一般。

MyISAM表锁:

查询表锁争用情况:

mysql> SHOW STATUS LIKE 'table%';
+----------------------------+-------+
| Variable_name              | Value |
+----------------------------+-------+
| Table_locks_immediate      | 44571 |
| Table_locks_waited         | 0     | -- 值越高,争用越大
| Table_open_cache_hits      | 16    |
| Table_open_cache_misses    | 0     |
| Table_open_cache_overflows | 0     |
+----------------------------+-------+
5 rows in set (0.08 sec)

MySQL表级锁的锁模式:

  • MySQL的表级锁采用的是读写锁的方式,即允许多读,但不允许多写。

如何加表锁:

  • MyISAM表在执行SELECT会自动加上读锁,在执行UPDATE,DELETE,INSERT前会自动加上写锁。
  • 但有时为了多个表的操作,我们会手动设置锁,如:
LOCK TABLES orders READ LOCAL, order_detail READ LOCAL; -- LOCAL使得表可以并发在表尾插入数据
SELECT sum(total) FROM orders;
SELECT sum(subtotal) FROM order_detail;
UNLOCK TABLES;
NOTE: LOCK TABLES必须同时取得所有涉及表的锁,并且MySQL不支持锁升级,即LOCK TABLES后,只能访问显示加锁的表,不能访问未加锁的表, 自动加锁的情况也差不多如此,因此MyISAM不会出现死锁。
  • 在SQL语句使用了表别名,那么别名也要进行加锁,如:
LOCK TABLE users AS u1 READ, users AS u2 READ

并发插入:

  • 对于MyISAM表,我们可以通过系统变量concurrent_insert设置并发插入:
mysql> SHOW VARIABLES LIKE 'concurrent%';
+-------------------+-------+
| Variable_name     | Value |
+-------------------+-------+
| concurrent_insert | AUTO  | -- 默认值为1
+-------------------+-------+
1 row in set (0.00 sec)
  • concurrent_insert值:0表示不允许并发插入,1表示若表中间没有删除的行,则允许并发插入,2表示完全允许并发插入。

MyISAM的锁调度:

  • MyISAM表默认写锁比读锁优先级更高,即便读请求在写请求之前进入等待队列,写请求也会先放到读请求前。
  • 有关MyISAM锁调度的设置:

       1. 通过启动参数low-priority-updates,使MyISAM给予读请求优先的权利。

       2. 通过SET LOW_PRIORITY_UPDATES=1, 使当前连接读请求优先。

       3. 通过设置INSERT, UPDATE, DELETE的LOW_PRIORITY属性,降低该语句的优先级。

  • 我们也可以设置max_write_lock_count来设置,当写锁达到这个值后,会降低写请求的优先级,给读请求获取锁的机会。

InnoDB锁问题:

  • 与MyISAM相比,InnoDB主要特点是支持事务行级锁
  • 事务(Transaction)ACID属性:

       1. 原子性(Atomicity):事务是一个原子操作,要么全都执行,要么全都不执行。

       2. 一致性(Consistent):在事务的开始和结束时,数据都必须保持一致状态。

       3. 隔离性(Isolation):数据库系统提供一定的隔离机制,保证事务在不受外部并发操作影响的“独立”环境。

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

  • 并发事务处理带来的问题:

      1. 更新丢失(Lost Update): 当两个或多个事务选择同一行,然后基于最初选定的值更新该行时,由于每个事务都不知道其他事务的存在,就会发生丢失更新的问题--最后的更新覆盖了由其他事务所做的更新。

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

      3. 不可重复读(Non-Repeatable Reads): 一个事务在读取某些数据后的某个时间,再次读取以前读过的数据,却发现其读出的数据已经发生变化或某些记录已经被删除!这种现象就叫"不可重复读"。

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

事务隔离级别:

  • 更新丢失应该由应用来负责避免。
  • 脏读,不可重复读,幻读都是数据库读一致性问题,必须由数据库提供一定的事务隔离机制来解决。
  • 数据库实现事务隔离的方式,基本分为两种:

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

       2. 另一种是不加任何锁,通过一定机制生成一个数据请求访问点的一致性数据快照(Snapshot),并用这个快照来提供一定级别(语句级或事务级),从用户的角度来看,好像是数据库可以提供同一数据的多个版本,因此这种技术叫做数据库多版本并发控制(MultiVersion Concurrentcy Control, 简称MVCCMCC),也经常称为多版本数据库

  • 为了解决"隔离"与"并发"的矛盾,ISO/ANSI SQL92定义了4个隔离级别:

      MySQL管理与优化(16):锁问题_第1张图片

  • 各数据库不一定完全支持4个隔离级别,请参看各数据库实现描述。

获取InnoDB行锁争用情况

  • 可通过检查InnoDB_row_lock状态变量来分析系统上的行锁的争夺情况,若Innodb_row_lock_current_waitsInnodb_row_lock_time_avg值比较高,说明争用比较严重。
mysql> SHOW STATUS LIKE 'innodb_row_lock%';
+-------------------------------+-------+
| Variable_name                 | Value |
+-------------------------------+-------+
| Innodb_row_lock_current_waits | 0     |
| Innodb_row_lock_time          | 1     |
| Innodb_row_lock_time_avg      | 1     |
| Innodb_row_lock_time_max      | 1     |
| Innodb_row_lock_waits         | 1     |
+-------------------------------+-------+
5 rows in set (0.16 sec)
  • 我们也可以通过InnoDB Monitors来进一步观察发生冲突的表,数据行等:
SHOW ENGINE INNODB STATUS --显示标准InnoDB Monitor的扩展信息
SHOW ENGINE INNODB MUTEX  --显示InnoDB互斥量和读写锁信息
SHOW ENGINE {NDB | NDBCLUSTER} STATUS
SHOW ENGINE PERFORMANCE_SCHEMA STATUS
详情可参考: http://dev.mysql.com/doc/refman/5.6/en/innodb-monitors.html

InnoDB的行锁模式及加锁方法

InnoDB实现了两种类型的行锁:

  • 共享锁(S):允许一个事务去读一行,阻止其他事务获得相同数据集的排他锁。
  • 排他锁(X):允许获取排他锁的事务更新数据,阻止其他事务取得相同数据集的共享读锁和排他写锁。

为了允许行锁与表锁共存,InnoDB还有两种意向锁:

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

InnoDB行锁模式兼容性:

MySQL管理与优化(16):锁问题_第2张图片

  • 对于DELETE, INSERT, UPDATE操作,MySQL会自动加上排他锁(X),对于SELECT操作,MySQL不会加任何锁。
  • 可以通过SQL显示加锁:
SELECT * FROM table WHERE ... LOCK IN SHARE MODE; --共享锁,不要在共享模式下执行写操作,有可能死锁
SELECT * FROM table WHERE ... FOR UPDATE; --排他锁

InnoDB行锁实现方式:

  • InnoDB通过给索引上的索引项加锁来实现的,而Oracle是对数据块中对应的数据行加锁来实现的。
  • 不通过索引条件查询的时候,InnoDB使用的是表锁
  • 由于MySQL的行锁针对索引加的锁,不是针对记录加的锁,所以虽然是访问不同行的记录,如果使用了相同的索引键,也会出现锁冲突。
  • 当表有多个索引时,不同的事务可以使用不同的索引锁定不同的行

间隙锁(Next-Key锁):

  • 当使用范围查询时,并请求共享或排他锁,InnoDB会给符合条件的已有数据记录的索引项加锁:对于键值在条件范围内但不存在的的记录,叫做"间隙"(GAP)。InnoDB也会对这个"间隙"加锁,这种锁机制就是"间隙锁"。

恢复和复制的需要,对InnoDB锁机制的影响:

MySQL的恢复机制主要有以下特点:

  • MySQL的恢复是SQL语句级别的,也就是重新执行BINLOG中的SQL语句,而Oracle是基于数据库文件块。
  • MySQL的BinLog是按事务提交先后顺序记录的,恢复也是按这个顺序执行的,而Oracle按系统更新号来执行的饿。
  • 对于INSERT...SELECT...和CREATE TABLE...SELECT语句,MySQL会对源表加锁,可以通过设置innodb_locks_unsafe_for_binlog等于"on"来取消自动锁表;或使用SELECT * FROM source_tab INTO outfile和LOAD DATA infile来间接代替,这种方式MySQL不会加锁。

InnoDB在不同隔离级别下的一致性读及锁的差异:

  • InnoDB存储引擎中不同SQL在不同隔离级别下锁比较:

     MySQL管理与优化(16):锁问题_第3张图片

什么时候使用表锁:

  • 通常选择InnoDB作为存储引擎,应该使用行锁,但对个别特殊事务需要使用表锁:

       1. 当表比较大,且需要更新大部分或全部数据时。

       2. 事务涉及多个表,比较复杂,很可能引起死锁,造成大量事务回滚。

  • 对于InnoDB类型的表,加表锁,需要做两件事,SET AUTOCOMMIT=0LOCK TABLE,如:
SET AUTOCOMMIT=0;
LOCK TABLE t1 WRITE, t2 READ, ...; 
---do sth to t1 and t2
COMMIT;
UNLOCK TABLES;

关于死锁:

  • MyISAM是表锁级别,不会出现死锁。
  • 对于InnoBD我们可以通过设置innodb_lock_wait_timeout来解决死锁问题,该参数还能防止因在并发访问大而造成等待线程的堆积带来的资源浪费问题。

几种常用于避免死锁的方法:

  • 相同的顺序来访问表。
  • 批量处理数据时,可事先对数据进行排序,也可降低死锁可能。
  • 进行更新操作应该使用排它锁,而不应先申请共享锁,更新时再申请排它锁。
  • 可通过一些命令来检查死锁状态,如:
SHOW ENGINE INNODB STATUS \G
SHOW ENGINE INNODB MUTEX \G
不吝指正。

你可能感兴趣的:(mysql,锁)