后端面试每日一题 Spring事务、Mysql事务、分布式事务

Spring事务

核心就是 TransactionManager

实际上TransactionTemplate内部也是使用TransactionManager来完成事务管理的,我们之前也看过它的execute方法的实现了,其实内部就是调用了TransactionManager的方法,实际上就是分为这么几步

  1. 开启事务
  2. 执行业务逻辑
  3. 出现异常进行回滚
  4. 正常执行则提交事务

例子

// 定义事务
DefaultTransactionDefinition def = new DefaultTransactionDefinition();
def.setName("SomeTxName");
def.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRED);
// txManager,事务管理器
// 通过事务管理器开启一个事务
TransactionStatus status = txManager.getTransaction(def);
try {
    // 完成自己的业务逻辑
}
catch (MyException ex) {
    // 出现异常,进行回滚
    txManager.rollback(status);
    throw ex;
}
// 正常执行完成,提交事务
txManager.commit(status);

Spring事务抽象的关键就是事务策略的概念,事务策略是通过TransactionManager接口定义的。TransactionManager本身只是一个标记接口,它有两个直接子接口

  1. ReactiveTransactionManager,这个接口主要用于在响应式编程模型下,不是我们要讨论的重点
  2. PlatformTransactionManager,命令式编程模型下我们使用这个接口。
public interface PlatformTransactionManager extends TransactionManager {
 // 开启事务
    TransactionStatus getTransaction(TransactionDefinition definition) throws TransactionException;
 
    // 提交事务
    void commit(TransactionStatus status) throws TransactionException;
 
    // 回滚事务
    void rollback(TransactionStatus status) throws TransactionException;
}

通常来说,我们不会直接实现这个接口,而是通过继承AbstractPlatformTransactionManager,这个类是一个抽象类,主要用作事务管理的模板,这个抽象类已经实现了事务的传播行为以及跟事务相关的同步管理

TransactionDefinition它的主要完成了对事务定义的抽象,这些定义有些是数据库层面本身就有的

  • AbstractPlatformTransactionManager提供了四个常见的子类,其说明如下

img

Mysql事务

四大特性ACID

  • 原子性 Atomicity
  • 一致性 Consistency
  • 隔离型 Isolation
  • 持久性 Durability

事务的隔离级别

  • 读未提交 ,存在脏读、不可重复读、幻读问题
  • 读已提交,解决脏读问题,存在不可重复读、幻读问题
  • 可重复读,解决脏读、不可重复读问题,存在幻读问题
  • 可串行化,解决所有问题

不可重复读跟幻读的区别在于,「前者是数据发生了变化,后者是数据的行数发生了变化」

保存点

我们可以在事务执行的过程中定义保存点,在回滚时直接指定回滚到指定的保存点而不是事务开始之初,有点像我们玩游戏的时候可以存档而不是每次都要重新再来

定义保存点的语法如下:

SAVEPOINT 保存点名称;

当我们想回滚到某个保存点时,可以使用下边这个语句(下边语句中的单词WORKSAVEPOINT是可有可无的):

ROLLBACK [WORK] TO [SAVEPOINT] 保存点名称;

Mysql几个重要知识点

  • 「MySQL默认采用的是自动提交的方式」,也就是说如果不是显示的开始一个事务,则系统会自动向数据库提交结果。在当前连接中,还可以通过设置AUTOCONNIT变量来启用或者禁用自动提交模式。

  • MySQL的默认隔离级别是可重复读(REPEATABLE READ)

  • ACID中的一致性是事务的最终目标,前面提到的原子性、持久性和隔离性,都是为了保证数据库状态的一致性

事务实现原理

  1. InnoDB是一个将表中的数据存储到磁盘上的存储引擎,所以即使关机后重启我们的数据还是存在的。而真正**「处理数据的过程是发生在内存中的」「所以需要把磁盘中的数据加载到内存中,如果是处理写入或修改请求的话,还需要把内存中的内容刷新到磁盘上」。而我们知道读写磁盘的速度非常慢,和内存读写差了几个数量级,所以当我们想从表中获取某些记录时,InnoDB存储引擎需要一条一条的把记录从磁盘上读出来么?不,那样会慢死,InnoDB采取的方式是:「将数据划分为若干个页,以页作为磁盘和内存之间交互的基本单位,InnoDB中页的大小一般为 *16* KB。也就是在一般情况下,一次最少从磁盘中读取16KB的内容到内存中,一次最少把内存中的16KB内容刷新到磁盘中。」**
  2. 我们还需要对MySQL中的日志有一定了解。MySQL的日志有很多种,如二进制日志(bin log)、错误日志、查询日志、慢查询日志等,此外InnoDB存储引擎还提供了两种事务日志:「redo log(重做日志)和undo log(回滚日志)。其中redo log用于保证事务持久性;undo log则是事务原子性和隔离性实现的基础。」
  3. InnoDB作为MySQL的存储引擎,数据是存放在磁盘中的,但如果每次读写数据都需要磁盘IO,效率会很低。为此,InnoDB提供了**「缓存(Buffer Pool)」,Buffer Pool中包含了磁盘中部分数据页的映射,作为访问数据库的缓冲:「当从数据库读取数据时,会首先从Buffer Pool中读取,如果Buffer Pool中没有,则从磁盘读取后放入Buffer Pool;当向数据库写入数据时,会首先写入Buffer Pool,Buffer Pool中修改的数据会定期刷新到磁盘中(这一过程称为刷脏)。」**
  4. InnoDB存储引擎文件主要可以分为两类,表空间文件及重做日志文件(redo log file),表空间文件又可以细分为两类,共享表空间跟独立表空间。「undo log位于共享表空间中的undo段中」,每个表空间都被划分成了若干个页面,「凡是页面的读写都在buffer pool中进行,这意味着undo log也需要先写入到buffer pool,所以undo log的生成也需要持久化,也就是说undo log的生成需要记录对应的redo log」。(注意:不是所有的undo log的生成都会产生对应的redo log,对于操作临时表生成的undo log并不会生成对应的undo log,因为修改临时表而产生的undo日志只需要在系统运行过程中有效,如果系统奔溃了,那么在重启时也不需要恢复这些undo日志所在的页面,所以在写针对临时表的Undo页面时,并不需要记录相应的redo日志。)

持久性实现原理

通过前面的补充知识我们知道InnoDB引入了Buffer Pool来优化读写的性能,但是虽然Buffer Pool优化了性能,但同时也带来了新的问题:「如果MySQL宕机,而此时Buffer Pool中修改的数据还没有刷新到磁盘,就会导致数据的丢失,事务的持久性无法保证」

基于此,redo log就诞生了,「redo log是物理日志,记录的是数据库中数据库中物理页的情况」,redo log包括两部分:一是内存中的日志缓冲(redo log buffer),该部分日志是易失性的;二是磁盘上的重做日志文件(redo log file),该部分日志是持久的。在概念上,innodb通过**「force log at commit」**机制实现事务的持久性,即在事务提交的时候,必须先将该事务的所有事务日志写入到磁盘上的redo log file和undo log file中进行持久化。

看到这里可能有的小伙伴又会有疑问了,既然redo log也需要在事务提交时将日志写入磁盘,为什么它比直接将Buffer Pool中修改的数据写入磁盘(即刷脏)要快呢?主要有以下两方面的原因:

(1)刷脏是随机IO,因为每次修改的数据位置随机,但写redo log是追加操作,属于顺序IO。

(2)刷脏是以数据页(Page)为单位的,MySQL默认页大小是16KB,一个Page上一个小修改都要整页写入;而redo log中只包含真正需要写入的部分,无效IO大大减少。

这里我以文章开头的例子进行说明redo log为何能保证持久性:

// 第一步:开始事务
start transaction;
// 第二步:A账户余额减少减少1000  
update balance set money = money -500 where name= ‘A’;
// 第三步:B账户余额增加1000  
update balance set money = money +500 where name= ‘B’;
// 第四步:提交事务
commit;

img

❝ 这里需要对redo log的刷盘补充一点内容:
MySQL支持用户自定义在commit时如何将log buffer中的日志刷log file中。这种控制通过变量 innodb_flush_log_at_trx_commit 的值来决定。该变量有3种值:0、1、2,「默认为1」。但注意,这个变量只是控制commit动作是否刷新log buffer到磁盘。

  • 当设置为1的时候,事务每次提交都会将log buffer中的日志写入os buffer并调用fsync()函数刷到log file on disk中。这种方式即使系统崩溃也不会丢失任何数据,但是因为每次提交都写入磁盘,IO的性能较差。
  • 当设置为0的时候,事务提交时不会将log buffer中日志写入到os buffer(内核缓冲区),而是每秒写入os buffer并调用fsync()写入到log file on disk中。也就是说设置为0时是(大约)每秒刷新写入到磁盘中的,当系统崩溃,会丢失1秒钟的数据。
  • 当设置为2的时候,每次提交都仅写入到os buffer,然后是每秒调用fsync()将os buffer中的日志写入到log file on disk。

「可以看到设置为0或者2时,都有可能丢失1s的数据」

原子性实现原理

前面提到了,所谓原子性就是指整个事务是一个不可分隔的整体,组成事务的一组SQL要么全部成功,要么全部失败,要达到这个目的就意味着当某一个SQL执行失败时,我们要能够撤销掉其它SQL的执行结果,在MySQL中这是依赖undo log(回滚日志)来实现。

undo log属于**「逻辑日志」前面提到的redo log属于物理日志,记录的是数据页的情况),我们可以这么认为,「当delete一条记录时,undo log中会记录一条对应的insert记录,反之亦然,当update一条记录时,它记录一条对应相反的update记录。」**

但执行发生异常时,会根据undo log中的记录进行回滚。undo log主要分为两种

  1. insert undo log
  2. update undo log

「insert undo log是指在insert 操作中产生的undo log」,因为insert操作的记录,只对事务本身可见,对其他事务不可见。故该undo log可以在事务提交后直接删除,不需要进行purge操作。

「而update undo log记录的是对*delete 和update*操作产生的undo log」,该undo log可能需要提供MVCC机制,因此不能再事务提交时就进行删除。提交时放入undo log链表,等待purge线程进行最后的删除。

❝ 补充:purge线程两个主要作用是:清理undo页和清除page里面带有Delete_Bit标识的数据行。在InnoDB中,事务中的Delete操作实际上并不是真正的删除掉数据行,而是一种Delete Mark操作,在记录上标识Delete_Bit,而不删除记录。是一种"假删除",只是做了个标记,真正的删除工作需要后台purge线程去完成。

这里我们就来看看insert undo log的结构,如下:

img

在上图中,undo type记录的是undo log的类型,对于insert undo log,该值始终为11(TRX_UNDO_INSERT_REC),undo no在一个事务中是从0开始递增的,也就是说只要事务没提交,每生成一条undo日志,那么该条日志的undo no就增1。table id记录undo log所对应的表对象。如果记录中的主键只包含一个列,那么在类型为TRX_UNDO_INSERT_RECundo日志中只需要把该列占用的存储空间大小和真实值记录下来,如果记录中的主键包含多个列(复合主键),那么每个列占用的存储空间大小和对应的真实值都需要记录下来(图中的len就代表列占用的存储空间大小,value就代表列的真实值),「在回滚时只需要根据主键找到对应的列然后删除即可」。end of record记录了下一条undo log在页面中开始的地址,start of record记录了本条undo log在页面中开始的地址。

对undo log有一定了解后,我们再回头看看文章开头的例子,分析下为什么undo log能保证原子性

// 第一步:开始事务
start transaction;
// 第二步:A账户余额减少减少1000  
update balance set money = money -500 where name= ‘A’;
// 第三步:B账户余额增加1000  
update balance set money = money +500 where name= ‘B’;
// 第四步:提交事务
commit;

img

考虑到排版,这里我只画了一条语句的流程图,第二条也是一样的,每次更新或者插入前,先记录undo,再修改内存中数据,再记录redo。

隔离性实现原理

我们知道,一个事务中的读操作是不会影响到另外一个事务的,所以在讨论隔离性我们主要分为两种情况

  1. 一个事务中的写操作,对另外一个事务中写操作的影响
  2. 一个事务中的写操作,对另外一个事务中读操作的影响

写操作之间的隔离是通过锁来实现的,MySQL中的锁机制要详细来讲是很复杂的,要讲明白整个锁需要从索引开始介绍,限于笔者能力及文章篇幅,本文只对MySQL中的锁机制做一个简单的介绍

分布式事务

分布式事务:
- 数据库层面 2PC/XA(直接使用中间件,比如Seata)
- 应用层 TCC(服务接口需要实现三接口)
- 微服务场景:Saga模式(针对状态机建模)
数据库 + MQ的一致性:
- 本地消息表
- 事务消息
涉及外部服务整体一致性:
- 补偿 + 最终一致
- 以外部服务为主,内部服务为辅思想

参考:

  • https://zhuanlan.zhihu.com/p/180540226
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

lakernote

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值