MySQL事务的四大特性和实现方式详解

什么是事务:

在关系型数据库中,事务可以是一条sql语句,或一组sql语句,也可以是一整个应用程序.是用户定义的一个操作序列;

即:作为一个逻辑单元需要执行的操作,要么全成功,要么都失败;比如扣库存和创建订单;

事务有4个基本特征:简称为ACID特性.

原子性(Atomicity):一个事务是一个不可分割的工作单位,事务中包括的操作要么都做,要么都不做。

一致性(Consistency):事务必须是从一个一致性状态变为另一个一致性状态.一致性和原子性是密切相关的.

隔离性(Isolation):一个事务的执行不能被其他事务干扰。即一个事务内部的操作及使用的数据对并发的其他事务是隔离的,并发执行的各个事务之间不能互相干扰。

持久性(durability)。持久性也称永久性(permanence),指一个事务一旦提交,它对数据库中数据的改变就应该是永久性的。接下来的其他操作或故障不应该对其有任何影响。

为什么会有事务:

事务的提出就是为了解决并发情况下保持数据一致性的问题

事务是数据库维护数据一致性的单位,在每个事务结束时,都能保持数据一致性。

事务的四大特性是如何实现的

事务的原子性是通过undo log来实现的

undo log的生成

假设有两个表 bank和finance,表中原始数据如图所示,当进行插入,删除以及更新操作时生成的undo log如下面图所示:

从上图可以了解到数据的变更都伴随着回滚日志的产生:

(1) 产生了被修改前数据(zhangsan,1000) 的回滚日志;

(2) 产生了被修改前数据(zhangsan,0) 的回滚日志。

根据上面流程可以得出如下结论:

1.每条数据变更(insert/update/delete)操作都伴随一条undo log的生成,并且回滚日志必须先于数据持久化到磁盘上;

2.所谓的回滚就是根据回滚日志做逆向操作,比如delete的逆向操作为insert,insert的逆向操作为delete,update的逆向为update等。

根据undo log 进行回滚

为了做到同时成功或者失败,当系统发生错误或者执行rollback操作时需要根据undo log 进行回滚。

回滚操作就是要还原到原来的状态,undo log记录了数据被修改前的信息以及新增和被删除的数据信息,根据undo log生成回滚语句,比如:

(1) 如果在回滚日志里有新增数据记录,则生成删除该条的语句;

(2) 如果在回滚日志里有删除数据记录,则生成生成该条的语句;

(3) 如果在回滚日志里有修改数据记录,则生成修改到原先数据的语句。

事务的持久性是通过redo log 来实现的

事务一旦提交,其所作的修改就会永久保存到数据库中,此时即使系统崩溃修改数据也不会丢失.

mysql的数据存储机制,mysql的表数据是存放在磁盘上的,因此想要存取的时候都要经历磁盘IO,然而即使是使用SSD磁盘IO也是非常消耗性能的

为了提升性能InnoDB提供了缓冲池(Buffer Pool),Buffer Pool中包含了磁盘数据页的映射,可以当做缓存来使用:

读数据:会首先从缓冲池中读取,如果缓冲池中没有,则从磁盘读取在放入缓冲池;

写数据:会首先写入缓冲池,缓冲池中的数据会定期同步到磁盘中;

上面这种缓冲池的措施虽然在性能方面带来了质的飞跃,但是它也带来了新的问题,当MySQL系统宕机,断电的时候可能会丢数据!!!

因为我们的数据已经提交了,但此时是在缓冲池里头,还没来得及在磁盘持久化,所以我们急需一种机制需要存一下已提交事务的数据,为恢复数据使用。

redo log产生:

既然redo log也需要存储,也涉及磁盘IO为啥还用它

1.redo log的存储是顺序存储,而缓存同步是随机操作

2.缓存同步是以数据页为单位的,每次传输的数据大小大于redo log

事务的隔离性是通过(读写锁+MVCC)来实现的

后面主要讲这个,所以留到后面讲

而事务的一致性是通过原子性,持久性,隔离性来实现的

写与写隔离基于锁实现,读与写隔离基于MVCC实现

什么是MVCC

MVCC即多版本并发控制。MVCC是一种并发控制的方法,一般在数据管理系统中,实现对数据库的并发访问,在编程语言中实现事务内存

MVCC在MySQL InnoDB中的实现主要是为了提高数据库并发性能,用更好的方式去处理读-写冲突,做到即使有读写冲突时,也能做到不加锁,非阻塞并发读

数据库隔离级别读已提交,可重复读都是基于MVCC实现

MVCC实现的关键点

事务的版本号:

事务每次开启前,都会从数据库中获得一个自增长的事务ID,可以从事务ID判断事务的执行先后顺序

隐式字段:

对于InnoDB存储引擎,每一行记录都有两个隐藏列trx_id ,roll_pointer如果表中没有主键和非Null唯一键时,则还会有第三个隐藏的主键列row_id

undo log

undo log ,回滚日志,用于记录数据被修改前的信息。在表记录修改前,会把数据拷贝到undo log里面,如果事务回滚,即可以通过undo log 来还原数据

undo log的用途

1.事务回滚,保证原子性和一致性

2.用于MVCC快照读

版本链

多个事务并行操作某一行数据时,不同事物对该行数据的修改会产生多个版本,然后通过回滚指针,连成一个链表,这个链表就称为版本链

其实,通过版本链,我们就可以看出事务版本号、表格隐藏的列和undo log它们之间的关系。我们再来小分析一下。

  1. 假设现在有一张core_user表,表里面有一条数据,id为1,名字为孙权:

  1. 现在开启一个事务A: 对core_user表执行update core_user set name ="曹操" where id=1,会进行如下流程操作
  • 首先获得一个事务ID=100
  • 把core_user表修改前的数据,拷贝到undo log
  • 修改core_user表中,id=1的数据,名字改为曹操
  • 把修改后的数据事务Id=101改成当前事务版本号,并把roll_pointer指向undo log数据地址。

快照读和当前读

快照读:读取的是记录数据的可见版本。不加锁,普通的select语句都是快照读

当前读:读取的是记录数据的最新版本,显示加锁都是当前读

Read View

Read View是什么

它是事物执行sql语句时,产生的读视图。实际上在innoDB中,每个sql语句执行前都会得到一个读视图

Read View有什么用

它主要是用来做可见性判断的,即判断当前事务可见哪个版本的数据

Read view中的重要属性

m_ids:当前系统中那些活跃(没有提交)的读写事务ID,数据结构为List

min_limit_id:表示在生成ReadView时,当前系统中活跃的读写事务中最小的事务id,即m_ids中的最小值。

max_limit_id:表示生成ReadView时,系统中应该分配给下一个事务的id值

creator_trx_id:创建当前read view的事务ID

ReadView匹配条件规则

1.如果数据事务ID trx_id

2.如果trx_id >=max_limit_id ,表明生成该版本的事务在生成 ReadView后才生成,所以该版本不可以被当前事务访问。

3.如果min_limit_id=

  • (1).如果m_ids包含trx_id,则代表Read View生成时刻,这个事务还未提交,但是如果数据的trx_id等于creator_trx_id的话,表明数据是自己生成的,因此是可见的。
  • (2)如果m_ids包含trx_id,并且trx_id不等于creator_trx_id,则Read View生成时,事务未提交,并且不是自己生产的,所以当前事务也是看不见的;
  • (3).如果m_ids不包含trx_id,则说明你这个事务在Read View生成之前就已经提交了,修改的结果,当前事务是能看见的。

MVCC实现原理分析

查询一条记录,基于MVCC,是怎么样的流程

1.获取事务自己的版本号,即事务ID

2.获取ReadView

3.查询得到的数据,然后ReadView中的事务版本号进行比较

4.如果不符合ReadView的可见性规则,即需要UndoLog中的历史快照

5.最后返回符合规则的数据

InooDB实现MVCC,是通过Read View +Undo log实现的,Undo log保存了历史快照,Read View可见性规则帮助判断当前版本的数据是否可见。

搞清楚几个关键点:

视图的生成的时间:每个sql语句执行前都会生成一个视图

每个事务所匹配的视图:是本事务生成的视图

trx_id : 为版本链中的事务id,是已提交的事务id

creator_trx_id:为创建ReadView的事务id,就是ReadView属于哪个事务

m_ids:为生成该ReadView时,还没有提交的事务集合,存储的是事务的id

max_limit_id:为所有未提交的事务的id最大值+1,即要分配给下一个还未执行的事务的id

min_limit_id:为所有未提交的事务的id最小值

读已提交,在事务中的每条sql执行前都会生成新的ReadView,会导致不可重复读的现象

读已提交(RC)隔离级别,存在不可重复读问题的分析历程

  1. 创建core_user表,插入一条初始化数据,如下:

  1. 隔离级别设置为读已提交(RC),事务A和事务B同时对core_user表进行查询和修改操作。

ini复制代码事务A: select * fom core_user where id=1 事务B: update core_user set name =”曹操”

执行流程如下:

最后事务A查询到的结果是,name=曹操的记录,我们基于MVCC,来分析一下执行流程:

(1). A开启事务,首先得到一个事务ID为100

(2).B开启事务,得到事务ID为101

(3).事务A生成一个Read View,read view对应的值如下

变量

m_ids

100,101

max_limit_id

102

min_limit_id

100

creator_trx_id

100

然后回到版本链:开始从版本链中挑选可见的记录:

由图可以看出,最新版本的列name的内容是孙权,该版本的trx_id值为100。开始执行read view可见性规则校验:

ini复制代码min_limit_id(100)=<trx_id(100)<102; creator_trx_id = trx_id =100;

由此可得,trx_id=100的这个记录,当前事务是可见的。所以查到是name为孙权的记录。

(4). 事务B进行修改操作,把名字改为曹操。把原数据拷贝到undo log,然后对数据进行修改,标记事务ID和上一个数据版本在undo log的地址。

(5) 提交事务

(6) 事务A再次执行查询操作,新生成一个Read View,Read View对应的值如下

变量

m_ids

100

max_limit_id

102

min_limit_id

100

creator_trx_id

100

然后再次回到版本链:从版本链中挑选可见的记录:

从图可得,最新版本的列name的内容是曹操,该版本的trx_id值为101。开始执行Read View可见性规则校验:

ini复制代码min_limit_id(100)=<trx_id(101)<max_limit_id(102); 但是,trx_id=101,不属于m_ids集合

因此,trx_id=101这个记录,对于当前事务是可见的。所以SQL查询到的是name为曹操的记录。

综上所述,在读已提交(RC)隔离级别下,同一个事务里,两个相同的查询,读取同一条记录(id=1),却返回了不同的数据(第一次查出来是孙权,第二次查出来是曹操那条记录),因此RC隔离级别,存在不可重复读并发问题。

可复读(RR)隔离级别,解决不可重复读问题

如何解决不可重复读问题嘞,就是通过不同的ReadView生成方式

在RR中一个事务只能生成一个ReadView,就是在事务生成的时候创建

这样就可以避免出现不可重复读的问题了

在RR隔离级别下,是如何解决不可重复读问题的呢?我们一起再来看下,

还是4.2小节那个流程,还是这个事务A和事务B,如下:

4.3.1 不同隔离级别下,Read view的工作方式不同

实际上,各种事务隔离级别下的Read view工作方式,是不一样的,RR可以解决不可重复读问题,就是跟Read view工作方式有关。

  • 在读已提交(RC)隔离级别下,同一个事务里面,每一次查询都会产生一个新的Read View副本,这样就可能造成同一个事务里前后读取数据可能不一致的问题(不可重复读并发问题)。

begin

select * from core_user where id =1

生成一个Read View

/

/

/

/

select * from core_user where id =1

生成一个Read View

  • 在可重复读(RR)隔离级别下,一个事务里只会获取一次read view,都是副本共用的,从而保证每次查询的数据都是一样的。

begin

select * from core_user where id =1

生成一个Read View

/

/

select * from core_user where id =1

共用一个Read View副本

4.3.2 实例分析

我们穿越下,回到刚4.2的例子,然后执行第2个查询的时候:

事务A再次执行查询操作,复用老的Read View副本,Read View对应的值如下

变量

m_ids

100,101

max_limit_id

102

min_limit_id

100

creator_trx_id

100

然后再次回到版本链:从版本链中挑选可见的记录:

从图可得,最新版本的列name的内容是曹操,该版本的trx_id值为101。开始执行read view可见性规则校验:

scss复制代码min_limit_id(100)=<trx_id(101)<max_limit_id(102); 因为m_ids{100,101}包含trx_id(101), 并且creator_trx_id (100) 不等于trx_id(101)

所以,trx_id=101这个记录,对于当前事务是不可见的。这时候呢,版本链roll_pointer跳到下一个版本,trx_id=100这个记录,再次校验是否可见:

scss复制代码min_limit_id(100)=<trx_id(100)< max_limit_id(102); 因为m_ids{100,101}包含trx_id(100), 并且creator_trx_id (100) 等于trx_id(100)

所以,trx_id=100这个记录,对于当前事务是可见的。即在可重复读(RR)隔离级别下,复用老的Read View副本,解决了不可重复读的问题

参看文章:

Mysql的事务实现原理 - 知乎 (zhihu.com)

看一遍就理解:MVCC原理详解 - 掘金 (juejin.cn)

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值