事务详解

128 阅读11分钟

本节我要向和我一样在学习MySQL的伙伴们介绍一下事务。事务这个东西,很常见,大家在找工作时,在简历MySQL技能那里都会写上“熟悉MySQL,了解事务、索引、MVCC、日志、锁”等等。这样写的话,也可以体现出事务的重要性。
我会向大家介绍以下事务,如果有哪里理解不当或者表述错误,欢迎大家指正。
本文主要内容如下: 事务.png

什么是事务?

首先,事务是什么呢?又有什么用呢?为什么出现事务这个东西呢?事务使用需要注意什么呢?事务怎么去使用呢?像这些疑问大家应该都有过。
首先,事务是逻辑上的一组操作,要么都执行,要么都不执行。 在谈论事务的时候,一般指的就是数据库事务。 简单来说,数据库事务可以保证多个对数据库的操作(也就是 SQL 语句)构成一个逻辑上的整体。构成这个逻辑上的整体的这些数据库操作遵循:要么全部执行成功,要么全部不执行 。
从网上偷了一张guide哥网站上的图,如下图所示:

数据库事务示意图.png 事务是为了解决数据库数据不一致问题提出的概念,目前事务这个概念大家在各处都能看到,比如redis、分布式系统都有事务。

事务特性有什么呢?

事务有四大特性,分别是:

  • 原子性Atomicity):事务是最小的执行单位,不允许分割。事务的原子性确保动作要么全部完成,要么完全不起作用;
  • 一致性Consistency):执行事务前后,数据保持一致,例如转账业务中,无论事务是否成功,转账者和收款人的总额应该是不变的;
  • 隔离性Isolation):并发访问数据库时,一个用户的事务不被其他事务所干扰,各并发事务之间数据库是独立的;
  • 持久性Durability):一个事务被提交之后。它对数据库中数据的改变是持久的,即使数据库发生故障也不应该对其有任何影响。

即ACID,其中AID->C。也就是说原子性、隔离性、持久性都是手段,是为了达到一致性的手段。这又该如何解释呢?我会在后面给大家说下原因的。

事务如何开启呢?

事务有两种启动方式。分别是以下两种:

  1. 显式启动事务语句, begin 或 start transaction。配套的提交语句是 commit,回滚语句是 rollback。
  2. set autocommit=0,这个命令会将这个线程的自动提交关掉。意味着如果你只执行一个 select 语句,这个事务就启动了,而且并不会自动提交。这个事务持续存在直到你主动执行 commit 或 rollback 语句,或者断开连接。

哪种启动方式更好呢?

显式启动更好。大家可能会纠结显式启动有“多一次交互”的问题。对于一个需要频繁使用事务的业务,第二种方式每个事务在开始时都不需要主动执行一次 “begin”,减少了语句的交互次数。大家有这个顾虑很正常,可以使用 commit work and chain 语法去优化。 在 autocommit 为 1 的情况下,用 begin 显式启动的事务,如果执行 commit 则提交事务。如果执行 commit work and chain,则是提交事务并自动启动下一个事务,这样也省去了再次执行 begin 语句的开销。同时带来的好处是从程序开发的角度明确地知道每个语句是否处于事务中。

事务隔离

在应用程序中,事务不是单个执行的,一般都是多个事务并发运行,经常会操作相同的数据来完成各自的任务(多个用户对同一数据进行操作)。并发虽然是必须的,但可能会导致一些问题。

并发事务会引起什么问题呢?

数据库多个事务同时运行时,就可能出现脏读、幻读、不可重复读。
第一是脏读, 当一个事务正在访问数据并且对数据进行了修改,而这种修改 还没有提交到数据库中,这时另外一个事务也访问了这个数据,因为这个数 据是还没有提交的数据,那么另外一个事务读到的这个数据是“脏数据”,依据“脏数据”所做的操作可能是不正确的。
第二是不可重复读:比如在一个事务内多次读同一数据。在这个事务还没有 结束时,另一个事务也访问该数据。那么,在第一个事务中的两次读数据之 间,由于第二个事务的修改导致第一个事务两次读取的数据可能不太一样。 这就发生了在一个事务内两次读到的数据是不一样的情况,因此称为不可重复读。
第三是幻读(Phantom read):幻读与不可重复读类似。它发生在一个事务 (T1)读取了几行数据,接着另一个并发事务(T2)插入了一些数据时。在 随后的查询中,第一个事务(T1)就会发现多了一些原本不存在的记录,就 好像发生了幻觉一样,所以称为幻读。

并发事务的控制方式有哪些呢?

MySQL 中并发事务的控制方式无非就两种: 和 MVCC。锁可以看作是悲观控制的模式,多版本并发控制(MVCC,Multiversion concurrency control)可以看作是乐观控制的模式。

 控制方式下会通过锁来显式控制共享资源而不是通过调度手段,MySQL 中主要是通过 读写锁 来实现并发控制。

  • 共享锁(S 锁) :又称读锁,事务在读取记录的时候获取共享锁,允许多个事务同时获取(锁兼容)。
  • 排他锁(X 锁) :又称写锁/独占锁,事务在修改记录的时候获取排他锁,不允许多个事务同时获取。如果一个记录已经被加了排他锁,那其他事务不能再对这条记录加任何类型的锁(锁不兼容)。

读写锁可以做到读读并行,但是无法做到写读、写写并行。另外,根据根据锁粒度的不同,又被分为 表级锁(table-level locking)  和 行级锁(row-level locking)  。InnoDB 不光支持表级锁,还支持行级锁,默认为行级锁。行级锁的粒度更小,仅对相关的记录上锁即可(对一行或者多行记录加锁),所以对于并发写入操作来说, InnoDB 的性能更高。不论是表级锁还是行级锁,都存在共享锁(Share Lock,S 锁)和排他锁(Exclusive Lock,X 锁)这两类。

MVCC 是多版本并发控制方法,即对一份数据会存储多个版本,通过事务的可见性来保证事务能看到自己应该看到的版本。通常会有一个全局的版本分配器来为每一行数据设置版本号,版本号是唯一的。

MVCC 在 MySQL 中实现所依赖的手段主要是: 隐藏字段、read view、undo log

  • undo log : undo log 用于记录某行数据的多个版本的数据。
  • read view 和 隐藏字段 : 用来判断当前版本数据的可见性。

事务隔离级别

那这该怎么去解决脏读、幻读、不可重复读呢? 上事务隔离级别。事务隔离级别有四种,分别是读未提交、读已提交、可重复读、串行化。

  • 未提交是指,一个事务还没提交时,它做的变更就能被别的事务看到。
  • 读提交是指,一个事务提交之后,它做的变更才会被其他事务看到。
  • 可重复读是指,一个事务执行过程中看到的数据,总是跟这个事务在启动时看到的数据是一致的。当然在可重复读隔离级别下,未提交变更对其他事务也是不可见的。
  • 串行化,顾名思义是对于同一行记录,“写”会加“写锁”,“读”会加“读锁”。当出现读写锁冲突的时候,后访问的事务必须等前一个事务执行完成,才能继续执行。

事务隔离如何实现呢?

在 MySQL 中,实际上每条记录在更新的时候都会同时记录一条回滚操作。记录上的最新值,通过回滚操作,都可以得到前一个状态的值。 查询这条记录的时候,不同时刻启动的事务会有不同的 read-view。 同一条记录在系统中可以存在多个版本,就是数据库的多版本并发控制(MVCC)。

为什么建议尽量不要使用长事务?

长事务意味着系统里面会存在很老的事务视图。由于这些事务随时可能访问数据库里面的任何数据,所以这个事务提交之前,数据库里面它可能用到的回滚记录都必须保留,这就会导致大量占用存储空间。
在 MySQL 5.5 及以前的版本,回滚日志是跟数据字典一起放在 ibdata 文件里的,即使长事务最终提交,回滚段被清理,文件也不会变小。我见过数据只有 20GB,而回滚段有 200GB 的库。最终只好为了清理回滚段,重建整个库。
除了对回滚段的影响,长事务还占用锁资源,也可能拖垮整个库,这个我们会在后面讲锁的时候展开。

真假“美猴王”

西游记中,有真假美猴王,悟空和六耳猕猴的故事大家都知道。与之相似的, 在 MySQL 里,有两个“视图”的概念:

  • 一个是 view。它是一个用查询语句定义的虚拟表,在调用的时候执行查询语句并生成结果。创建视图的语法是 create view … ,而它的查询方法与表一样。
  • 另一个是 InnoDB 在实现 MVCC 时用到的一致性读视图,即 consistent read view,用于支持 RC(Read Committed,读提交)和 RR(Repeatable Read,可重复读)隔离级别的实现。 一个数据版本,对于一个事务视图来说,除了自己的更新总是可见以外,有三种情况:
  1. 版本未提交,不可见;
  2. 版本已提交,但是是在视图创建后提交的,不可见;
  3. 版本已提交,而且是在视图创建前提交的,可见。

InnoDB 的行数据有多个版本,每个数据版本有自己的 row trx_id,每个事务或者语句有自己的一致性视图。普通查询语句是一致性读,一致性读会根据 row trx_id 和一致性视图确定数据版本的可见性。

  • 对于可重复读,查询只承认在事务启动前就已经提交完成的数据;
  • 对于读提交,查询只承认在语句启动前就已经提交完成的数据;

而当前读,总是读取已经提交完成的最新版本

事务的可重复读的能力是怎么实现的?

可重复读的核心就是一致性读(consistent read);而事务更新数据的时候,只能用当前读。如果当前的记录的行锁被其他事务占用的话,就需要进入锁等待。

而读提交的逻辑和可重复读的逻辑类似,它们最主要的区别是:

  • 在可重复读隔离级别下,只需要在事务开始的时候创建一致性视图,之后事务里的其他查询都共用这个一致性视图;
  • 在读提交隔离级别下,每一个语句执行前都会重新算出一个新的视图。

AID——>C?

这就是咱们的重头戏了。 首先咱们都知道事务是为了解决一致性问题而提出的概念,所以就很明显,我们整个事务都是去解决一致性的手段。为了达到一致性,我们需要保障原子性、隔离性、持久性。三者缺一不可。
1、如果我们的事务不是具有原子性的,我们事务有一部分成功执行完毕,一部分执行失败。那么这两部分就会互相影响,我们的结果也会受到影响。
2、如果事务没有实现隔离性,对一份数据去开启事务修改数据,事务A在执行,事务B也去执行,两者会相互影响,最终的数据可能不会是我们所料想的结果,也就是没有达到一致性。
3、如果没有实现持久性,在咱们事务执行成功后,数据修改丢失了,我们的事务相当于没有去执行,这也达不到一致性。