MySQL - 事务与隔离级别

前言

本篇文章将以事务为主线,分别介绍事务的基本特性、事务并发问题、封锁协议、隔离级别及基本实现,最后简单介绍下 MySQL 对标准的隔离级别规范的实现。

事务基本特性

  • 原子性(Atomicity)

    要么全部完成,要么全部不完成。

  • 一致性(Consistency)

    一个事务单元需要提交之后才会被其它事务可见。

  • 隔离性(Isolation)

    并发事务之间不会相互影响。

  • 持久性(Durability)

    事务提交后即持久化到存储设备上。

注意,隔离性和一致性是有冲突的,有时候为了提高性能,会适度的破坏一致性,而这个权衡的结果会造成事务并发问题。

事务并发问题

  • 丢失修改

    回滚覆盖:回滚一个事务时,在该事务内的写操作要回滚,把其它已提交的事务写入的数据覆盖了。
    提交覆盖:提交一个事务时,把其它已提交的事务写入的数据覆盖了。

    上图描述的是回滚覆盖问题。

  • 脏读

    一个事务读取到另一个未提交事务修改过的数据。

  • 不可重复读

    一个事务中先后根据相同条件读取到的数据不一致。强调更新和删除操作。

  • 幻读

    一个事务中先后根据相同条件读取的数据记录数不一致。强调新增操作。

封锁协议

封锁类型

为了解决并发问题,数据库系统引入了锁锁机制。在事务T对某个数据对象操作之前,先向系统发出请求对其加锁。基本的封锁类型有两种,排它锁(Exclusive locks 简记为X锁)共享锁(Share locks 简记为S锁),其中前者又称写锁,后者又称读锁。

  • 排它锁(X锁):若事务T对数据对象A加上X锁,其它任何事务都不能在对A加任何类型的锁,直到事务T释放A上的锁为止。这就保证了其他事务在T释放A上的锁之前不能再读取和修改A。
  • 共享锁(S锁):若事务T对数据对象A加上S锁,其它事务只能再对A加S锁而不能加X锁,直到事务T释放A上的S锁为止。

封锁协议

在运用X锁和S锁对数据对象加锁时,还需要约定一些规则,例如何时申请X锁或S锁、持锁时间、何时释放等,称这些加锁规则为封锁协议(Locking Protocol)。对封锁方式规定不同的规则,就形成了各种不同的封锁协议。

  • 一级封锁协议

    定义:事务T在修改数据R之前必须先对其加X锁,直到事务结束才释放。事务结束包括正常结束(COMMIT)和非正常结束(ROLLBACK)。
    说明:一级封锁协议可以防止丢失修改,并保证事务T是可恢复的。使用一级封锁协议可以解决丢失修改问题。在一级封锁协议中,如果仅仅是读数据不对其进行修改,是不需要加锁的,它不能保证可重复读和不读“脏”数据。

  • 二级封锁协议

    定义:一级封锁协议基础上加事务T在读取数据R之前必须先对其加S锁,读完后方可释放S锁。
    说明:二级封锁协议除防止了丢失修改,还可以进一步防止读“脏”数据。但在二级封锁协议中,由于读完数据后即可释放S锁,所以它不能保证可重复读。

  • 三级封锁协议

    定义:一级封锁协议基础上加事务T在读取数据R之前必须先对其加S锁,直到事务结束才释放。
    说明:三级封锁协议除防止了丢失修改和不读“脏”数据外,还进一步防止了不可重复读。

上述三级协议的主要区别在于什么操作需要申请封锁,以及何时释放。

事务隔离级别

为了解决事务并发问题,进行并发控制,数据库系统提供了四种事务隔离级别。本质上三级封锁协议反映在实际的数据库系统上,就是四种事务隔离机制。总的来说,四种事务隔离机制就是在逐渐的限制事务的自由度,以满足对不同并发控制程度的要求。

隔离级别

  • 读未提交(Read Uncommitted)

    可以读取未提交的记录,会出现脏读,幻读,不可重复读,所有并发问题都可能遇到。

  • 读已提交(Read Committed)

    只能读取到已经提交的数据。不会出现脏读现象,但是会出现幻读,不可重复读;(大多数数据库的默认隔离级别都是 RC,但是 MySQL InnoDb 默认是 RR)。

  • 可重复读(Repeated Read)

    在同一个事务内的查询都是事务开始时刻一致的,MySQL InnoDb 默认的隔离级别,解决了不可重复读问题,但是仍然存在幻读问题。

  • 串行化(Serializable)

    所有的增删改查串行执行,啥并发问题都没有。

需要明确的是,以上的隔离级别是在SQL规范层面的定义,不同数据库的实现方式和使用方式并不相同,类似于JVM规范和JVM厂商的关系。

传统的隔离级别实现

SQL 规范中定义的四种隔离级别,分别是为了解决事务并发时可能遇到的四种问题,至于如何解决,实现方式是什么,规则中并没有严格定义。锁作为最简单最显而易见的实现方式被广为人知,因此我们在讨论某个隔离级别的时候,通常会说这个隔离级别的加锁方式是什么样的。其实,锁只是实现隔离级别的方式之一,除了锁,实现并发问题的方式还有时间戳,多版本控制等等,这些也可以称为无锁的并发控制。

采用基于锁的并发控制实现,通过对读写操作加不同的锁,以及对释放锁的时机进行不同的控制,就可以实现四种隔离级别。

MySQL事务隔离级别

虽然数据库的四种隔离级别通过基于锁的并发控制(Lock-Based Concurrent Control,简写 LBCC) 技术都可以实现,但是它最大的问题是只实现了并发的读读,对于并发的读写还是冲突的,写时不能读,读时不能写,当读写操作都很频繁时,数据库的并发性将大大降低。针对这种场景,MVCC 技术应运而生,全称叫做 Multi-Version Concurrent Control(多版本并发控制),为了兼容落后的规范,数据库引擎厂商都想办法贴近四大隔离级别,但是和标准可能会有差别。

InnoDB 对事务隔离级别的实现依赖两个重要手段:LBCC、MVCC(多版本并发控制)。MVCC 可以认为是对锁机制的优化,让普通 SELECT 避免加锁,同时保证事务隔离级别的语义。

InnoDB 默认的事务隔离级别是 RR 隔离级别,它采用通过 MVCC间隙锁 解决了标准的 RR 级别下存在的幻读问题。因为 幻读 的这个字在 MySQL 里本身就存在歧义,这个指的是快照读还是当前读呢?如果是快照读,MySQL 通过版本链来保证同一个事务里每次查询得到的结果集都是一致的;如果是当前读,MySQL 通过间隙锁保证其他事务无法插入新的数据,从而避免幻读问题。当然,如果场景中一会是快照读,一会是当前读,导致幻读现象,那就太为难 MySQL 了。

InnoDB 对串行化隔离级别是通过 临键锁 实现的,普通 SELECT 语句使用 S临键锁,当前读语句使用 X临键锁,加锁规则和 RR 隔离级别一致。

小结

本篇文章主要对事务隔离级别的规范以及传统实现原理进行了介绍,并对 MySQL 的事务隔离级别的实现进行了简单说明。有了对事务整体的深入了解,对于理解 MySQL 中的锁机制、MVCC 原理会有很大的帮助。如果不知道事务隔离级别的基本实现,或者不清楚事务隔离级别和锁的关系,那么对于 MySQL 只能是管中窥豹。关于锁机制、MVCC原理会在后面的文章详细说明。