Mysql 事务

Page content

03 | 事务隔离:为什么你改了我还看不见? 07 | 行锁功过:怎么减少行锁对性能的影响? 08 | 事务到底是隔离的还是不隔离的?

一、前置知识

什么是事务?

简单来说,就是保证一组数据库操作要么全部执行成功,要么一个也不执行

事务的ACID

A (atomiciy 原子性)

一个事务被视为一个最小的工作单元,该单元内所有的操作要么全部执行要么回滚全部不执行,不可能部分执行;

C (consistency 一致性)

数据必须保证从一种一致性状态转换为另一种一致性状态; eg:A 转账给 B 100元,那么转账操作前是一种一致性状态,转账成功后 A 少了100元,B 多了100元,这是另一种一致性状态;

I (isolation 隔离性)

并发执行的多个事务之间互不干扰;

D (durability 持久性)

一旦事务提交,该事务所做的更改会被永久保存在数据库中;

tip: 一致性是最基本的属性,其它三个属性可以说都是为了保证数据的一致性; A 通过 undo log 实现;D 通过 redo log 实现;I 通过锁实现;

多个事务并发可能会产生的问题

脏读

事务一在执行过程中修改了记录a,且事务一未完成处于不一致状态时,事务二读取了记录a,并据此做进一步处理,不巧的是事务一执行了回滚操作,这个时候我们认为事务二读取了脏数据,称之为脏读;

不可重复读

在同一个事务内,两个相同的查询返回了不同的结果;

幻读

在同一个事务内,两个相同读查询返回读结果集数量不一致;

不可重复读与幻读读区别

  1. 不可重复读的重点在于 update & delete,强调的是同一个查询返回的内容不同;在可重复读隔离级别中,当事务第一次读取了一部分数据后就会对这部分数据加锁,其它事务无法操作这部分数据,这就实现了可重复读,但是这种锁无法避免其它事务的insert操作,这就会导致另一个问题:幻读;
  2. 幻读的重点在于insert,强调的是同一个查询返回的结果集数量不一致;

为了解决事务的并发问题:事务隔离

读未提交

一个事务还没提交时,它做的变更就能被别的事务看到。

读提交

一个事务提交之后,它做的变更才会被其他事务看到。

可重复读

一个事务执行过程中看到的数据,总是跟这个事务在启动时看到的数据是一致的。当然在可重复读隔离级别下,未提交变更对其他事务也是不可见的。

串行化

对于同一行记录,“写”会加“写锁”,“读”会加“读锁”。当出现读写锁冲突的时候,后访问的事务必须等前一个事务执行完成,才能继续执行。

举个栗子说明不同的隔离级别下数据库的表现

create table T(c int) engine=InnoDB;
insert into T(c) values(1);
事务A 事务B
启动事务A查询得到值 1 启动事务
查询得到值 1
将 1 改成 2
查询得到值 V1
提交事务 B
查询得到值 V2
提交事务 A
查询得到值 V3
  • 若隔离级别是读未提交:V1、V2、V3 都是 2,虽然事务 B 还未提交,但是它的操作结果已经被事务 A 看到,所以V1、V2、V3 都是 2
  • 若隔离级别是读提交:V1是1,V2、V3 是 2;事务B提交后它的更改才能被事务A看到,所以 V1=1,之后的数据版本都是最新的 2;
  • 若隔离级别是可重复读:V1、V2是1,V3是2;在整个事务执行期间,看到的数据版本一致,都是最初的 1,所以 V1、V2是1;
  • 若隔离级别是串行化:V1、V2是1,V3是2;事务B执行更新操作的时候会被锁住,等到事务A完成后,才能继续执行,所以 V1、V2是1,V3是2

读未提交隔离级别下,每次获取数据,直接返回对应记录的最新值即可;可串行化隔离级别是通过加锁实现的,即读的时候加读锁、写的时候加写锁;读提交可重复读隔离级别是通过多版本并发控制系统(MVCC)实现的,数据库里会创建一个视图,访问的时候以视图的逻辑结果为准。

二、视图:MVCC是如何实现快照功能的

事务ID:每个事务都有一个唯一的ID标识,它是系统按照事务启动顺序分配的,严格递增

数据多版本:数据库的每行数据存在多个版本,每次事务的更新操作都会产生一个新的版本,并把事务ID与这个数据行的版本对应起来,记为 row trx_id,旧的版本也会被保留,通过 undo log 可以从新版本推算得出旧版本,大概的样子如下: 20201216161828 图片来源《极客时间-Mysql实战45讲》

InnoDB 利用了“所有数据都有多个版本”的这个特性,实现了“秒级创建快照”的能力

三、可重复读的实现

create table T(c int) engine=InnoDB;
insert into T(c) values(1);
事务A(100)) 事务B(101)
启动事务A查询得到值 1(99) 启动事务B
查询得到值 1(99)
将 1 改成 2(99, 101)
查询得到值 V1(99, 101)
提交事务 B
查询得到值 V2(99, 101)
提交事务 A
查询得到值 V3(99, 101)

假设改行数据的 row trx_id 为 99,事务A的ID为100,事务B的ID为100,表格中标注了每次操作过后改行数据的版本信息

可重复读隔离级别下,事务启动的同时会创建视图,但是要注意启动事务的方式不同,事务的启动时机会有所差距

启动事务的方式 启动时机
begin/start transaction 在执行第一个操作InnoDB语句的时候
start transaction with consistent snapshot 立马启动

事务B更新操作过后,数据行存在两个版本:99、101,当事务A查询此行数据时,先查到的是101版本,但是发现101比自己的事务ID大,说明这个事务是在自己启动以后启动的,它做的更改我可以忽略,查找上一个版本,发现它的版本是99比自己的事务ID小,说明它在自己启动以前就被更改了,我应该取这个版本。所以 V1、V2都是 1

最后一次的查询操作,它其实也是一个事务,我们假设它的事务ID是105,它查询到数据行的最新版本是 101,比自己的事务ID小,接受了这个版本,所以V3=2

更新逻辑

create table T(c int) engine=InnoDB;
insert into T(c) values(1);
事务A(100) 事务B(101)
启动事务A查询得到值 1(99) 启动事务B
执行操作 c = c+1(99, 101)
查询得到值 V1(99, 101)
提交事务 B
执行操纵 c = c+1
查询得到值 V2(99, 100, 101)
提交事务 A

结果: V1=1, V2=3 原因: 更新数据都是先读后写的,而这个读,只能读当前的值,称为“当前读”(current read)

查询V1时,数据行存在两个版本99、101,且 101 还未提交,不可见,所以 V1=1

事务B执行更新操作时,需要先去查询当前行的值,且这个查询操作是当前读,即是说要读取最新版本,由于事务B已经提交,所以101版本是可见的,查询得到值是 2,进行 +1 操作,得到数据版本100对应的值是3

查询 V2 时,先看到 101 版本,比自己的事务ID大,忽略;查询上一个版本是 100,是自己的事务更新的,它接受了这个值,所以 V2=3