事务的实现原理

提到事务,小伙伴们肯定不陌生,在基于spring框架的开发过程中一般是通过@Transactional注解来实现的,使用的场景一般是在一个方法中需要去连续的操作数据库,为了保证数据的一致性,加上事务,但是小伙伴有没有思考过,@Transactional注解这么的神奇么,那么这个注解到底做了什么事情呢,数据库的事务是怎么实现的呢,这个注解会不会失效呢,夺命三连,哈哈哈,不要慌,下面就慢慢的解答这一连串的问题。

事务的特点

想了解事务是怎么实现的,我们首先应该知道事务是什么东东,它能够帮助我们干什么,那我先来介绍一下事务的特性吧,那就是ACID了。

原子性(Atomicity):这里的原子性其实和我们开发中的原子操作其实是很类似的,意思就是一个操作,要么是全部完成,要么就是全部失败,不存在一部分成功一部分失败的,就像在开发中的原子操作.

一致性(Consistency): 这里的一致性指的是数据的一致性,意思是在经过一系列的操作之后,数据库的数据是一致不会存在修改,关键是要体会这个"一致",有个很简单的例子,银行转账,A账户500,B账户300,A向B转了100,那么A400,B400,在转账前后,其AB两个账户的总金额是不会变的,还是800.

隔离性(Isolation):这里是指在并发操作同一个数据库时,是相互隔离的,互相不会产生干扰,关于隔离性,比较重要,后面我会详细讲解。

持久性(Durability): 这里是指事务成功之后对数据的操作是永久保存的,就算数据库发生宕机重启,数据还是可以恢复到事务成功提交时的状态,意思是事务对于数据的数据修改是永久性的。

事务的隔离级别

上面讲到了事务的隔离性,是在于维护多个事务并发操作数据库从而会造成数据不一致的问题,在Mysql中会有几种隔离级别来避免数据不一致问题的发生。

读未提交(Read Uncommited): 该隔离级别是比较低的,从字面意思我们就可以知道,一个事务可以读取到另一个事务没有提交的数据,这样就会造成脏读的现象。

读已提交(Read Commited):这个隔离级别意思是一个事务可以读取到另一个事务已提交的数据,但是这样就会出现一个问题,但一个事务在重复读取一行数据是,不能保证每一次读到的数据是一样的,以为在读取的期间数据可能会被其他的事务进行了修改。但RC可以解决脏读的现象。

可重复度(Repeatable Read):这是Innodb存储引擎的默认隔离级别,意思的话,当前事务开启后,只会看到事务开启时数据数的数据,期间其他事务对此数据的修改是不会看见的,这样就可以避免脏读和不可重复读的显现,但是如果当前事务结束了,又开启了一个新的事务来读取数据,在开启新的数据之前,有其他事务对此数据脏成了修改,这个时候新开启的数据是可以读到修改后的数据,也就会脏成幻读现象。

串行化:最高级别的隔离级别,意味着事务只能串行执行,不能并发执行。

事务的隔离级别的原理

数据库是如何做到事务之间的隔离呢,不知道大家有没有好奇,之前对于不可重复读和幻读的区别很模糊,在了解隔离级别的实现原理之后就慢慢变得清晰了,下面我们来讲一讲。

数据库隔离级别的实现是通过MVCC(Multi-Version Concurrency Control)多版本并发控制,MVCC出现的原因主要是因为早先数据库实现事务隔离的方式是通过锁的,当数据被锁住以后,其他就无法访问,但是对于数据库来说,一般都是读多写少的场景,后面就出现的读写锁,读操作之间不会互斥,只有读写,写写操作会互斥,但随着技术发发展,发现读写不互斥更加好了,所以MVCC就出现了,MVCC是将数据维护成多个版本,每个事务会看到自己版本相应的数据,互不干扰,来达到事务隔离的作用。

我们来看看MVCC在RC和RR中的区别,两个区别在于创建readView的机制不同,RR的视图是在事务开启的时候创建的,在事务的执行过程中用的都是同一个视图不会改变,所以在同一个视图中是不会读取到其他事务进行的操作,而RC视图是在sql语句执行时候生成的视图,在一个事务中,总是生成最新的视图,因此可以读到最新的数据,对于读未提交来说,没有视图的概念,直接返回最新值,串行化不存在事务并发的情况,也就不存在读取不一致的发生。

这里的readview是和undo log日志有关系的,后面我会用一篇来讲解数据库的日志哦。

事务的传播机制

小伙伴们在用@Transactional注解式,有没有注意到其中的参数配置呢,其中就可以配置事务的传播机制,一共有七种,下面我们来看看:

REQUIRED:如果当前方法有事务则加入事务,没有则创建一个事务。

NOT_SUPPORTED:不支持事务,如果当前有事务则挂起事务运行。

REQUIREDS_NEW:新建一个事务并在这个事务中运行,如果当前存在事务就把当前事务挂起。新建的事务的提交与回滚一挂起事务没有联系,不会影响挂起事务的操作。

MANDATORY:强制当前方法使用事务运行,如果当前没有事务则抛出异常。

NEVER:当前方法不能存在事务,即非事务状态运行,如果存在事务则抛出异常。

SUPPORTS:支持当前事务,如果当前没事务也支持非事务状态运行。

NESTED:如果当前存在事务,则在嵌套事务内执行。嵌套事务的提交与回滚与父事务没有任务关系,反之,当父事务提交嵌套事务也一起提交,父事务回滚会也回滚嵌套事务的。如果当前没有事务,则新建一个事务运行,这时候则与PROPAGATION_REQUIRED场景一致。

事务的实现原理

仔细想了一下不说日志的话是没有办法把事务的原理性讲明白的,所以在这里还是加上了。

  • 原子性的实现

首先我们先来看看原子性的实现原理,上面讲事务的特性知道,原子性的核心也就是支持回滚操作的,但事务不提交的过程中能够将数据回退到原始数据,那么数据库怎么知道原始数据是多少的呢,这里我们就要依靠于undo log了,但我们更新一行数据的时候,mysql会把之前的原始数据写入到undo log中,用于事务提交失败时,数据能够得到回滚。

undo log就是保证事务原子性的关键所在。

  • 持久性的实现

这里我们还是从数据更新的步骤看看:

RobJwd.png

这里的redo log日志就是为了数据持久化的目的,刚刚上面提到的undo log是为了记录数据修改后的原始数据,那么这里的redo log就是为了记录数据修改后的数据,当数据库发生宕机就可以根据redo log日志将数据进行恢复。

有的小伙伴可能就有疑问,记得自己项目中mysql数据库的数据恢复是通过binlog日志的,那这里的redo log日志是不是重复了么,mysql需要存储两份日志文件么? 哈哈 当然不是,这个是数据库的版本机制造成的原因。

binlog日志是所有mysql都有的,但是redo log日志是Innodb所特有的,所以如果你的数据库的存储引擎不是用的Innodb的话,是不存在redo log的

其次redo log和bin log所存储的内容也是有所区别的

redo log:存储的是物理修改,比如:某某字段值进行 了修改为88

bin log: 存储的是逻辑修改,是修改对应的sql语句

同时 redo的存储大小是有大小限制的,可以看作一个环,当数据满的时候,会强制io把数据刷新到磁盘,再将已经刷新到磁盘的数据空间清空。记住一点,当发生数据更新发时候,是先写日志,并更新内存,后面到了合适的时机再写入磁盘,这个时间,mysql是可以通过参数来进行设置的。

而bin log是没有大小限制的,它的大小限制可以追加扩展。

还有一点是 ,如果两个日志都存在的情况下,先写redo log,后写 bin log

这里的redo log和bin log就是事务持久化的关键所在。

  • 隔离性的实现

相比来说,隔离性的实现是最为复杂的,数据库为了能够支持事务的并发执行,做了很多设计,其中视图就是重中之重。

在数据库中有两种视图:

一种是普通的视图(view): 在我项目中的应用场景是,因为项目架构是微服务的,每个服务都有自己的独立数据库,在本服务中可能需要操作其他数据库,当然配置多数据源可以解决,但创建视图的成本更小。

另外就是这里要说的基于MVCC的视图(read view),用于支持上面所说的RC和RR的实现。

接下来就来就来说一下,视图是怎么实现RR和RC的

RR的实现

在可重复提交的隔离级别中,在开始事务的时候,会新建一个mysql数据库的数据快照,然后事务在这个快照中进行数据操作,有的小伙伴又有疑问了,数据库要是很大,达到几百g,那么这个快照不是要很久么,哈哈 ,并不是,这里的快照并不是全量复制数据库的数据,当我们开启一个事务的时候,事务都会有一个事务id,并且这个id是递增的,当事务更新了一行数据之后,会将当前事务的id与这个行数据进行绑定,从而数据库中的数据就会有多个版本,这个多个版本的实现就是我们刚刚所将的undo log,它会存储每个版本的数据。 同时在开启一个数据的时候,mysql会为每个事务创建一个数组,数组中存储的是活跃事务,这里的活跃事务就是启动还没提交的事务,由于事务id是递增的特点,这里我们就可以通过排序,找到事务的低水位和高水位,低水位之前的就是已经提交成功的事务。如下图所示:

RT9Ms1.png

这样当前事务就会更根据事务的id去找到当前版本数据已经成功提交的版本,对于还未提交的版本是不可见的。

RC的实现

在读已提交的隔离级别中,视图的创建是每次sql语句创建的过程中,每次sql的创建都是用的最新的视图,所以如果在两次查询的间隙,有其他的事务修改了数据,再次读取就会发生两次数据不一致的情况。

上面说的都是读取数据的情况,那么对于更新数据来说,实际又是怎么样的呢?

对于写数据来说,每次得到的数据都是最新的版本,然后在最新的版本之上进行数据操作,如果有两个数据同时更新同一行数据时,必须要等其中一个事务释放行锁之后再获取行锁进行数据的更新,这样才能保证数据的更新是安全一致的。

Innodb中的行数据很多个版本,每个版本都有自己对应的事务id(row trx_id),每个事务都会有自己对应的视图,会根据事务id来确定事务的可见性。

对于RR可重复读,查询只承认在事务创建之前已经成功提交的数据。

对于RC读已提交,查询只承认在查询语句创建之前已经成功提交的数据。

对于当前读,总是读取当前已经成功提交的版本。

一致性的实现:

这里一致性的保证就是通过上面的原子性,持久性和隔离性来实现的。

好了,关于事务部分就讲到这里了,其实这里还有很多没有仔细的深入,比如redo log的数据接口,写入磁盘的时机,事务的失效场景,事务的嵌套,事务是怎么来防止幻读的等等,希望小伙伴可以自己好好思考,当然了,后面我也会进行讲解。

下一篇我会通过一条sql更新语句来讲解一下数据库的锁,期待哦。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值