MySQL学习笔记(二) -- 事务

1. 事务的定义

  事务  数据库事务是数据库管理系统执行过程中的一个逻辑单位,有一个有限的数据库操作序列完成。以 A 账户向B 账户汇钱 为例,一个事务是下面一个操作序列:

  1. 从 A 账号中把余额读出来
  2. 对 A 账号做减法操作
  3. 更新 A 账户的余额
  4. 从 B 账号中把余额读出来
  5. 对 B 账号做加法操作
  6. 更新 B 账户的余额
      以上过程为一个原子过程要么全部成功,要么全部失败

2. 事务的特性(ACID)

事务有四大特征,分别为原子性(Atomic),一致性(Consistency),隔离性(Isolation),持久性(Durability)

2.1 原子性(Atomic)

  事务中包含的操作被看做一个逻辑单元,这个逻辑单元中的操作要么全部成功,要么全部失败回滚。 事务的原子性也体现在事务对数据的读取上,例如一个事务对同一数据项的多次读取的结果一定是相同的。

2.2 一致性(Consistency)

  一致性是指事务必须使数据库从一个正确的一致性状态变换到另一个正确的一致性状态,也就是说一个事务执行之前和执行之后都必须处于一致性状态。比如,假设 用户A 和 用户B 两者的钱加起来一共是 5000,那么不管 A 和 B 之间如何转账,转几次账,事务结束后两个用户的钱相加起来应该还得是 5000,这就是事务的一致性。
  有些时候这种一致性由数据库的内部规则保证,例如数据的类型必须正确,数据值必须在规定的范围内,等等。另外一些时候这种一致性由应用保证的,例如,一般情况下银行账务余额不能是负数,信用卡消费不能超过该卡的信用额度等。

2.3 隔离性(Isolation)

  事务允许多个用户对同一个数据并发访问,而不会破坏数据的正确性和完整行。同时,并行事务的修改必须与其它事务的修改互相独立。事务的隔离性一般由锁来进行控制。许多时候数据库在并发执行多个事务,每个事务可能需要对多个表进行修改和查询,与此同时,更多的查询请求可能需要执行,数据库需要保证每一个事务在它的修改完成前对其他事务是不可见的
  换句话说就是不能让其它事务看到该事务的中间态,例如:要从 A 账户给 B 转 a 元,不能让其它账户看到(有其他事务查询 A,B 的余额) A 账户扣了 a 元,但是 B 账户没有增加 a 元的状态。

2.4 持久性(Durability)

  事务结束后,事务处理的结果必须能够得到固化,即使系统出现各种异常也是如此。一个事务一旦被提交了,那么对数据库中数据的改变就是永久性的,即便是在数据库系统遇到故障的情况下也不会丢失提交数据的操作。

3. 事务的实现

image.png
  在介绍事务的实现原理之前,首先了解一下 MySQL 的事务日志,请移步InnoDB 存储引擎提供了两种事务日志:redo log(重做日志)和 undo log(回滚日志)。其中 redo log 用于保证事务持久性;undo log 则是事务原子性和隔离性实现的基础。

3.1 原子性实现原理

  实现原子性的关键在于,当事务中的某一个操作失败后能够保证回滚所有已经成功的 SQL 语句。InnoDB 通过 undo log 来实现回滚操作。当事务对数据库进行修改时, InnoDB 会生成对应的 undo log(同时也会生成 redo log 来保证 undo log 的持久化),如果事务执行失败或者调用了 rollback,导致事务需要回滚,便可以利用 undo log 中的信息将数据回滚到修改之前的样子。

3.2 隔离性实现原理

这里感觉是很大一块东西觉得有必要抽出来单独讲,这里先简单说下吧

3.2.1 为什么需要隔离性?

  正如之前隔离性所说的,一个事务不能将自己的中间态让其它事务感知,所以按道理某个事务对某个数据进行操作时,其它事务应该排队,当该事务提交后,其它事务才可以继续访问这个数据,但是这样会大大降低 MySQL 的执行效率。所以数据库提出了各种 隔离级别,来最大限度的提升系统并发处理事务的能力。系统并发性与隔离性时相悖的,并发性好隔离性就查,需要按照特定的使用场景兼顾并发与隔离。

3.2.2 没有隔离性的危害

   为什么一定要隔离?不隔离有哪些影响呢?

  • 脏写:脏写是指事务回滚了其他事务对数据项的已提交修改,比如下面这种情况
    image.png
    在事务1对数据A的回滚,导致事务2对A的已提交修改也被回滚了。

  • 更新丢失:丢失更新是指事务覆盖了其他事务对数据的已提交修改,导致这些修改好像丢失了一样。
    image.png
    事务1 和 事务2 读取 A 的值都为 10,事务2 先将 A 加上 10 并提交修改,之后 事务2 将 A 减少 10 并提交修改,A 的值最后为 0,导致 事务2 对 A 的修改好像丢失了一样.

  • 脏读:指一个事务读取到另一个事务未提交的数据
    image.png
    在 事务1 对 A 的处理过程中,事务2 并发读取 A,但之后 事务1 因为一些情况导致事务回滚,导致 事务2 读取到的是未提交的脏数据。

  • 不可重复读:指一个事务对同一数据的读取结果见后不一致。脏读和不可重复读的区别在于,前者读取的是事务未提交的数据,后者读取的是已提交的数据,只不过因为数据被其他事务修改过导致前后两次读取的结果不一样。
    image.png
    由于 事务2 对 A 的已提交修改,事务1 前后两次读取的结果不一致。

  • 幻读:指事务读取某个范围的数据时,其它事务的操作导致前后两次读取的结果不一致。幻读和不可重复读的区别是,不可重复读是针对确定的某一行数据而言,而幻读是针对不确定的多行数据。因而幻读通常出现在带有查询条件的范围查询中。
    image.png
    事务1 查询 A < 5 的数据,由于 事务2 插入了一条 A = 4 的数据,导致 事务1 两次查询得到的结果不一样

3.2.3 隔离级别

  为了避免上述并发处理事务产生的问题,同时兼顾并发的性能,SQL 标准为事务定义了不同的隔离级别,从低到高依次是

  • 读未提交(READ UNCOMMITTED)
  • 读已提交(READ COMMITTED)
  • 可重复读(REPEATABLE READ)
  • 串行化(SERILIZABLE)

  事务的隔离级别越低,可能出现的并发异常越多,但是通常而言系统能提供的并发能力越强。不同的隔离级别与可能的并发异常的对应情况如下表所示,有一点需要强调,这种对应关系只是理论上的,对于特定的数据库实现不一定准确,比如 MySQLInnoDB 存储引擎通过 Next-Key Locking 技术在可重复读级别就消除了幻读的可能。

image.png

  所有事务隔离级别都不允许出现脏写,而串行化可以避免所有可能出现的并发异常,但是会极大的降低系统的并发处理能力。

3.2.4 锁机制

关于锁这里会拿出单独的篇幅进行讲解,这么做一是要真的把原理和实现细节讲清楚需要涉及的东西太多,篇幅太长,从作者和读者角度而言都不是一件轻松的事,所以只对其实现的核心思想和实现要点进行了简单的介绍,其他部分就一笔带过了。

  首先来看两个事务的写操作之间的相互影响。隔离性要求同一时刻只能有一个事务对数据进行写操作,InnoDB 通过锁机制来保证这一点。
  锁机制的基本原理可以概括为:事务在修改数据之前,需要获得相应的锁;获得锁以后,事务便可以修改数据;该事务操作期间,这部分数据是锁定的,如果其他事务需要修改数据,需要等到当前事务操作结束释放锁后才能拿锁操作。

3.2.4.1 行锁与表锁

  按照粒度,锁可以分为行锁与表锁以及其它位于二者之间的锁比如间隙锁。

  • 表锁在操作数据的时候会锁定整张表,并发性能较差
  • 行锁只需要锁定需要操作的数据,并发性能好
      但是由于枷锁本身需要耗费资源(获取锁,检查锁,释放锁等),因此锁定数据比较多情况下下可以使用间隙锁或者表锁可以节省大量资源。 MySQL 中不同的存储引擎支持的锁是不一样的。例如 MyISAM 只支持表锁,而 InnoDB 支持表锁和行锁。

3.2.5 MVCC(多版本并发控制)

   RR 解决脏读、不可重复读、幻读等问题,使用的是 MVCC(Multi-Version Concurrency Control),即多版本的并发控制协议。下面的例子很好的体现了 MVCC 的特点:在同一时刻,不同的事务读取到的数据可能是不同的(即多版本)——在T5时刻,事务A和事务C可以读取到不同版本的数据。
image.png
  MVCC 最大的优点是读不加锁,因此读写不冲突,并发性能好。InnoDB 实现 MVCC,多个版本的数据可以共存,主要基于以下技术及数据结构:

  • 隐藏列:InnoDB中每行数据都有隐藏列,隐藏列中包含了本行数据的 事务ID、指向 undo log 的指针等
  • 版本链:前面说到每行数据的隐藏列中包含了指向undo log的指针,而每条undo log也会指向更早版本的undo log,从而形成一条版本链
    image.png
  • ReadView:通过隐藏列和版本链,MySQL可以将数据恢复到指定版本;但是具体要恢复到哪个版本,则需要根据 ReadView 来确定。所谓 ReadView,是指事务(记做事务A)在某一时刻给整个事务系统(trx_sys)打快照,之后再进行读操作时,会将读取到的数据中的 事务id 与 trx_sys 快照比较,从而判断数据对该 ReadView 是否可见,即对事务A是否可见

trx_sys中的主要内容,以及判断可见性的方法如下:

  • low_limit_id:表示生成 ReadView 时系统中应该分配给下一个事务的id。如果数据的事务id大于等于low_limit_id,则对该 ReadView 不可见
  • up_limit_id:表示生成 ReadView 时当前系统中活跃的读写事务中最小的事务id。如果数据的事务id小于 up_limit_id,则对该 ReadView 可见
  • rw_trx_ids:表示生成 ReadView 时当前系统中活跃的读写事务的事务id列表。如果数据的事务id在 low_limit_idup_limit_id 之间,则需要判断事务id是否在 rw_trx_ids 中:如果在,说明生成ReadView 时事务仍在活跃中,因此数据对 ReadView 不可见;如果不在,说明生成 ReadView时事务已经提交了,因此数据对 ReadView 可见。

  MySQL 中,READ COMMITTEDREPEATABLE READ 隔离级别的的一个非常大的区别就是它们生成 ReadView 的时机不同。

3.3 持久性实现原理

  首先了解到为了加快 MySQL 的执行效率,减小 I/O 操作次数,InnoDB 提供了缓存机制 Buffer Pool(详细了解缓存机制请移步)。当需要向数据库写入数据时,首先会写入 Buffer Pool 中,然后在通过后台线程定期刷到磁盘中(这一个过程称为刷脏)。
   Buffer Pool 的使用极大的提高了 MySQL 的读写效率,但是也带来的新的问题。由于是在内存中的数据,如果 MySQL 宕机而此时 Buffer Pool 中的数据还没有刷新到磁盘,那么就会导致数据的丢失,事务的持久性无法保证。
  于是, redo log 被引入来解决这个问题。当数据被修改时,除了会直接修改 Buffer Pool 中的数据,会同时在 redo log 记录这些操作;当事务提交时会调用 fsync 接口对 redo log 进行刷盘。如果 MySQL 宕机,重启时可以读取 redo log 中的数据对数据库进行恢复。 redo log采用的时 WAL(Write Ahead Logging,预写式日志),所以修改先写入日志,再更新到 Buffer Pool,保证了数据不会因为 MySQL 宕机而丢失,从而满足了事务的一致性。

4. 总结

  事务是数据库系统进行并发控制的基本单位,是数据库系统进行故障恢复的基本单位,从而也是保持数据库状态一致性的基本单位。ACID 是事务的基本特性,数据库系统是通过并发控制技术和日志恢复技术来对事务的 ACID 进行保证的,从而可以得到如下的关于数据库事务的概念体系结构。
image.png

5. 参考文献