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

  1. MySQL中的锁与锁策略

在MySQL中,为了应对并发场景下的读写,锁通常分为两类:共享锁以及排他锁。其中,共享锁允许多个连接在同一时间并发的读取相同的资源,彼此之间互不影响,所以又称为读锁。排他锁则会阻塞其他尝试获取共享锁或者排他锁的操作,确保同一时间只有一个连接可以写入数据,并禁止其他用户的读写,又称写锁。

在实际使用下,加锁往往意味着高昂的开销,MySQL为了平衡锁的开销以及并发的线程之间的安全,采用了两种不同的锁策略:

 

  1. table lock(表锁)

表锁会锁定整张表,如果当前有用户正在执行写操作并且获取了写锁,这可能导致整张表被锁定,阻塞其他用户的读写操作。如果用户执行的是读操作,则会获取读锁,此时其他用户的并发读操作将被接受,写操作会被阻塞。

举个例子,执行语句

:

如果b字段不存在索引,那么会锁住所有的记录,即锁上了表锁。

 

  1. row lock(行锁)

行锁的粒度是在每一条行数据,这意味行锁可以尽可能的支持并发处理,相应的行锁

开销也会比较大。并且,在InnoDB中的行锁是针对索引加的锁,不是针对记录加的锁,并且该索引不能失效,否则行锁将会自动升级为表锁

 

       相比较而言,表锁的优势在于开销小,加锁快,无死锁,劣势是锁的粒度大,发生锁冲突的概率较高,并发能力较弱。而行锁则相反。实际使用中,两者都会由MySQL自动加锁。行锁冲突可以通过执行 show status like 'innodb_row_lock%'语句进行分析,表锁冲突则可通过执行show status like 'table_locks%' 进行查看。

 

 

 

  1. MySQL中的事务与隔离级别

事务就是一组原子性的sql,要么MySQL引擎会全部执行这一组sql语句,要么全部不

行(不允许任何一条失败)。失败的语句将导致事务的整个回滚。事务系统通常满足四个特性,分别为原子性(要么全部执行、要么全部回滚)、一致性(数据必须从一个一致性状态转换为另一种一致性状态)、隔离性(事务未执行成功,其他人无法看到结果)、持久性(事务在commit之后,数据不会丢失)。

       由上述概念可知,事务是用来保障数据的一致性以及完整性的。也是MySQL中用来平衡效率与安全之间的一种手段,所以,InnoDB引擎下的事务通常提供了四种事务的隔离级别,方便用户自己在效率和安全之间做出权衡。

 

  1. READ UNCOMMITED(未提交读)

事务中的修改,即使该事务未提交,对其他的事务也是可见的。可以读取到其他事务

中的数据,又称为脏读,在实际数据库事务中,脏读会破坏数据的一致性,对业务产生极大影响,所以一般不推荐采用READ UNCOMMITED作为数据库事务的隔离级别。

  1. READ COMMITED(提交读)

在提交读级别中,数据库将保证如果一个事务没有完全执行成功(commit完成),事务中的操作对其他的事务是不可见的。在该隔离级别下,虽然杜绝了脏读的发生,但是还是存在着不可重复读以及幻读的问题。不可重复读发生在事务T1读取了一行数据,事务T2接着修改或者删除了该行数据(已提交),当T1事务再次读取同一行数据的时候,发现数据已经被修改或者被删除。示例如下图:

幻读则发生在事务T1读取了满足某条件的一个数据集,事务T2此时插入了一行或者多行满足T1查询条件的的数据并提交,当T1再次采用相同的条件进行读取时,得到了与第一次不同的结果集。示例如下:

  1. REPEATABLE READ(提交读)

REPEATEABLE READ是MySQL的默认隔离级别,它确保同一个事务的多个实例在并

发读取数据时,会看到同样的数据。按照该隔离级别定义,还是会存在幻读的问题。MySQL通过InnoDB存储引擎的多版本并发控制机制(MVCC)解决了部分该问题的场景,但仍然存在,此处后文会另有分析。

 

  1. SERIALIZABLE(串行化)

串行化是最严格的隔离级别,通过给事务中的每次读写操作都加锁,保证了不产生任何

脏读、不可重复读以及幻读问题,但是随之引入的是大量的读超时以及锁竞争,导致数据库性能的严重下降。

 

另外来看看ANSI SQL STANDARD中,对于数据库隔离级别以及相应问题的规定:

所以,对于REPEATABLE READ隔离级别下,是允许出现幻读的。

 

 

  1. 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指向了上一个版本。

所以在并发读的时候,不需要等到访问行上的锁释放,只需要读取一个行的快照即可。既然是多版本的读取,就肯定读取不到其他事务中的新插入的数据了,也就避免了上述场景中提到的幻读。

  1. 幻读

从上述信息我们已经知道,在REPEATABLE READ级别下,InnoDB采取多版本策略成功

避免了部分幻读现象,但是实际使用中,还是会有幻读产生,先看场景:

通过MVCC,在事务中的多次读取不会出现幻读,但是此时的插入操作依旧会发生主键重复的错误,并且因为MVCC机制,在上图中的会话1无论读取多少次都不会读到导致冲突产生的数据,确实就如“幻影”一般诡异。

 

 

为了解决上述场景中的幻读,需要简单提一下InnoDB的行锁机制,在InnoDB引擎下存在三种行锁,分别为:

    1. Record Lock:在单行记录上的锁
    2. Gap Lock:间隙锁,锁定一个范围,但不包括记录本身,。GAP锁的目的,是为了防止同一事务的两次读出现幻读的情况
    3. Next-Key Lock: 前两个锁的共同使用,即锁定了记录本身,也锁定了一定的范围。

 

通常情况下,INSERT/UPDATE/DELETE默认会在操作的记录上加上Next-Key Lock,而

普通的SELECT因为MVCC的关系反而只需要读取快照即可,所以如果业务需要再REPEATABLE READ场景下保证绝对不产生幻读,需要手动给SELECT加锁,在类似SELECT…WHERE加入FOR UPDATE(排它锁)或者LOCK IN SHARE MODE(共享锁)。

 

  1. 总结
  1. InnoDB中使用索引作为检索条件修改数据时采用行锁,否则使用表锁
  2. InnoDB自动给修改操作加锁,给查询操作不自动加锁
  3. 在REPEATABLE READ级别下,如果要完全杜绝幻读,需要手动给关键查询语句加锁
  4. 表的大部分数据需要修改时,行锁反而不如表锁更有效率

最后,本文只是就网易云信业务中数据库的一些使用场景和问题作了总结,数据库的实

际使用还存在诸多学问,希望能抛砖引玉,让更多人分享出自己的心得。

邀请好友使用网易云信,好友下单成功即可获得500元网易考拉/严选无门槛现金券,点击立即推荐>>


网易云信(NeteaseYunXin)是集网易19IM以及音视频技术打造的PaaS服务产品,来自网易核心技术架构的通信与视频云服务,稳定易用且功能全面,致力于提供全球领先的技术能力和场景化解决方案。开发者通过集成客户端SDK和云端OPEN API,即可快速实现包含IM、音视频通话、直播、点播、互动白板、短信等功能。

 

发布了90 篇原创文章 · 获赞 18 · 访问量 5万+

猜你喜欢

转载自blog.csdn.net/netease_im/article/details/104679075