事务是怎样工作的

目录

事务特性

事务隔离性解决什么问题

隔离级别

事务隔离实现原理

可重复读隔离级别的实现原理

知识铺垫

InnoDB中的事务ID

数据的多版本(MVCC)

InnoDB 为每个事务构造了一个由事务ID组成的数组

一致性视图(read-view)

举例:可重复读的事务隔离级别工作原理

问答

问:回滚日志不会一直保留,什么时候删除

问:为什么尽量不要使用长事务

问:如何正确的开启事务

问:哪些操作会导致长事务,或者说如何避免长事务

问:如何查询数据库当前所有的事务


事务特性

Atomicity(原子性)

Consistency(一致性)

Isolation(隔离性)

Durability(原子性)

事务隔离性解决什么问题

解决 脏读(dirty read)、不可重复读(non-repeatable read)、幻读(phantom read)的问题

隔离级别

隔离级别越高,性能越低

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

事务隔离实现原理

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

在“可重复读”隔离级别下,这个视图是在事务启动时创建的,整个事务存在期间都用这个视图。

在“读提交”隔离级别下,这个视图是在每个sql语句开始执行的时候创建的。

“读未提交”隔离级别,没有视图的概念直接读取记录上的最新值

“串行化”隔离级别下直接使用加锁的方式来避免并行访问

可重复读隔离级别的实现原理

知识铺垫

InnoDB中的事务ID

InnoDB 里面每个事务有一个唯一的事务 ID,叫作 transaction id。它是在事务开始的时候向 InnoDB 的事务系统申请的,是按申请顺序严格递增的(线程安全的)

数据的多版本(MVCC)

每行数据都有多个版本,每次事务更新数据的时候,都会生成一个新的数据版本,并且把 transaction id 赋值给这个数据版本中的事务 ID(记为 row trx_id)。

同时,旧的数据版本要保留,并且在新的数据版本中,能够有信息可以直接拿到它(可以使用链表实现)

也就是说,表中的一行记录,其实可能有多个版本 ,每个版本有自己的 row trx_id 其值为 transaction id

示意图如下

图中虚线框里是同一行数据的 4 个版本,当前最新版本是 V4,k 的值是 22,它是被 transaction id 为 25 的事务更新的,因此它的 row trx_id 是 25。

写操作会生成 undolog(回滚日志)图中的三个虚线箭头 U1、U2、U3 就是 undolog;而 V1、V2、V3 并不是物理上真实存在的,而是每次需要的时候根据当前版本和 undolog 计算出来的。比如,需要 V2 的时候,就是通过 V4 依次执行 U3、U2 算出来。

InnoDB 为每个事务构造了一个由事务ID组成的数组

InnoDB 为每个事务构造了一个由事务ID组成的数组,用来保存这个事务启动瞬间当前正在“活跃”的所有事务 ID。“活跃”指的就是,启动了但还没提交。

数组里面事务 ID 的最小值记为低水位(所有“活跃”的事务ID中的最小值)

当前系统里面已经创建过的事务 ID 的最大值加 1 记为高水位(所有“活跃”事务ID中的最大值)

一致性视图(read-view)

InnoDB 在实现 MVCC 时用到的一致性读视图,即 consistent read view,用于支持 RC(Read Committed,读提交)和 RR(Repeatable Read,可重复读)隔离级别的实现。作用是事务执行期间用来定义“我能看到什么数据”。

这个由事务ID组成的数组(视图数组)和数组中的高低水位,就组成了当前事务的一致性视图(read-view)。

而数据版本的可见性规则,就是基于数据的 row trx_id(多版本) 和这个一致性视图的对比结果得到的。

这样,对于当前事务的启动瞬间来说,一个数据版本的 row trx_id,有以下几种可能:

如果落在绿色部分,表示这个版本是已提交的事务或者是当前事务自己生成的,这个数据是可见的;

如果落在红色部分,表示这个版本是由将来启动的事务生成的,是肯定不可见的;

如果落在黄色部分,那就包括两种情况

  • a. 若 row trx_id 在数组中,表示这个版本是由还没提交的事务生成的,不可见;
  • b. 若 row trx_id 不在数组中,表示这个版本是已经提交了的事务生成的,可见。

举例:可重复读的事务隔离级别工作原理

事务A的可重复读实现逻辑

创建表

CREATE TABLE `t` (
 `id` int(11) NOT NULL,
 `k` int(11) DEFAULT NULL, 
  PRIMARY KEY (`id`) ) 
ENGINE=InnoDB; 

insert into t(id, k) values(1,1),(2,2);

sql语句执行时间,time数字越大,时间越大

时刻

事务A(transaction id 是 100)

事务B(transaction id 是 101)

事务C(transaction id 是 102)

time1

start transaction with consistent snapshot;

time2

start transaction with consistent snapshot;

time3

begin;

update t set k = k + 1 where id = 1;

commit;

time4

update t set k = k + 1;

select k from t where id = 1;

time5

select k from t where id = 1;

commit;

time6

commit;

先说下结果,事务A 查询到的 k 值为1,为什么为1,分析如下

这里,我们不妨做如下假设:

  • 事务 A 开始前,系统里面只有一个活跃事务 ID 是 99;
  • 事务 A、B、C 的版本号分别是 100、101、102,且当前系统里只有这四个事务;
  • 三个事务开始前,(1,1)这一行数据的 row trx_id 是 90。

这样,事务 A 的视图数组就是[99,100],

事务 B 的视图数组是[99,100,101],

事务 C 的视图数组是[99,100,101,102]。

事务 A 查询数据逻辑图

从图中可以看到,第一个有效更新是事务 C,把数据从 (1,1) 改成了 (1,2)。这时候,这个数据的最新版本的 row trx_id 是 102,而 90 这个版本已经成为了历史版本。

第二个有效更新是事务 B,把数据从 (1,2) 改成了 (1,3)。这时候,这个数据的最新版本(即 row trx_id)是 101,而 102 又成为了历史版本。

你可能注意到了,在事务 A 查询的时候,其实事务 B 还没有提交,但是它生成的 (1,3) 这个版本已经变成当前版本了。但这个版本对事务 A 必须是不可见的,否则就变成脏读了。

好,现在事务 A 要来读数据了,它的视图数组是[99,100]。当然了,读数据都是从当前版本读起的。所以,事务 A 查询语句的读数据流程是这样的:

  • 找到 (1,3) 的时候,判断出 row trx_id=101,比高水位大,处于红色区域,不可见;
  • 接着,找到上一个历史版本,一看 row trx_id=102,比高水位大,处于红色区域,不可见;
  • 再往前找,终于找到了(1,1),它的 row trx_id=90,比低水位小,处于绿色区域,可见。

这样执行下来,虽然期间这一行数据被修改过,但是事务 A 不论在什么时候查询,看到这行数据的结果都是一致的,所以我们称之为一致性读。

事务B的更新逻辑

先说结果,事务B的查询结果 k 为 3

事务 B 的 update 语句,如果按照一致性读,好像结果不对哦?事务 B 的视图数组是先生成的,之后事务 C 才提交,查询结果不应该是 k 等于 2,怎么会是 3 呢?

原因是,更新数据都是先读后写的,而这个读,只能读当前的值,称为“当前读(current read)”。如果没有“当前读”,那事务C的更新就丢失了。

读提交隔离级别实现原理

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

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

问答

问:回滚日志不会一直保留,什么时候删除

答:当所有活跃的事务都用到的undolog日志之外的,undolog日志都可以被删除

分析如下

假设当前活跃的事务,就三个 事务 id 100,事务id101,事务id 102,事务id 90已经commit,事务id 89 已提交

事务A需要使用到的,undolog日志是 U1、U2。

事务B需要使用到的 undolog日志是U1、U2

事务C需要使用到的 undolog日志是U2

所以,U3 undolog 就可以被删除了

问:为什么尽量不要使用长事务

答:

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

问:如何正确的开启事务

答:

  • 显示启动事务(begin 或 start transaction)
  • 开启自动提交

问:哪些操作会导致长事务,或者说如何避免长事务

答:

这个问题,我们可以从应用开发端和数据库端来看。

首先,从应用开发端来看:

  1. 确认是否使用了 set autocommit=0。这个确认工作可以在测试环境中开展,把 MySQL 的 general_log 开起来,然后随便跑一个业务逻辑,通过 general_log 的日志来确认。一般框架如果会设置这个值,也就会提供参数来控制行为,你的目标就是把它改成 1。
  2. 确认是否有不必要的只读事务。有些框架会习惯不管什么语句先用 begin/commit 框起来。我见过有些是业务并没有这个需要,但是也把好几个 select 语句放到了事务中。这种只读事务可以去掉。
  3. 业务连接数据库的时候,根据业务本身的预估,通过 SET MAX_EXECUTION_TIME 命令,来控制每个语句执行的最长时间,避免单个语句意外执行太长时间。(为什么会意外?在后续的文章中会提到这类案例)

其次,从数据库端来看:

  1. 监控 information_schema.Innodb_trx 表,设置长事务阈值,超过就报警 / 或者 kill;
  2. Percona 的 pt-kill 这个工具不错,推荐使用;
  3. 在业务功能测试阶段要求输出所有的 general_log,分析日志行为提前发现问题;
  4. 如果使用的是 MySQL 5.6 或者更新版本,把 innodb_undo_tablespaces 设置成 2(或更大的值)。如果真的出现大事务导致回滚段过大,这样设置后清理起来更方便。

问:如何查询数据库当前所有的事务

答:可以在 information_schema 库的 innodb_trx 这个表中查询长事务

比如下面这个语句,用于查找持续时间超过 60s 的事务

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

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值