mysql 锁

引言:

锁是计算机协调多个进程或者多个线程之间并发访问同一资源的机制。在数据库系统中,除了传统的计算机资源(CPU、RAM、I/O)的争用之外,数据也是一种供许多用户共享的资源。如何保证数据并发访问的一致性和有效性是全部数据库系统须要考虑的问题。锁冲突也是影响数据库并发访问性能的一个重要因素。从这个角度来说,锁对于数据库尤为重要,也更加复杂。

对于mysql来讲,不一样的引擎锁的实现方式不同,因此须要根据不一样数据库引擎来进行讨论。mysql

1.MyISAM

锁分类

MyISAM 引擎的表锁有2种模式,读锁写锁sql

锁之间的关系

  • 读锁之间:并行,不阻塞
  • 读写锁之间:串行阻塞
  • 写锁之间:串行阻塞

加锁时机

MyISAM在执行查询语句(SELECT)前,会自动给涉及的全部表加读锁,在执行更新操做(UPDATE、DELETE、INSERT等)前,会自动给涉及的表加写锁,这个过程并不须要用户干预,所以用户通常不须要直接用LOCK TABLE命令给MyISAM表显式加锁。数据库

MyISAM存储引擎的读和写锁是互斥,读操做是串行的。那么,一个进程请求某个MyISAM表的读锁,同时另外一个进程也请求同一表的写锁,MySQL如何处理呢?答案是写进程先得到锁。不只如此,即便读进程先请求先到锁等待队列,写请求后到,写锁也会插到读请求以前!这是由于MySQL认为写请求通常比读请求重要。这也正是MyISAM表不太适合于有大量更新操做和查询操做应用的缘由,由于,大量的更新操做会形成查询操做很难得到读锁,从而可能永远阻塞。这种状况有时可能会变得很是糟糕!幸亏咱们能够经过一些设置来调节MyISAM的调度行为。经过指定启动参数low-priority-updates,使MyISAM引擎默认给予读请求以优先的权利。session

2.InnoDB

InnoDB与MyISAM的最大不一样有两点:一是支持事务(TRANSACTION);二是采用了行级锁。行级锁和表级锁原本就有许多不一样之处,另外,事务的引入也带来了一些新问题。数据结构

1.事务及其ACID属性

​ 事务是由一组SQL语句组成的逻辑处理单元,事务具备4属性,一般称为事务的ACID属性。并发

  • 原性性(Actomicity):事务是一个原子操做单元,其对数据的修改,要么全都执行,要么全都不执行。
  • 一致性(Consistent):在事务开始和完成时,数据都必须保持一致状态。这意味着全部相关的数据规则都必须应用于事务的修改,以操持完整性;事务结束时,全部的内部数据结构(如B树索引或双向链表)也都必须是正确的。
  • 隔离性(Isolation):数据库系统提供必定的隔离机制,保证事务在不受外部并发操做影响的“独立”环境执行。这意味着事务处理过程当中的中间状态对外部是不可见的,反之亦然。
  • 持久性(Durable):事务完成以后,它对于数据的修改是永久性的,即便出现系统故障也可以保持。

2.并发事务带来的问题

​ 相对于串行处理来讲,并发事务处理能大大增长数据库资源的利用率,提升数据库系统的事务吞吐量,从而能够支持能够支持更多的用户。但并发事务处理也会带来一些问题,主要包括如下几种状况。性能

  • 更新丢失(Lost Update):当两个或多个事务选择同一行,而后基于最初选定的值更新该行时,因为每一个事务都不知道其余事务的存在,就会发生丢失更新问题——最后的更新覆盖了其余事务所作的更新。例如,两个编辑人员制做了同一文档的电子副本。每一个编辑人员独立地更改其副本,而后保存更改后的副本,这样就覆盖了原始文档。最后保存其更改保存其更改副本的编辑人员覆盖另外一个编辑人员所作的修改。若是在一个编辑人员完成并提交事务以前,另外一个编辑人员不能访问同一文件,则可避免此问题
  • 脏读(Dirty Reads):一个事务正在对一条记录作修改,在这个事务并提交前,这条记录的数据就处于不一致状态;这时,另外一个事务也来读取同一条记录,若是不加控制,第二个事务读取了这些“脏”的数据,并据此作进一步的处理,就会产生未提交的数据依赖关系。这种现象被形象地叫作“脏读”。
  • 不可重复读(Non-Repeatable Reads):一个事务在读取某些数据已经发生了改变、或某些记录已经被删除了!这种现象叫作“不可重复读”。
  • 幻读(Phantom Reads):一个事务按相同的查询条件从新读取之前检索过的数据,却发现其余事务插入了知足其查询条件的新数据,这种现象就称为“幻读”。

3.事务隔离级别

在并发事务处理带来的问题中,“更新丢失”一般应该是彻底避免的。但防止更新丢失,并不能单靠数据库事务控制器来解决,须要应用程序对要更新的数据加必要的锁来解决,所以,防止更新丢失应该是应用的责任。优化

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

一种是在读取数据前,对其加锁,阻止其余事务对数据进行修改。设计

另外一种是不用加任何锁,经过必定机制生成一个数据请求时间点的一致性数据快照(Snapshot),并用这个快照来提供必定级别(语句级或事务级)的一致性读取。从用户的角度,好像是数据库能够提供同一数据的多个版本,所以,这种技术叫作数据多版本并发控制(MultiVersion Concurrency Control,简称MVCC或MCC),也常常称为多版本数据库。

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

为了解决“隔离”与“并发”的矛盾,ISO/ANSI SQL92定义了4个事务隔离级别,每一个级别的隔离程度不一样,容许出现的反作用也不一样,应用能够根据本身业务逻辑要求,经过选择不一样的隔离级别来平衡"隔离"与"并发"的矛盾

隔离级别 读数据一致性 脏读 不可重复读 幻读
未提交读(Read uncommitted) 最低级别,只能保证不读取物理上损坏的数据
已提交度(Read committed) 语句级
可重复读(Repeatable read) 事务级
可序列化(Serializable) 最高级别,事务级

4.锁分类:

InnoDB锁包括了共享锁和排他锁,同时为了容许行锁和表锁共存,实现多粒度锁机制,InnoDB还有两种内部使用的意向锁(Intention Locks),这两种意向锁都是表锁。

  • 共享锁(s):容许一个事务去读一行,阻止其余事务得到相同数据集的排他锁。
  • 排他锁(X):容许获取排他锁的事务更新数据,阻止其余事务取得相同的数据集共享读锁和排他写锁。
  • 意向共享锁(IS):事务打算给数据行共享锁,事务在给一个数据行加共享锁前必须先取得该表的IS锁。
  • 意向排他锁(IX):事务打算给数据行加排他锁,事务在给一个数据行加排他锁前必须先取得该表的IX锁。
当前锁模式/是否兼容 X IX S IS
X(排他锁) 冲突 冲突 冲突 冲突
S(共享锁) 冲突 冲突 兼容 兼容
IX(意向排他锁) 冲突 兼容 冲突 兼容
IS(意向共享锁) 冲突 兼容 兼容 兼容

4.1.间隙锁(Next-Key锁):影响并发insert的锁

键值在条件范围内但并不存在的记录,叫作“间隙(GAP)”,InnoDB也会对这个“间隙”加锁,这种锁机制就是所谓的间隙锁 (Next-Key锁)。举例来讲,假若有以下数据表:

Id Name Age
1 p1 10
4 p2 18
8 p3 12

这时候执行以下的sql语句:

select * from demo where id>1 for update;

这时候在数据库中会对id=4,8的记录添加行锁。同时会添加以下间隙锁:(2,4], (4,8],(8, +suprenum],这种状况下若是要在数据库中insert id在间隙锁之间的记录是会被阻塞的。

4.1.1.范围查询间隙锁

当咱们用范围条件而不是相等条件检索数据,并请求共享或排他锁时,InnoDB会给符合条件的已有数据记录的 索引项加锁。
举例来讲有表demo:

Id Name Age
1 p1 10
2 p2 18
3 p3 12

开启第一个命令执行窗口执行for update操做:

mysql> set autocommit = 0;
Query OK, 0 rows affected (0.01 秒)

mysql> select * from demo where id>2 for update;
+----+------+------+
| id | name | age  |
+----+------+------+
| 3  | p3   | 12   |
| 4  | p4   | 11   |
+----+------+------+
2 行于数据集 (0.02 秒)

mysql>

这是一个范围条件的检索,InnoDB不只会对符合条件的empid值为3,4的记录加锁,也会对id大于3(这些记录并不存在)的“间隙”加锁。

开启第二个命令窗口执行插入数据操做:

mysql> set autocommit = 0;
Query OK, 0 rows affected (0.01 秒)

mysql> insert into demo(name,age) values('p5', 22);
Lock wait timeout exceeded; try restarting transaction

能够看到这条sql执行会被阻塞,由于大于3的不存在的记录也被加上了间隙锁了,因此在执行插入时就执行不下去了,由于新增记录的id是大于3的。

4.1.2.等值记录不存在致使间隙锁

除了范围查询会使用间隙锁以外,对于等值查询而不存在的记录也会使用间隙锁,一样打开第一个命令窗口,去更新一条不存在的记录。

假如当前数据库记录以下:

Id Name Age
1 p1 10
2 p2 18
6 p3 12

开启第一个命令执行窗口执行for update操做:

mysql> set autocommit = 0;
Query OK, 0 rows affected (0.01 秒)

mysql> update demo set name='pp' where id=3;
Query OK, 0 rows affected (0.00 秒)

这时候去另外一个命令窗口新增一条记录,能够看到这条记录一直阻塞到超时也没有执行,这是由于若是一个等值的记录不存在的状况下,mysql会使用间隙锁锁住它临近的前面和后面2条记录之间的全部记录。对于id=3的记录来讲不存在,因此他会锁住id(2-6]的全部记录。

mysql> set autocommit = 0;
Query OK, 0 rows affected (0.01 秒)

mysql> insert into demo(id,name, age) values(4,'p5', 88);
Lock wait timeout exceeded; try restarting transaction

InnoDB使用间隙锁的目的,一方面是为了防止幻读,以知足相关隔离级别的要求,对于上面的例子,要是不使 用间隙锁,若是其余事务插入了empid大于100的任何记录,那么本事务若是再次执行上述语句,就会发生幻读;另一方面,是为了知足其恢复和复制的须要。有关其恢复和复制对锁机制的影响,以及不一样隔离级别下InnoDB使用间隙锁的状况,在后续的章节中会作进一步介绍。

很显然,在使用范围条件检索并锁定记录时,InnoDB这种加锁机制会阻塞符合条件范围内键值的并发插入,这每每会形成严重的锁等待。所以,在实际应用开发中,尤为是并发插入比较多的应用,咱们要尽可能优化业务逻辑,尽可能使用相等条件来访问更新数据,避免使用范围条件,对于删除。

5.加锁时机:

在MVCC并发控制中,读操做能够分为2类。快照读当前读,其中快照读读取的是可见版本,不加锁;而当前读读取的是最新版本,而且当前读返回的记录都会加锁,保证其余并发事务不能修改该记录。那么MVCC中那些语句会加锁,那些不加锁呢?

快照读简单的select属于快照读,不须要加锁。

当前读:特殊的select操做(select for update),insert/update/delete属于当前读,都须要加锁。

6.何时使用表锁

​ 对于InnoDB表,在绝大部分状况下都应该使用行级锁,由于事务和行锁每每是咱们之因此选择InnoDB表的理由。但在个另特殊事务中,也能够考虑使用表级锁。

  • 第一种状况是:事务须要更新大部分或所有数据,表又比较大,若是使用默认的行锁,不只这个事务执行效率低,并且可能形成其余事务长时间锁等待和锁冲突,这种状况下能够考虑使用表锁来提升该事务的执行速度。
  • 第二种状况是:事务涉及多个表,比较复杂,极可能引发死锁形成大量事务回滚。这种状况也能够考虑一次性锁定事务涉及的表,从而避免死锁、减小数据库因事务回滚带来的开销。

​ 固然,应用中这两种事务不能太多,不然,就应该考虑使用MyISAM表。

​ 在InnoDB下 ,使用表锁要注意如下两点。

​ (1)使用LOCK TALBES虽然能够给InnoDB加表级锁,但必须说明的是,表锁不是由InnoDB存储引擎层管理的,而是由其上一层MySQL Server负责的,仅当autocommit=0、innodb_table_lock=1(默认设置)时,InnoDB层才能知道MySQL加的表锁,MySQL Server才能感知InnoDB加的行锁,这种状况下,InnoDB才能自动识别涉及表级锁的死锁;不然,InnoDB将没法自动检测并处理这种死锁。

​ (2)在用LOCAK TABLES对InnoDB锁时要注意,要将AUTOCOMMIT设为0,不然MySQL不会给表加锁;事务结束前,不要用UNLOCAK TABLES释放表锁,由于UNLOCK TABLES会隐含地提交事务;COMMIT或ROLLBACK产不能释放用LOCAK TABLES加的表级锁,必须用UNLOCK TABLES释放表锁,正确的方式见以下语句。

注意:

InnoDB 行锁是经过给索引上的索引项加锁来实现的,只有经过索引条件检索数据,InnoDB 才使用行级锁,不然,InnoDB将使用表锁
因为 MySQL 行锁是针对索引加的锁,不是针对记录加的锁,因此虽然是访问不一样行的记录,可是若是是使用相同的索引键,是会出现锁冲突的,设计的时候要注意这一点

当表有多个索引的时候,不一样的事务可使用不一样的索引锁定不一样的行,另外,不管是使用主键索引、惟一索引或普通索引,InnoDB都会使用行锁来对数据加锁

即使在条件中使用了索引字段,可是否使用索引来检索数据是由 MySQL 经过判断不一样执行计划的代价来决定的,若是 MySQL 认为全表扫描效率更高,好比对一些很小的表,它就不会使用索引,这种状况下 InnoDB 将使用表锁,而不是行锁。所以,在分析锁冲突时,别忘了检查 SQL 的执行计划,以确认是否真正使用了索引

检索值的数据类型与索引字段不一样,虽然 MySQL 可以进行数据类型转换,但却不会使用索引,从而致使 InnoDB 使用表锁。经过用explain 检查两条SQL的执行计划

3.死锁

1.死锁的产生缘由

所谓死锁是2个或者2个以上的并发进程在执行过程当中因争夺资源而相互等待的现象,若无外力做用它们都将没法继续推动下去,此时称系统处于死锁状态或系统产生了死锁。这些永远相互等待的进程称为死锁进程。表级锁不会产生死锁,因此解决死锁主要仍是针对于InnoDB。

死锁的关键在于:两个(或以上)的Session加锁的顺序不一致

那么对应的解决死锁问题的关键就是:让不一样的session加锁有次序

2.常见的死锁案例:

1.不一样表相同记录行锁冲突

这种状况很好理解,事务A和事务B操做两张表,但出现循环等待锁状况。

事务A 事务B
begin begin
delete from t1 where id=1
update t2 set name='test' where id=2
update t2 set name='test' where id=2
delete from t1 where id=1
2.相同表记录行锁冲突

这种状况比较常见,以前遇到两个job在执行数据批量更新时,jobA处理的的id列表为[1,2,3,4],而job处理的id列表为[8,9,10,4,2],这样就形成了死锁。

事务A 事务B
begin begin
update t2 set name='test' where id=1
update t2 set name='test' where id=2
update t2 set name='test' where id=2
update t2 set name='test' where id=1
3.不一样索引锁冲突

这种状况比较隐晦,事务A在执行时,除了在二级索引加锁外,还会在聚簇索引上加锁,在聚簇索引上加锁的顺序是[1,4,2,3,5],而事务B执行时,只在聚簇索引上加锁,加锁顺序是[2,3,4,5],这样就形成了死锁的可能性。

事务A 事务B
update msg set message='订单' where token>'abc' delete from msg where id>1
4. gap锁冲突

innodb在RR级别下,以下的状况也会产生死锁,比较隐晦。不清楚的同窗能够自行根据上节的gap锁原理分析下。

事务A 事务B
begin begin
update msg set message='订单' where token='asd'
update msg set message='订单' where token='aaa'
insert into msg values(null, 'aad', 'hello');
commit insert into msg values(null, 'bsa', 'hello');
commit

3.如何尽量避免死锁

  1. 固定的顺序访问表和行。好比对第2节两个job批量更新的情形,简单方法是对id列表先排序,后执行,这样就避免了交叉等待锁的情形;又好比对于3.1节的情形,将两个事务的sql顺序调整为一致,也能避免死锁。
  2. 大事务拆小。大事务更倾向于死锁,若是业务容许,将大事务拆小。
  3. 在同一个事务中,尽量作到一次锁定所须要的全部资源,减小死锁几率。
  4. 下降隔离级别。若是业务容许,将隔离级别调低也是较好的选择,好比将隔离级别从RR调整为RC,能够避免掉不少由于gap锁形成的死锁。
  5. 为表添加合理的索引。能够看到若是不走索引将会为表的每一行记录添加上锁,死锁的几率大大增大。

总结

MyISAM注意点

​ (1)共享读锁(S)之间是兼容的,但共享读锁(S)和排他写锁(X)之间,以及排他写锁之间(X)是互斥的,也就是说读和写是串行的。

​ (2)在必定条件下,MyISAM容许查询和插入并发执行,咱们能够利用这一点来解决应用中对同一表和插入的锁争用问题。

​ (3)MyISAM默认的锁调度机制是写优先,这并不必定适合全部应用,用户能够经过设置LOW_PRIPORITY_UPDATES参数,或在INSERT、UPDATE、DELETE语句中指定LOW_PRIORITY选项来调节读写锁的争用。

​ (4)因为表锁的锁定粒度大,读写之间又是串行的,所以,若是更新操做较多,MyISAM表可能会出现严重的锁等待,能够考虑采用InnoDB表来减小锁冲突。

InnoDB 注意点

​ (1)InnoDB的行销是基于索引实现的,若是不经过索引访问数据,InnoDB会使用表锁。

​ (2)InnoDB间隙锁机制,以及InnoDB使用间隙锁的缘由。

​ (3)在不一样的隔离级别下,InnoDB的锁机制和一致性读策略不一样。

​ (4)MySQL的恢复和复制对InnoDB锁机制和一致性读策略也有较大影响。

​ (5)锁冲突甚至死锁很难彻底避免。

​ 在了解InnoDB的锁特性后,用户能够经过设计和SQL调整等措施减小锁冲突和死锁,包括:

  • 尽可能使用较低的隔离级别
  • 精心设计索引,并尽可能使用索引访问数据,使加锁更精确,从而减小锁冲突的机会。
  • 选择合理的事务大小,小事务发生锁冲突的概率也更小。
  • 给记录集显示加锁时,最好一次性请求足够级别的锁。好比要修改数据的话,最好直接申请排他锁,而不是先申请共享锁,修改时再请求排他锁,这样容易产生死锁。
  • 不一样的程序访问一组表时,应尽可能约定以相同的顺序访问各表,对一个表而言,尽量以固定的顺序存取表中的行。这样能够大减小死锁的机会。
  • 尽可能用相等条件访问数据,这样能够避免间隙锁对并发插入的影响。
  • 不要申请超过实际须要的锁级别;除非必须,查询时不要显示加锁。
  • 对于一些特定的事务,可使用表锁来提升处理速度或减小死锁的可能。
  • 间隙锁是针对insert而致使阻塞的锁,实际开发中应该尽可能避免间隙锁的发生,对于范围查询以及等值查询的修改,尽可能先去查询数据是否存在,存在再去执行更新和删除操做,不然可能会产生死锁。

    锁、事务、隔离级别、acid之间的关系:

    锁是解决并发的机制,事务是对一组sql处理单元的统称,ACID是事务的4个属性,隔离级别是对事务之间隔离程度的一种描述。

相关文章
相关标签/搜索