MySQL并发事务控制:锁、MVCC

缘起

最近同事开发遇到了一个事务阻塞的问题,查了一些资料发现关于MySQL事务、锁这一块的资料都比较絮乱,让人看的云里雾里.借这个机会,恰好也对这一块作一个总结梳理,但愿能比较全面去写一下MySQL的并发事务处理mysql

事务的特性与隔离级别

要讲锁,必需要先讲事务的特性与隔离级别,由于锁机制的存在是为了保证事务对应隔离级别下的特性.事务具备如下几个特性 在MySQL中,存在如下几种隔离级别 RU 读未提交,顾名思义,在这种隔离级别下,当多个事务并行对同一数据进行操做时,会读取未提交的数据,也被称之为脏读.这种隔离级别由于会出现脏读现象,因此在实际场景中不多用.sql

RC 读提交,一个事务只能看见已经提交事务所作的改变.但这种隔离级别会出现 不可重复读现象,即在一个事务内,屡次读同一数据,在这个事务尚未结束时,若是另外一个事务刚好修改了这个数据,那么,在第一个事务中,两次读取的数据就可能不一致.数据库

RR 可重复读,这是MySQL的默认事务隔离级别,在这种隔离级别下,解决了RC存在的不可重复读问题,确保在同一事务中,会看到一样的数据行.但可能会出现幻读,即当一个事务在执行读取操做,第一次查询数据总量后,另外一个事务执行了新增数据的操做并提交后,这个时候第一个事务读取的数据总量和以前统计的不同,就像产生幻觉同样.markdown

SERIALIZABLE 串行化,是4种隔离级别中最高的级别,解决了脏读、可重复读、幻读的问题,可是性能最差,它将事务的执行变为顺序执行,与其余三个隔离级别相比,在并行事务执行过程当中,后一个事务的执行必须等待前一个事务结束.并发

MySQL中的锁

在MySQL中,按锁类型划分,有如下种类 提到锁到种类,须要提一下MySQL到存储引擎,MySQL经常使用引擎有MyISAM和InnoDB,而InnoDB是mysql默认的引擎。MyISAM是不支持行锁的,而InnoDB支持行锁和表锁。
MyISAM在执行查询语句(SELECT)前,会自动给涉及的全部表加读锁,在执行更新操做(UPDATE、DELETE、INSERT等)前,会自动给涉及的表加写锁,读锁会阻塞对同一张表对写操做,而写锁既会阻塞对同一张表对写操做,也会阻塞读操做.性能

对于InnoDB来讲,你们都知道InnoDB相对与MyISAM,支持了事务和行锁.而行锁顾名思义,就是针对具体某一行数据上的锁,更切确的说是针对索引加的锁(这个会在下文锁的实现中讲到).优化

排他锁,一般咱们在InnoDB中执行一个更新操做,针对这一行数据会持有排他锁,持有排他锁时,不容许再在数据行上添加写锁与读锁,其余事务对此行数据的读、写操做都会被阻塞,只有当前事务提交了,锁释放了才容许其余事务进行读写,达到避免 脏读 的效果spa

共享锁,主要是为了支持并发的读取数据而出现的,当一个事务持有某一数据行的共享锁时,容许其它事务再获取共享锁,但不容许其它事务获取排他锁,也就是说,在持有共享锁时,多个事务能够读取当前数据,但不不容许任何事务对当前数据进行修改操做,从而避免 不可重复 的问题3d

意向锁,首先须要明白一点,意向锁的做用是在表上的,当一个事务须要获取共享锁或排他锁时,首先要获取对应的意向锁,为何要这样作呢,举个例子,假设在事务A中,某一行数据持有共享锁,这一行只能读,不能写.此时事务B申请得到表的写锁,假如加锁成功,那么事务B将可以对整个表的数据进行读写,与事务A冲突.这种操做确定是不容许的,因此MySQL会在申请共享锁或者排他锁的时候,先获取对应的意向锁,也就是说,你要操做表中的某一行锁数据,先要看看整个表能不能被操做.意向锁的申请是有数据库完成的,不须要人为申请.code

行锁的3种实现

上文对几种锁类型进行了分析,其实平时开发中接触到最多的仍是行锁,行锁的实现有如下几种

在InnoDB中,锁的实现是基于索引的
Record Lock(记录锁),会锁住索引记录,好比 update table where id = 1;,会是这种实现

Gap Lock(间隙锁),实质上是对索引先后的间隙上锁,不对索引自己上锁,目的是为了防止幻读.当使用范围条件而不是相等条件检索数据并请求排他锁、或共享锁时,对于该范围内不存在的记录,不容许其修改插入.举个例子,当表中只有一条id=101的记录,一个事务执行select * from user where user_id > 100 for update;,此时另外一个事务执行插入一条id=102的数据是会阻塞的,必须等待第一个事务提交后才能完成.间隙锁是针对事务隔离级别为可重复读或以上级别

Next-Key Lock,是记录锁和间隙锁对结合,会同时锁住记录与间隙.在可重复读(Repeatable Read)隔离级别下,会以Next-Key Lock的方式对数据行进行加锁

MVCC

锁机制能够控制并发操做,来保证一致性,可是系统开销会很大.在RC、RR的隔离级别下,MySQL InnoDB经过MVCC (多版本并发控制)机制来解决幻读,使事务在并发过程当中,SELECT 操做不用加锁,读写不冲突从而提升性能.其原理是经过保存数据在某个时间点的快照来实现的.经过在每行记录后面保存隐藏列来存放事务ID,这样每个事务,都会对应一个递增的事务ID.假设三个事务同时更新来同一行数据,那么就会对应三个数据版本,但实际上版本一、版本2并非物理存在的,而是经过关联记录在undo log中,这样就能够经过undo log找回数据的历史版本,好比回滚的操做,会使用上一个版本的数据覆盖数据页上的数据

下面举例一个RR隔离级别下快照读例子1:开启事务A按条件A查询到两条数据,此时事务B再插入1条数据知足条件A的数据,并提交事务,此时事务A再按条件A进行查询,查询到的依然是两条数据,也就是说,事务A查询到的并非当前最新的数据版本,而是经过MVCC实现的历史快照版本.这也是可重复读的实现.

上面的例子介绍了读操做,那么写操做呢,也是如此事务之间互不干扰吗.再举例一个RR隔离级别下更新操做的例子2:假设事务A执行一个更新语句,知足更新条件A的的数据是2条,更新成功后不提交事务,此时事务B插入一条新的知足条件A的数据,此时事务A再按条件A去更新数据,实验发现事务B新插入的数据也被更新了.出现了幻读,这就是当前读,即对数据修改的操做(update、insert、delete)都会读到已提交事务的最新数据.

那么当前读的幻读问题如何解决呢?MVCC不能解决的问题固然是交给锁来解决了.上文提到的Next-Key Lock正是解决这个问题的方法,还以上面的例子2为例,给条件A字段非惟一索引,事务B进行插入数据的时候就会被阻塞,缘由是事务A持有了Gap Lock,只有事务A提交了,事务B才能成功插入数据.这就解决了当前读操做下的幻读问题.

因此MVCC机制可防止快照读引发的幻读,next-key锁可防止当前读引发的幻读.须要说明的是,MVCC只在RC和RR两个隔离级别下工做。其余两个隔离级别和MVCC不兼容, 由于 RU老是读取最新的数据行, 而不是符合当前事务版本的数据行.而SERIALIZABLE 则会对全部读取的行都加锁.

锁的触发和升级

以默认的InnoDB引擎RR级别说明,表锁能够理解为每一行记录都持有Record Lock,更新记录时,当更新字段没有走索引时,没法获取对应记录的Record Lock,行锁便会升级为表锁,这一点能够结合MySQL Explain去分析.须要注意的是当普通索引值区分度低时,此时观察Explain显示是走了索引的,但当另外一个事务并发操做不一样数据时,依然发现第二个事务会阻塞,这是由于MySQL的执行优化器认为给多行记录一次一次当加锁不如表锁来的高效,因此不会把这个普通索引当作索引,而当区分度高时,则认为是高效的,不会升级为表锁.因此,建立合适的索引很重要,区分度低的字段不建议建立索引.

何时会出现DeadLock

什么是死锁呢?死锁是指两个或两个以上的进程在执行过程当中,因为竞争资源或者因为彼此通讯而形成的一种阻塞的现象,若无外力做用,它们都将没法推动下去。此时称系统处于死锁状态或系统产生了死锁,这些永远在互相等待的进程称为死锁进程。举个例子4: 事务A获取 id=20的锁,事务B获取id=30的锁,而后,事务A试图获取id=30的锁,而该锁已经被事务B持有,因此事务A等待事务 B释放该锁,而后事务B又试图获取id =20 的锁这个锁被事务 A 占有,因而两个事务之间相互等待,这就会致使死锁.死锁的场景还有许多,归根结底,都是由于多个事务想要获取的锁互斥且获取的顺序不一致所形成.如何避免死锁呢,一般Record Lock引发的死锁问题开发时都会比较当心,但Gap Lock可能致使死锁的问题一般会被忽略,因此这一点要多加注意,另外就是创建合适的索引,若是没有索引,那么在操做数据时会锁住每一行,会增大死锁的几率.

锁问题的排查命令

show open tabbles; SHOW OPEN TABLES where In_use > 0;查看那些表被锁了
show status like 'table%';
table_locks_waited 出现表级锁定争用发生等待的次数,此值高说明存在验证的表记锁争用状况
table_locks_immediate 表示当即释放表锁的次数
show status like 'innodb_row_lock%'; Innodb_row_lock_current_waits 当前正在等待锁定的数量
Innodb_row_lock_time 系统启动到如今锁定总时间
Innodb_row_lock_time_avg 每次等待话费的平均时间 Innodb_row_lock_time_max 系统启动到如今等待最长一次所花时间 Innodb_row_lock_waits 系统启动后到如今共等待次数
information_schema
information_schema是MySQL专门记录性能信息的库,在5.7版本后默认打开
SELECT * FROM INFORMATION_SCHEMA.INNODB_LOCKS; 查看当前InnoDB的锁的信息,会显示是什么锁类型,属于那个事务ID
SELECT * FROM INFORMATION_SCHEMA.INNODB_TRX; 查看InnoDB事务ID,会显示是什么操做和一些常规信息,例如是否在运行running,仍是等待锁. SELECT * FROM INFORMATION_SCHEMA.INNODB_LOCK_WAITS;查看InnoDB锁的等待时间,和等待的是哪一个事务ID的锁

相关文章
相关标签/搜索