阅读 879

MySQL学习系列之InnoDB下事务隔离机制

一. MySQL常用存储引擎

MyISAM(MySQL 5.5.5 之前默认的存储引擎)

特点:

  • 访问速度快。
  • 不支持事务,因此适用不要求事务完整性或以select和insert为主。
  • 锁粒度是支持并发的表级锁,这样的优点是加锁快,开销小,而对应的是并发性比较弱,容易引发锁冲突。

应用场景

  1. 在读写操作非常频繁的时候时候忌用,因为这样容易产生大量的锁冲突,形成大量的等待。
  2. 适用于查询为主的应用场景。

InnoDB(MySQL 5.5.5及以后默认的存储引擎)

特点:

  • 支持事务,支持回滚,因此适用于需要进行事务处理的应用场景
  • 锁粒度是支持MVCC(这里包含乐观锁的概念,下文进行解释)的行级锁,因此有很高的并发性
  • 支持外键

应用场景:

  • 适用于需要事务操作,例如修改、删除为主的项目中

ACID事务:

  • A事务的原子性(Atomicity):事务是一个不可分割的单元,事务中的所有操作,要么全做完,要么全不做

  • C 事务的一致性(Consistency):一致性,即在事务开始之前和事务结束以后,数据库的完整性约束没有被破坏。比如用户A给用户B转账,用户B给用户A转账,而约束就是两个人的总金额还是一样的

  • I 事务的隔离性(Isolation):多个事务并发访问时,事务之间是隔离的,一个事务不应该影响其它事务运行效果。在下面会扩展MySQL事务隔离级别。

  • D 事务的持久性(Durability):意味着在事务完成以后,该事务所对数据库所作的更改便持久的保存在数据库之中,并不会被回滚。

Archive

特点:

  • 不支持事务
  • 锁粒度是行级锁
  • 只支持insert和select

应用场景:

  • 适用于存储操作日志记录的数据

二、InnoDB中事务隔离级别

什么是脏读,不可重复读,幻读

  • 脏读: 对于两个事务A,B,事务A读取了已经被B更新但还没有提交的字段,之后,若B回滚,T1读取到的内容就是临时无效的内容。

  • 不可重复读: 在事务A中,多次对同一数据进行读取,此时事务B也对该数据进行访问,也许事务B的修改,事务A多次获得的数据可能不一样。

  • 幻读:事务A读取与搜索条件相匹配的若干行。事务B以插入或删除行等方式来修改事务A的结果集,然后再提交。这里幻读与不可重复读的差别在于不可重复读强调的是修改和删除,而幻读强调的是插入

MySQL中如何避免脏读、不可重复读、幻读

MySQL中存在四种隔离等级:

隔离级别 脏读 不可重复读 幻读
未提交读(Read uncommitted) 可能 可能 可能
已提交读(Read committed) 不可能 可能 可能
可重复读(Repeatable read) 不可能 不可能 可能
可串行读(Serializable) 不可能 不可能 不可能
  • 未提交读:允许脏读,可能会读取到别的事务中未提交的临时数据
  • 已提交读:只已经提交的数据,Oracle等数据库默认是这个级别
  • 可重复读:可重复读,InnoDB默认的等级是这个,可是还是出现幻读
  • 可串行读:完全串行化的读,表示每次加锁都是表级锁,而且读写相互阻塞,读是共享锁,写是排他锁

这里思考一下,为啥阻止幻读的隔离级别比不可重复读的高,因为InnoDB是行级锁,不可重复读是针对于对于正在修改或删除的数据行加锁,但还是可以对表进行插入,所以可能出现幻读,要避免幻读就要把表的读写都变成表级锁,才能避免幻读,也因此变成了隔离等级为“可串行读”。

PS:以上内容以 悲观锁 的概念可以更好理解,不过实际中出于性能考虑,是用以乐观锁 为理论基础的MVCC(多版本并发控制,Multi-Version Concurrency Control )

什么是悲观锁,什么是乐观锁

悲观锁:

悲观锁,正如其名,具有强烈的独占和排他特性。它指的是对数据被外界(包括本系统当前的其他事务,以及来自外部系统的事务处理)修改持保守态度,因此,在整个数据处理过程中,将数据处于锁定状态。悲观锁的实现,往往依靠数据库提供的锁机制(也只有数据库层提供的锁机制才能真正保证数据访问的排他性,否则,即使在本系统中实现了加锁机制,也无法保证外部系统不会修改数据)。
即在事务A中数据在进行读取(共享锁)的时候,其他事务不能进行修改(排他锁),当在事务A的数据进行修改(排他锁)的时候,不能进行读取(共享锁)。

优点:可以独自占有,直到执行操作完成,然后释放锁,为数据处理的安全提供了保障。

缺点:但是在效率方面,处理加锁的机制会让数据库产生额外的开销,还有增加产生死锁的机会


乐观锁:

悲观锁大多数情况下依靠数据库的锁机制实现,以保证操作最大程度的独占性。但随之而来的就是数据库 性能的大量开销,特别是对长事务而言,这样的开销往往无法承受。
而乐观锁就很好的解决了这个问题,乐观锁可以有两种方式实现,一种是version,一种是时间戳,这里以version为例,假设数据一般情况下不会发生冲突,只有在提交的时候,才会进行加锁,并判断这提交的事务的版本与当前数据库的版本的对比,如果提交的数据版本号大于数据库表当前版本号,则予以更新,否则认为是过期数据,则把请求驳回。

优点:

乐观锁机制避免了长事务中的数据库加锁开销,大大提升了大并发量下的系统整体性能表现。

缺点:

如果是在高并发下,很多用户容易出现冲突,即请求容易驳回


InnoDB中MVCC的实现:

MVCC的实现没有固定的规范,每个数据库中都会有不同的实现,这里讨论InnoDB的MVCC,其内部原理是通过乐观锁,在InnoDB中,会在每行数据后面添加两个额外的隐藏的值来实现MVCC,一个是这个数据何时被创建,一个是这数据何时过期。在实际中,这里存储的是版本号,每开启一个新的事务,事务的版本号就回增加。在可重读Repeatable reads事务隔离级别下:

  1. SELECT时,读取创建的版本号<=当前数据库的版本。删除版本为空或>当前当前事务版本的
  2. INSERT时,保存当前事务版本为行的创建版本
  3. DELETE时,保存当前版本为行的删除版本
  4. UPDATE时,插入一条新数据。保存当前事务版本为行创建版本,同一时候保存当前事务版本到原来删除的行
  5. 通过MVCC,尽管每行记录都须要额外的存储空间,很多其它的行检查工作以及一些额外的维护工作。但能够降低锁的使用,大多数读操作都不用加锁,读数据操作非常easy,性能非常好,而且也能保证仅仅会读取到符合标准的行。也仅仅锁住必要行。

通过MVCC,虽然每行记录都需要额外的存储空间,更多的行检查工作以及一些额外的维护工作,但可以减少锁的使用,大多数读操作都不用加锁,读数据操作很简单,性能很好,并且也能保证只会读取到符合标准的行,也只锁住必要行。

我们不管从数据库方面的教课书中学到,还是从网络上看到,大都是上文中事务的四种隔离级别这一模块列出的意思,RR级别是可重复读的,但无法解决幻读,而只有在Serializable级别才能解决幻读。于是我就加了一个事务C来展示效果。在事务C中添加了一条teacher_id=1的数据commit,RR级别中应该会有幻读现象,事务A在查询teacher_id=1的数据时会读到事务C新加的数据。但是测试后发现,在MySQL中是不存在这种情况的,在事务C提交后,事务A还是不会读到这条数据。可见在MySQL的RR级别中,是解决了幻读的读问题的。参见下图

MVCC中RR隔离等级下解决幻读示例图


MVCC中可能读取的之前版本的数据,要如何读取当前数据呢?

这里又引申出两个概念:快照读和当前读
快照读:就是普通的select

  • select * from table ...;

当前读:特殊的读操作(是要获取锁的),例如插入/更新/删除就是当前读

  • select * from table where ? lock in share mode;(共享锁)
  • select * from table where ? for update;(排他锁)
  • insert;
  • update;
  • delete;

事务的隔离级别实际上都是定义了当前读的级别,MySQL为了减少锁处理(包括等待其它锁)的时间,提升并发能力,引入了快照读的概念,使得select不用加锁。事务的隔离只定义了读数据的要求,而写的要求当然是"当前读"。

这里注意,InnoDB默认的是行级锁,行级锁都是基于索引的。在当前读的查询语句中,如果一条SQL语句用不到索引是不会使用行级锁的,会使用表级锁把整张表锁住,这点需要注意。

在MySQL中,insert、update、delete语句默认会对涉及的数据集加排他锁,在Read committed等级下select语句默认是不会加的,如果要加的话,则需要显示在后面加 lock in share mode。在Repeatable read以及在Serializable隔离机制下,select是加共享锁的。


我是MySQL菜鸟,如果对于本文中有什么疑问或者问题,欢迎互相讨论提高

参考内容:
《深入理解乐观锁与悲观锁》
《Innodb中的事务隔离级别和锁的关系》
《慕课网-数据库设计那些事》
《MySQL常用数据存储引擎区别》

文章分类
后端