Mysql初探:数据库事务概述

此文为极客时间:MySQL实战45讲的 3、8、18、19节事务相关部分的总结

一、事务的启动方式

mysql 主要有两种事务的启动方式:

  1. beginstart transaction显式启动事务。对应的提交语句是 commit ,回滚是 rollback
  2. set autocommit = 0关闭自动提交,然后在执行第一条 sql 的时候启动事务,这个事务会一直持续到你主动 commit 或者 rollback,或者断开连接才会结束。

有一些客户端连接框架会在连接成功后默认修改设置,这可能导致意外的长事务。因此,显示启动事务明显是比较安全的,但是对于一些需要频繁使用事务的业务,每次都需要调用 begin 然后再 commit。对于这种情况,可以使用 commit work and chain,当 autocommit = 1时,使用该语句可以在提交以后自动开启下一个新事务

这样省去了再次执行 begin 语句的开销,而且可以明确地知道每个语句是否处于事务中。

除此之外,我们还可以使用 sql 去在 information_schema 库的 innodb_trx 这个表中查询长事务:

select * from information_schema.innodb_trx where TIME_TO_SEC(timediff(now(),trx_started)) > 60

比如上面这条语句,就是用于查找持续时间超过 60s 的事务

二、事务的隔离级别

我们知道事务有四大特性(ACID):原子性,一致性,隔离性,持久性。

针对隔离性,我们有:

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

简单的理解:

  1. 读未提交:别人改数据的事务尚未提交,我在我的事务中也能读到
  2. 读已提交:别人改数据的事务已经提交,我在我的事务中才能读到
  3. 可重复读:别人改数据的事务已经提交,我在我的事务中也不去读
  4. 串行化:我的事务尚未提交,别人就别想改数据。

以这张图为例:

image-20200930144400423

  1. 读未提交: 则 V1 的值就是 2。这时候事务 B 虽然还没有提交,但是结果已经被 A 看到了。因此,V2、V3 也都是 2。
  2. 读已提交:则 V1 是 1,V2 的值是 2。事务 B 的更新在提交后才能被 A 看到。所以, V3 的值也是 2。
  3. 可重复读:则 V1、V2 是 1,V3 是 2。之所以 V2 还是 1,遵循的就是这个要求:事务在执行期间看到的数据前后必须是一致的。
  4. 串行化:则在事务 B 执行“将 1 改成 2”的时候,会被锁住。直到事务 A 提交后,事务 B 才可以继续执行。所以从 A 的角度看, V1、V2 值是 1,V3 的值是 2。

我们不难看出,读已提交和可重复读,最大的区别在于,当一个查询的事务尚未提交,另一个修改的事务的提交是否会影响到这次查询结果。

三、事务隔离的实现

1.脏读,幻读,不可重复读

说起事务,就不得不提到三种错误读:

  1. 脏读(读到了RoolBack):表示一个事务能够读取另一个事务中还未提交的数据。这个未提交数据就是脏读(Dirty Read)。
  2. 幻读(读到了insert):指同一个事务内多次查询返回的结果集不一样。
  3. 不可重复读(读到了update):是指在一个事务内,多次读同一数据。
    • 第一类丢失更新:两个事务更新同一条数据资源,后做的事务撤销,发生回滚造成已完成事务的更新丢失
    • 第二类丢失更新:两个事务更新同一条数据资源,后完成的事务会造成先完成的事务更新丢失

2.事务隔离的实现

在实现上,数据库里面会创建一个视图,当访问的时候以视图的逻辑结果为准。

这里需要注意一下,这里的视图区别于我们自己创建的 View :

innodb 创建的,用于实现 MVCC 时的一致性读视图,即 consistent read view,用于支持 RC(Read Committed,读提交)和 RR(Repeatable Read,可重复读)隔离级别。

  1. 读未提交:直接返回记录上的最新值,没有视图概念;
  2. 读已提交:这个视图是在每个 SQL 语句开始执行的时候创建的。
  3. 可重复读:这个视图是在事务启动时创建的,整个事务存在期间都用这个视图。
  4. 串行化:直接用加锁的方式来避免并行访问。

这里单独对读已提交和可重复读的逻辑做一个区分:

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

四、MVCC

1.概述

MVCC 即是并发版本控制。拿可重复读举个例子:

我们知道 innodb 有个 undo log ,每条记录在更新的时候都会在 undo log 中记录一条回滚操作,通过日志记录可以回滚到上一状态的值。

假设一个值从 1 被按顺序改成了 2、3、4,在回滚日志里面就会有类似下面的记录。

image-20200930145105580

当前的值是4,但是对于不同时间段启动的事务创建的视图ABC而言,分别为1,2,4,这时就算把4再改成5,对于ABC三个视图也不会有影响。

同一条记录在系统中可以存在多个版本,就是数据库的多版本并发控制(MVCC)

2.一致性读视图的实现

当在可重复读隔离级别下时,事务在启动的时候就给整库“拍了个快照”,这个快照就是我们在事务的隔离提到过一致性读视图。这个视图是逻辑上的,用于描述事务之间的可见性。

在 innodb 里,每个事务都有独有的 transaction id,这是在事务开始的时候向系统申请的,是严格递增的。

而每行数据也有多个版本,每次事务更新数据的时候都会把 id 赋给对应版本数据的 row trx_id。

image-20201023102713922

如上图,我们可以看到这一行数据被三个事务进行了修改,现在有四个版本,每个版本更新前都会记录一条回滚的语句在 undo log。

事实上,V1,V2这些版本的数据并不是真实存在的,而是在需要的时候才通过 undo log 计算获取。比如需要 V2,就从 V4 经过 U3 和 U2 获得。

现在我们知道数据版本是如何跟事务绑定的,那么事务的隔离就很好理解了:当一个事务启动的时候,获取事务 id,事务id比他小的说明是在他之前就产生的,这些事务对应的版本就是被本事务承认的,反之,则这些数据是不被本事务承认的,要向前找到可以承认的数据版本。

为此,innodb 会在事务启动的时候,为事务创建一个数组,这个数字里会存放所有当前启动了但是还没提交的事务的 id。这个数组里最小的视为低水位,最大的+1视为高水位,从低水位到高水位中间的这块区域,就是当前事务的一致性视图。这段操作是在锁保护性进行的。

3.数据版的一致性读

假如我们只在事务里面进行查询,而暂时不涉及到更新,那么基于一致性视图,当前事务就可以根据数据版本id,也就是 row trx_id 来判断当前数据版本对于自己而言是否可见:

image-20201023104832814

  1. 如果落在绿色部分,表示这个版本是已提交的事务或者是当前事务自己生成的,这个数据是可见的;
  2. 如果落在红色部分,表示这个版本是由将来启动的事务生成的,是肯定不可见的;
  3. 如果落在黄色部分,那就包括两种情况
    • 若 row trx_id 在数组中,表示这个版本是由还没提交的事务生成的,不可见;
    • 若 row trx_id 不在数组中,表示这个版本是已经提交了的事务生成的,可见。

当然,可能存在这么一种情况:如果有一个事务在未提交事务的区间,但是在当前事务获

仍然以上图为例,假如有一个事务,他的低水位是18,也就是说在他启动的时候,row trx_id 是17 的V3是最新的版本,在他查询的时候,而最新的版本变成了 row trx_id 是25的V4,那么对他而言V4是不可见的,于是通过 undo log U3 计算得到V3,V3低于他的低水位,所以V3是可见的,故对于该事务而言值就是V3的值。

可以看到,事务开始前和事务开始后读到的数据都一致的,这个就是一致性读。可重复读依赖这个隔离级别核心依赖于此。

4.数据的当前读

当事务里只进行查询的时候一致性读可以保证读取的正确性,但是如果进行的是更新,那么一致性读反而会导致错误。我们以下图为例:

image-20201023110829190

原本 k 是2,事务C进行了更新并且率先提交,对于事务C而言,此时k是3,但是事务B又进行了一次更新,那么等到提交的时候,k该是3还是4?

这里涉及到一个规则。因为更新总是需要先读后改,所以更新的读必须要读最新的数据,也就是当前读

值得一提的是,如果是 select 语句,如果加了读锁或者写锁,也是当前读:

# 加读锁
select k from t where id=1 lock in share mode;
# 加写锁
select k from t where id=1 for update;

然后,我们在前面了解了行锁,而行锁有一个两阶段锁的机制:事务里的有对某一行数据的更新,那么sql执行前就会去获取行锁,然后执行完sql之后不释放,等到事务提交之后才会去释放锁。由于事务C先获取了行锁,那么事务B的更新就会等待事务C释放锁以后才会得到锁。反映到执行上,就是事务B的 update 等到 事务C提交了才会继续进行。

也就说,而行锁的两阶段锁保证了更新的顺序进行,当前读机制保证的更新语句总是能拿到最新的数据。

5.一致性视图与可重复读和读已提交

MVCC 实现的核心在于一致性视图,可重复读和读已提交建立视图的机制决定了他们实现效果的不同:

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

6.为什么要避免长事务

  1. 占用日志空间:因为一致性视图需要通过 undo log 去计算旧版本的数据,而 undo log 只有在没有比某条日志更早的一致性视图时才会删除。所以如果存在长事务,可能就会导致数据库的视图存在很长时间,直到这些视图删除前日志都会一直保留,这将会导致占用大量存储空间。
  2. 影响版本控制计算性能:在可重复读这个隔离级别下,如果其他事务对某条数据进行了非常多次的操作,最后会导致本事务读取的时候必须要通过 undo log 计算非常多次才能找到最初的数据版本。
  3. 占用锁资源:长事务还会可能会占用锁资源,比如只有等事务提交才能释放的行锁。

五、总结

1.事务的启动

  • beginstart transaction显式启动事务。对应的提交语句是 commit ,回滚是 rollback
  • set autocommit = 0`关闭自动提交,然后在执行第一条 sql 的时候启动事务,这个事务会一直持续到你主动 commit 或者 rollback,或者断开连接才会结束。

2.事务的隔离级别

  • 读未提交:别人改数据的事务尚未提交,我在我的事务中也能读到。会脏读,幻读,不可重复读;
  • 读已提交:别人改数据的事务已经提交,我在我的事务中才能读到。会幻读,不可重复读;
  • 可重复读:别人改数据的事务已经提交,我在我的事务中也不去读。会不可重复读;
  • 串行化:我的事务尚未提交,别人就别想改数据。加锁,不会错误读。

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

  • 每个事务的更新都会产生一个新版本数据,每个数据版本有自己的 row trx_id,对应更新他们的事务的 transaction id;

  • 事务启动时 innodb 为事务创建一个数组,这个数字里会存放所有当前启动了但是还没提交的事务的 id。这个数组里最小的视为低水位,最大的+1视为高水位,从低水位到高水位中间的这块区域,就是当前事务的一致性视图。根据事务版本 id 从一致性视图中判断该版本对本事务是否可见;

  • 可重复读和读已提交建立视图的机制决定了他们实现效果的不同:

    可重复读隔离级别下,只需要在事务开始的时候创建一致性视图,之后事务里的其他查询都共用这个一致性视图;

    读提交隔离级别下,每一个语句执行前都会重新算出一个新的视图。

相关文章

在正式开始之前,我们先来看下 MySQL 服务器的配置和版本号信...
> [合辑地址:MySQL全面瓦解](https://www.cnblogs.c...
物理服务机的CPU、内存、存储设备、连接数等资源有限,某个时...
1 回顾 上一节我们详细讲解了如何对数据库进行分区操作,包括...
navicat查看某个表的所有字段的详细信息 navicat设计表只能一...
文章浏览阅读4.3k次。转载请把头部出处链接和尾部二维码一起...