浅谈MySQL数据库中的锁与事务

1、MySQL中的锁与锁策略

在MySQL中,为了应对并发场景下的读写,锁一般分为两类:共享锁以及排他锁。其中,共享锁容许多个链接在同一时间并发的读取相同的资源,彼此之间互不影响,因此又称为读锁。排他锁则会阻塞其余尝试获取共享锁或者排他锁的操做,确保同一时间只有一个链接能够写入数据,并禁止其余用户的读写,又称写锁。
在实际使用下,加锁每每意味着高昂的开销,MySQL为了平衡锁的开销以及并发的线程之间的安全,采用了两种不一样的锁策略:
  • table lock(表锁)
表锁会锁定整张表,若是当前有用户正在执行写操做而且获取了写锁,这可能致使整张表被锁定,阻塞其余用户的读写操做。若是用户执行的是读操做,则会获取读锁,此时其余用户的并发读操做将被接受,写操做会被阻塞。
举个例子,执行语句:
若是b字段不存在索引,那么会锁住全部的记录,即锁上了表锁。
  • row lock(行锁)
行锁的粒度是在每一条行数据,这意味行锁能够尽量的支持并发处理,相应的行锁开销也会比较大。而且,在InnoDB中的行锁是针对索引加的锁,不是针对记录加的锁,而且该索引不能失效,不然行锁将会自动升级为表锁。
相比较而言,表锁的优点在于开销小,加锁快,无死锁,劣势是锁的粒度大,发生锁冲突的几率较高,并发能力较弱。而行锁则相反。实际使用中,二者都会由MySQL自动加锁。行锁冲突能够经过执行 show status like 'innodb_row_lock%'语句进行分析,表锁冲突则可经过执行show status like 'table_locks%' 进行查看。

2、MySQL中的事务与隔离级别

事务就是一组原子性的sql,要么MySQL引擎会所有执行这一组sql语句,要么所有不行(不容许任何一条失败)。失败的语句将致使事务的整个回滚。事务系统一般知足四个特性,分别为原子性(要么所有执行、要么所有回滚)、一致性(数据必须从一个一致性状态转换为另外一种一致性状态)、隔离性(事务未执行成功,其余人没法看到结果)、持久性(事务在commit以后,数据不会丢失)。
由上述概念可知,事务是用来保障数据的一致性以及完整性的。也是MySQL中用来平衡效率与安全之间的一种手段,因此,InnoDB引擎下的事务一般提供了四种事务的隔离级别,方便用户本身在效率和安全之间作出权衡。
  • READ UNCOMMITED(未提交读)
事务中的修改,即便该事务未提交,对其余的事务也是可见的。能够读取到其余事务中的数据,又称为脏读,在实际数据库事务中,脏读会破坏数据的一致性,对业务产生极大影响,因此通常不推荐采用READ UNCOMMITED做为数据库事务的隔离级别。
  • READ COMMITED(提交读)
在提交读级别中,数据库将保证若是一个事务没有彻底执行成功(commit完成),事务中的操做对其余的事务是不可见的。在该隔离级别下,虽然杜绝了脏读的发生,可是仍是存在着不可重复读以及幻读的问题。不可重复读发生在事务T1读取了一行数据,事务T2接着修改或者删除了该行数据(已提交),当T1事务再次读取同一行数据的时候,发现数据已经被修改或者被删除。示例以下图:
幻读则发生在事务T1读取了知足某条件的一个数据集,事务T2此时插入了一行或者多行知足T1查询条件的的数据并提交,当T1再次采用相同的条件进行读取时,获得了与第一次不一样的结果集。示例以下:
  • REPEATABLE READ(提交读)
REPEATEABLE READ是MySQL的默认隔离级别,它确保同一个事务的多个实例在并发读取数据时,会看到一样的数据。按照该隔离级别定义,仍是会存在幻读的问题。MySQL经过InnoDB存储引擎的多版本并发控制机制(MVCC)解决了部分该问题的场景,但仍然存在,此处后文会另有分析。
  • SERIALIZABLE(串行化)
串行化是最严格的隔离级别,经过给事务中的每次读写操做都加锁,保证了不产生任何
脏读、不可重复读以及幻读问题,可是随之引入的是大量的读超时以及锁竞争,致使数据库性能的严重降低。
另外来看看ANSI SQL STANDARD中,对于数据库隔离级别以及相应问题的规定:
因此,对于REPEATABLE READ隔离级别下,是容许出现幻读的。

3、MySQL中的MVCC

MVCC(multiple-version-concurrency-control)是个行级锁的变种,它在普通读状况下避免了加锁操做,所以开销更低。其原理具体为,在InnoDB存储引擎中,每行数据会加入一些隐藏字段DATA_TRX_ID,DATA_ROLL_PTR,DB_ROW_ID,DELETE_BIT。DATA_TRX_ID 字段记录了数据的建立和删除时间,这个时间指的是对数据进行操做的事务的id,DATA_ROLL_PTR 指向当前数据的undo log记录,回滚数据就是经过这个指针,DELETE BIT位用于标识该记录是否被删除,这里的不是真正的删除数据,而是标志出来的删除。真正意义的删除是在mysql进行数据的GC,清理历史版本数据的时候。
相应的,其DML的处理方式也发生了变化:
SELECT语句先查找DATA_TRX_ID早于当前事务ID的数据行。这样就保证了读取的数据要么是在这个事务开始以前就已经commit了的(早于当前事务ID),要么是在这个事务中自身建立的数据(等于当前事务ID)。查找行的DELETE_BIT为1时,查找删除事务ID对应的事务,肯定此条记录在当前事务开始以前,行没有被删除。
INSERT语句会在新插入行数据以后,保存当前事务ID做为行的DATA_TRX_ID。
DELETE语句为每一条删除的记录保存当前的事务ID做为行的删除标记。
UPDATE语句将复制变动的记录,并把新记录的DATA_TRX_ID置为当前事务ID,同时更新老记录中的DB_ROLL_PT指向了上一个版本。
因此在并发读的时候,不须要等到访问行上的锁释放,只须要读取一个行的快照便可。既然是多版本的读取,就确定读取不到其余事务中的新插入的数据了,也就避免了上述场景中提到的幻读。

4、幻读

从上述信息咱们已经知道,在REPEATABLE READ级别下,InnoDB采起多版本策略成功
避免了部分幻读现象,可是实际使用中,仍是会有幻读产生,先看场景:
经过MVCC,在事务中的屡次读取不会出现幻读,可是此时的插入操做依旧会发生主键重复的错误,而且由于MVCC机制,在上图中的会话1不管读取多少次都不会读到致使冲突产生的数据,确实就如“幻影”通常诡异。
为了解决上述场景中的幻读,须要简单提一下InnoDB的行锁机制,在InnoDB引擎下存在三种行锁,分别为:
  • Record Lock:在单行记录上的锁
  • Gap Lock:间隙锁,锁定一个范围,但不包括记录自己,。GAP锁的目的,是为了防止同一事务的两次读出现幻读的状况
  • Next-Key Lock: 前两个锁的共同使用,即锁定了记录自己,也锁定了必定的范围。
一般状况下,INSERT/UPDATE/DELETE默认会在操做的记录上加上Next-Key Lock,而
普通的SELECT由于MVCC的关系反而只须要读取快照便可,因此若是业务须要再REPEATABLE READ场景下保证绝对不产生幻读,须要手动给SELECT加锁,在相似SELECT…WHERE加入FOR UPDATE(排它锁)或者LOCK IN SHARE MODE(共享锁)

5、总结

  • InnoDB中使用索引做为检索条件修改数据时采用行锁,不然使用表锁
  • InnoDB自动给修改操做加锁,给查询操做不自动加锁
  • 在REPEATABLE READ级别下,若是要彻底杜绝幻读,须要手动给关键查询语句加锁
  • 表的大部分数据须要修改时,行锁反而不如表锁更有效率
最后,本文只是就网易云信业务中数据库的一些使用场景和问题做了总结,数据库的实
际使用还存在诸多学问,但愿能抛砖引玉,让更多人分享出本身的心得。
相关文章
相关标签/搜索