从本篇文章开始准备改变风格,减少对原文的摘抄,增加归纳,整理和知识的迁移以及更多的例子,以锻炼自己的总结能力。

1.什么是事务?

ACID:数据库系统在写入或更新资料过程中,为保证**事务(transcation)**正确可靠,必须具备四个特性 —— 原子性(atomicity)、一致性(consistency)、隔离性(isolation)、持久性(durability)

什么是数据库事务数据库管理系统执行过程中的一个逻辑单位,由一个有限的数据库操作序列构成。 —— 来自 Wiki百科

Java 中的原子操作一定会用银行转账做例子来介绍,数据库事务是: 保证一组数据库操作要么全部成功,要么全部失败,不存在中间状态的机制

MySQL 支持多种存储引擎,但是并非所有存储引擎都支持事务,原生的 MyISAM不支持事务,这个原因是其被 InnoDB 替代的重要原因。

事务的隔离性与隔离级别

当数据库上同时存在多个事务同时执行时,可能出现以下问题:

  • 脏读(dirty read):一个事务中访问到了另一个事务未提交的数据
会话1会话2
beginbegin
update table set age = 10 where id = 1
select age from table where id = 1
commitcommit

这里如果会话2更新 age =10,此时会话1获取age 看到的也是10,但是应该获取到的是1,因为此时会话2的事务还没有提交。或者会话2的事务进行了 rollback 回滚,而此时会话1获取到的 age 仍然是10,这就是脏读

  • 不可重复读(non-repeatable read):一个事务读取同一条记录2次,得到的结果不一致。
会话1会话2
beginbegin
select age from table where id = 1
update table set age = 10 where id = 1
select age from table where id = 1commit
commit

此时 ①处读到的age 和 ②处读到的不一致,因为在会话1事务查询期间会话2修改了查询的值,这种错误叫做不可重复度。

  • 幻读(phantom read):一个事务读取2次,得到的记录条数不一致。
会话1会话2
beginbegin
select age from table where id > 2
insert into table(id,age),values(5,10)
select age from table where id > 2commit
commit

会话2在会话1查询期间插入了一个新的值,于是会话1的两次查询结果不一样,第二次查询比第一次多了一条记录,这就是幻读。

【这里作者只列出了三个问题,但是我并不知道问题的细节,于是去找了幻读,脏读,不可重复度的具体问题场景,这样对问题的认知才比较完全。】

为了解决以上三个问题,诞生了 「隔离级别」 这个概念。

隔离级别越高,执行效率越低。 就跟并发中一样,使用的同步机制越多,效率也就越低,最极端的情况就是整个程序彻底丧失并发性,变成串行程序。

SQL 标准事务隔离级别有如下几种:

  • 读未提交:一个事务还没提交时,它做的变更就能被别的事务看到。 允许脏读,但不允许更新丢失。如果一个事务已经开始写数据,则另一个事务不允许同时进行写操作,但允许其他事务读此行数据,这种级别的隔离可以使用排他写锁实现。
  • 读提交:一个事务提交之后,做的变更才能背其他事务看到。允许不可重复度,不允许脏读。可以通过「瞬间共享读锁」和「排他写锁」实现。读取数据的事务允许其他事务继续访问该行数据,但是未提交的写事务将会禁止其他事务访问。
  • 可重复读:一个事务执行过程中看到的数据,总是跟这个事务在启动时看到的数据是一致的。**(也就是无论其他事务是否对当前事务读取的数据做了更改,这个事务读取到的数据都是不变的)**在这个隔离级别中,未提交的变更对其他事务不可见。
  • 串行化:对于同一行记录,写和读都会加锁,【类似 Java 中的互斥,只有一个线程能进入被锁保护的临界区】,在 MySQL 中当出现读写锁冲突时,后访问的事务必须等待前一个事务执行完成才能继续执行

避免不可重复度锁行,避免幻读锁表。

【可以看到,前两条都是针对事务的提交状态是否影响其他事务的可见性的,第三条我感觉有点像视图,读取了一行记录之后在事务执行过程中这个数据是不会随着其他事务对这个事务读取到的数据改变而发生改变的。】

最后串行化是好理解的,互斥。

下面是作者举的例子,用来说明这几种隔离级别的差异:

假设数据表 T 中只有一列,其中一行的值为1,下面是按照顺序执行两个事务的行为。

mysql> create T(c int) engine=InnoDB;
insert into T(c) values(1);
事务A事务B
启动事务,查询得到值1启动事务
查询得到值1
将 1 修改为 2
查询得到值 V1
提交事务B
查询得到值 V2
提交事务A
查询得到值V3

在不同的隔离级别下,事务A 的返回结果不同,也就是上表中的 V1 、V2、V3,下面是不同隔离级别的不同分析:

  • 读未提交:因为在事务B还没提交的时候修改就能被其他事务看到,所以这里的 V1 的值是 2V2、V3 也是2。

  • 读提交事务提交之后的修改才能被其他事务看到,那么 V1 读到的还是原来的值1V2、V3的值是2

  • 可重复读事务执行过程中读到的数据不变,所以 V1、V2 都是1,因为事务启动的时候得到值是1,提交之后再读取的值 V3 是 2.

  • 串行化事务A读取值的时候 事务B要等事务A执行完才能修改值,所以 V1、V2 都是1,然后事务B 将1改成2,事务A再次查询的值 V3 是2

【事实上如果你有对并发相关知识的学习铺垫,理解事务这块还是相当容易的。】

**数据库具体对于隔离级别的实现:**数据库创建一个视图,访问值的结果以视图的逻辑结果为准。所以看到的值取决于这个视图的值,而视图的值取决于视图创建的时间。

  • 可重复读视图在事务启动时创建,整个事务执行期间都读取这个视图的值,所以其他事务修改了数据值,在可重复度的事务执行期间读取到的数据值也不会发生改变
  • 读提交:视图在每个 SQL 语句开始执行时创建。
  • 读未提交直接返回记录上最新值,没有视图概念。【相当于直接指向对象的引用,可以看到最新的值】
  • 串行化:使用锁来保证互斥,所以不需要视图。

不同隔离级别,数据库有着不同的行为。 Oracle 默认隔离级别是度提交,而 MySQL 的默认隔离级别是 可重复读,所以如果是从 Oracle 迁移到 MySQL 的应用,需要修改 MySQL 的隔离级别

image-20200904024632936

每种隔离级别都有自己的使用场景:

  • 可重复读:一个数据校对逻辑的案例:

假设一个个人银行账户数据库,一个表存账户余额,一个表存账单明细。当月底对账的时候,希望在校对过程中即使用户发生了一笔新的交易也不影响校对结果。

这时使用可重复读的隔离级别就很合适了。因为在事务中读取到的数据是不变的。

事务隔离的实现

通过上面的 "可重复读" 作为例子说明事务隔离的具体实现方法

MySQL 中每条记录在更新的时候同时会记录一条回滚操作。记录上的最新值,通过回滚操作,都可以得到一个当前状态的值。

假设一个值从 1 被按顺序改成了 2、3、4 则回滚日志会存在如下记录:

当前值是4,但是不同时刻的事务对应着不同的 read-view。同一条记录在系统中可以存在多个版本,这就是数据库的多版本并发控制(MVCC)。

对于 read-view A,要得到1,则必须将当前值按图中所有的回滚操作执行之后得到。

回滚日志的删除时机:当系统里没有比回滚日志更早的 read-view 时(没有进行回滚的场景),系统判断回滚日志无用,进行删除。

不推荐使用长事务的原因①:长事务会导致系统中存在很老的事务视图(read-view) ,由于这些事务随时可能访问数据库里的任何数据,所以在事务提交之前,所有的回滚记录都会被系统保留,会占用大量的存储空间。

MySQL 5.5 之前,「回滚日志」和「数据字段」 一起存放在 ibdata 文件中,即使长事务被提交,回滚视图被清理,文件也不会变小。所以最终可能造成一个巨型的文件,最终为了清理回滚视图,重建整个库。

不推荐使用长事务的原因②:占用资源,可能拖垮整个库。

事务的启动方式

MySQL 事务启动的几种方式:

  • 显示启动事务语句:begin / start transaction,配套提交语句 —— commit回滚语句 —— rollback
  • set atuocommit = 0,这个配置会关闭线程自动提交如果只执行一个 select 语句,事务就启动了,并且不会自动提交,事务会持续到主动执行 commitrollback 语句,或者断开连接

如果一个业务需要频繁使用数据库事务,第二种会省去主动地 begin,减少了语句交互次数,作者建议使用 commit work and chain 语法。

【那么问题来了,这个语句在文章中并没有进行详细介绍,于是需要自己去查一下】

MySQL 通过 set autocommitstart transaction , coomitrollback 支持本地事务,具体语法:

START TRANSACTION | BEGIN [WORK]
COMMIT [WORK] [AND [NO] CHAIN] [[NO] RELEASE]
ROLLBACK [WORK] [AND [NO] CHAIN] [[NO] RELEASE]
SET AUTOCOMMIT = {0 | 1}

MySQL 默认自动提交事务,如果需要明确 CommitRollback 提交 和 回滚事务,需要明确的事务控制命令来开始事务 transaction 或使用 begin 语句。

commitrollback 用来提交和回滚事务

chainrelease 子句用来定义事务提交或者回滚后的操作,chain 会立即启动一个新事务,并且和刚才的事务具有相同给的隔离级别,release 会断开和客户端的连接。

----------- ↑ ----------- 是查询到的 commit work and chain 的解释,感觉也不是很清晰,只是介绍了 chain 和 release 子句的用处。

autocommit = 1 的情况下,使用 begin 可以显式的开启事务,执行 commit 提交事务。如果执行 commit work and chian ,则是提交事务并自动启动下一个事务,这样省去了再次执行 begin 语句的开销,从程序开发的角度看好处是也可以明确知道每个语句是否处于事务中。

MySQL 长事务保存在 information_schemal 库的 innodb_trx 表中,下面的语句可以查找持续时间超过 60s 的事务:

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

小结

介绍了 MySQL 中的事务隔离级别表现出的现象以及原理,根据隔离级别的实现原理分析了使用长事务存在的风险,如何避免长事务的存在。
这里 MVCC 是个重点,感觉可以挖很深,但是第一遍学习不要太纠结细节,而是要观其大略,抓大放小,这里先留个坑吧。

极客时间MySQL实战03-事务

思考

从业务开发和数据库负责人的角度思考,使用什么方案避免长事务。

从开发的角度来看,首先缩小事务范围,就跟在并发中使用锁一样,在最小范围使用锁可以获得最大的性能,对于数据库事务而言,少用长事务。

如果无法避免长事务,则需要保证逻辑日志空间足够,并且支持动态日志空间增长,对 innodb_trx 表进行监控,对长事务进行告警。

精华回复:

对于隔离级别的更简明扼要的总结:

  • 读未提交:别人改数据的事务尚未提交,我在我的事务中也能读到修改后的数据。
  • 读已提交:别人改事务的数据已经提交,我在我的事务中才能读到。
  • 可重复读:别人改数据的事务已经提交,我在我的事务中也不去读最新的数据。
  • 串行:我的事务尚未提交,别人就别想改数据。

这四种隔离级别,并行性能依次降低,安全性依次提高。

针对本章的知识总结了几个问题进行自检

  1. 事务的概念?
  2. MySQL 事务隔离有几个级别,每个级别的具体含义是什么?
  3. 读已提交、可重复读是怎样通过视图构建的?
  4. 可重复读的使用场景?【针对其特性构建使用场景】
  5. 事务隔离怎样通过 read-view 读视图实现?
  6. 并发版本控制 MVCC 的概念,以及实现原理。
  7. 长事务的弊端,为什么长事务会拖垮整个库的性能?
  8. 事务的启动方式?
  9. commit work and chain 语法的作用。
  10. 怎么查询各表中的长事务?
  11. 如何避免长事务?

其他资料

MySQL InnoDB MVCC 深度分析

MySQL · 源码分析 · InnoDB的read view,回滚段和purge过程简介

Q.E.D.

知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议

最是人间留不住,曾是惊鸿照影来。