MySQL中的「锁」与「事务」

0.前言

一方面要最大程度地利用数据库的并发访问,另外一方面还要确保每个用户能以一致的方式读取和修改数据。因此,便有了「锁」,用于对共享资源的并发访问[1]。

事务(Transaction)是数据库区别于文件系统的重要特性之一。事务会把数据库从一种一致状态转换为另一种一致状态。在数据库提交工作时,可以确保要么所有修改都已经保存了,要么所有修改都不保存。
本文主要以InnoDB为例,学习锁和事务的设计与实现。

1.锁

锁分类

  • 悲观锁:认为数据随时会被修改,数据操纵过程中始终加锁
  • 乐观锁:认为自己操作数据时,没有人修改数据,不加锁,但是更新时会判断在此期间有无他人修改(需编码实现,比如基于version、timestamp)

通常,我们说的锁一般是悲观锁。以下,对悲观锁进行分类:

按作用范围

  • 表级锁:锁定整张表,封锁力度高,并发力度低,不会死锁
    • InnoDB提供了 行锁 和 表锁,默认是 行锁
  • 行级锁:锁定行,并发力度高,开销也高,可能会死锁
    • MyISAM提供 表锁

按使用性质

  • 共享锁(读锁/S锁):允许事务读一行数据
  • 排他锁(写锁/X锁):允许事务删除或更新一行数据

注意

  1. 共享锁、排他锁都是行锁
  2. 锁兼容问题:
    仅共享锁和共享锁之间可兼容,其他组合一律不兼容

锁的算法

InnoDB有三种行锁的算法:

  • Record Lock:单个行记录上的锁
  • Gap Lock:间隙锁,锁定一个范围,但不包含记录本身
  • Next-Key Lock:Recod Lock + Gap Lock,锁定一个范围,包含记录本身

锁带来的问题(并发一致性问题)

通过锁定机制可以实现事务的隔离性要求,使得事务可以并发地工作。锁提高了并发,但是却会带来潜在的问题。不过好在因为事务隔离性的要求,锁只会带来三种问题,如果可以防止这三种情况的发生,那将不会产生并发异常。

脏读-违反隔离性

脏数据是指未提交的数据,如果读到了脏数据,即一个事务可以读到另外一个事务中未提交的数据,则显然违反了数据库的隔离性。

脏读是指:在不同的事务下,当前事务可以读到其他事务未提交的数据,也就是读到了脏数据。发生条件是:事务的隔离级别是 READ UNCOMMITED

不可重复读-违反一致性

不可重复读是指在同一事务下,连续执行两次同样的SQL语句可能导致不同的结果,第二次的SQL语句可能会不一样(行数据被修改)。
不可重复读一般来说,也是可以接受的(数据已经提交)。

在InnoDB存储引擎中,通过使用Next-Key Lock算法来避免不可重复读的问题。在MySQL官方文档中将不可重复读的问题定义为Phantom Problem,即幻像问题。在Next-Key Lock算法下,对于索引的扫描,不仅是锁住扫描到的索引,而且还锁住这些索引覆盖的范围(gap)。因此在这个范围内的插入都是不允许的。这样就避免了另外的事务在这个范围内插入数据导致的不可重复读的问题。
–《MySQL技术内幕:InnoDB存储引擎》

幻读

幻读是指在同一事务下,连续执行两次同样的SQL语句可能导致不同的结果,第二次的SQL语句可能会返回之前不存在的行

注意
有时候,也把幻读放入不可重复读范畴。

丢失更新

一个事务的更新操作会被另一个事务的更新操作所覆盖,从而导致数据的不一致。

在当前数据库的任何隔离级别下,都不会导致数据库理论意义上的丢失更新问题。这是因为,即使是READ UNCOMMITTED的事务隔离级别,对于行的DML操作,需要对行或其他粗粒度级别的对象加锁。

但是在生产应用中还有另一个逻辑意义的丢失更新问题,而导致该问题的并不是因为数据库本身的问题。

2.事务

事务是访问并更新数据库中各种数据项的一个程序执行单元。在事务中的操作,要么都做修改,要么都不做,这就是事务的目的。

image

对于InnoDB存储引擎而言,其默认的事务隔离级别为READ REPEATABLE,完全遵循和满足事务的ACID特性。
MySQL 默认采用自动提交模式。也就是说,如果不显式使用START TRANSACTION语句来开始一个事务,那么每个查询都会被当做一个事务自动提交。

这里,具体介绍事务的ACID特性,并给出相关概念。

ACID特性

  • A(Atomicity),原子性。
    原子性指整个数据库事务是不可分割的工作单位。只有使事务中所有的数据库操作都执行成功,才算整个事务成功。事务中任何一个SQL语句执行失败,已经执行成功的SQL语句也必须撤销,数据库状态应该退回到执行事务前的状态。
  • C(consistency),一致性。一致性指事务将数据库从一种状态转变为下一种一致的状态。在事务开始之前和事务结束以后,数据库的完整性约束没有被破坏。
  • I(isolation),隔离性。隔离性还有其他的称呼,如并发控制(concurrency control)、可串行化(serializability)、锁(locking)等。事务的隔离性要求每个读写事务的对象对其他事务的操作对象能相互分离,即该事务提交前对其他事务都不可见,通常这使用锁来实现。
  • D(durability),持久性。事务一旦提交,其结果就是永久性的。即使发生宕机等故障,数据库也能将数据恢复。

注意:ACID不是平级的关系

  • 只有满足一致性,事务的执行结果才是正确的。
  • 在无并发的情况下,事务串行执行,隔离性一定能够满足。此时只要能满足原子性,就一定能满足一致性。
  • 在并发的情况下,多个事务并行执行,事务不仅要满足原子性,还需要满足隔离性,才能满足一致性。
  • 事务满足持久化是为了能应对数据库崩溃的情况。

image

事务的分类

  • 扁平事务(Flat Transactions)
  • 带有保存点的扁平事务(Flat Transactions with Savepoints)
  • 链事务(Chained Transactions)
  • 嵌套事务(Nested Transactions)
  • 分布式事务(Distributed Transactions)

事务隔离级别

  • 未提交读(READ UNCOMMITTED):事务中的修改,即使没有提交,对其它事务也是可见的
  • 提交读(READ COMMITTED):一个事务所做的修改在提交之前对其它事务是不可见的
  • 可重复读(REPEATABLE READ):保证在同一个事务中多次读取同样数据的结果是一样的
  • 可串行化(SERIALIZABLE):强制事务串行执行

READ UNCOMMITTED称为浏览访问(browse access),仅仅针对事务而言的。READ COMMITTED称为游标稳定(cursor stability)。REPEATABLE READ是2.9999°的隔离,没有幻读的保护。SERIALIZABLE称为隔离,或3°的隔离。

隔离级别 脏读可能性 不可重复读可能性 幻读可能性
未提交读
提交读
可重复读
可串行化

InnoDB存储引擎默认支持的隔离级别是REPEATABLE READ,但是与标准SQL不同的是,InnoDB存储引擎在REPEATABLE READ事务隔离级别下,使用Next-Key Lock锁的算法,因此避免幻读的产生。这与其他数据库系统(如Microsoft SQL Server数据库)是不同的。所以说,InnoDB存储引擎在默认的REPEATABLE READ的事务隔离级别下已经能完全保证事务的隔离性要求,即达到SQL标准的SERIALIZABLE隔离级别。

隔离级别越低,事务请求的锁越少或保持锁的时间就越短。这也是为什么大多数数据库系统默认的事务隔离级别是READ COMMITTED。

事务的实现

  • 隔离性:由锁实现
  • 原子性、持久性:由redo log实现
    • redo log 是InnoDB 引擎层特有
    • 物理日志,记录“某个数据页上做了什么修改”
    • 循环写的,空间固定会用完
    • 保证即使数据库发生异常重启,之前提交的记录都不会丢失,这个能力称为 crash-safe
    • 当有一条记录需要更新的时候,InnoDB 引擎就会先把记录写到 redo log 里面,并更新内存,这个时候更新就算完成了。
    • 同时,InnoDB 引擎会在适当的时候,将这个操作记录更新到磁盘里面,而这个更新往往是在系统比较空闲的时候做
  • 一致性:由undo log实现
    • undo log 是逻辑日志,因此只是将数据库逻辑地恢复到原来的样子
    • 记录了事务的行为,可以很好地通过其对页进行“重做”操作
    • undo log 会产生 redo log

3.多版本并发控制(MVCC)

MySQL的大多数事务型存储引擎实现的都不是简单的行级锁。基于提升并发性能的考虑,它们一般都同时实现了多版本并发控制(MVCC)。不仅是MySQL,包括Oracle、PostgreSQL等其他数据库系统也都实现了MVCC,但各自的实现机制不尽相同,因为MVCC没有一个统一的实现标准。

  • 可以认为MVCC是行级锁的一个变种,但是它在很多情况下避免了加锁操作,因此开销更低。虽然实现机制有所不同,但大都实现了非阻塞的读操作,写操作也只锁定必要的行。
  • MVCC只在REPEATABLE READ和READ COMMITTED两个隔离级别下工作。其他两个隔离级别都和MVCC不兼容,因为READ UNCOMMITTED总是读取最新的数据行,而不是符合当前事务版本的数据行。而SERIALIZABLE则会对所有读取的行都加锁。

InnoDB实现

MVCC的实现,是通过保存数据在某个时间点的快照来实现的。也就是说,不管需要执行多长时间,每个事务看到的数据都是一致的。根据事务开始的时间不同,每个事务对同一张表,同一时刻看到的数据可能是不一样的。

  • 创建时间
  • 过期时间

InnoDB的MVCC,是通过在每行记录后面保存两个隐藏的列来实现的。这两个列,一个保存了行的创建时间,一个保存行的过期时间(或删除时间)。当然存储的并不是实际的时间值,而是***系统版本号(system version number)***。

每开始一个新的事务,系统版本号都会自动递增。事务开始时刻的系统版本号会作为事务的版本号,用来和查询到的每行记录的版本号进行比较。下面看一下在REPEATABLE READ隔离级别下,MVCC具体是如何操作的。

SELECT

  • InnoDB只查找版本早于当前事务版本的数据行(确保事务读取的行,要么已存在,要么是事务自身插入/修改的)
  • 行的删除版本要么未定义,要么大于当前事务版本号(确保事务读取到的行,在事务开始之前未被删除)

INSERT

  • InnoDB为新插入的每一行保存当前系统版本号作为行版本号

DELETE

  • InnoDB为删除的每一行保存当前系统版本号作为行删除标识

UPDATE

  • InnoDB为插入一行新记录,保存当前系统版本号作为行版本号,同时保存当前系统版本号到原来的行作为行删除标识

本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!