五分钟教你手写 SpringBoot 本地事务管理实现

白菜Java自习室 涵盖核心知识

1. SpringBoot 事务

一直在用 SpringBoot 中的 @Transactional 来做事务管理,但是很少没想过 SpringBoot 是如何实现事务管理的,今天从源码入手,看看 @Transactional 是如何实现事务的,最后我们结合源码的理解,自己动手写一个类似的注解来实现事务管理,帮助我们加深理解。

1.1. 事务的隔离级别

事务为什么需要隔离级别呢?这是因为在并发事务情况下,如果没有隔离级别会导致如下问题:

  • 脏读 (Dirty Read) :当A事务对数据进行修改,但是这种修改还没有提交到数据库中,B事务同时在访问这个数据,由于没有隔离,B获取的数据有可能被A事务回滚,这就导致了数据不一致的问题。
  • 丢失修改 (Lost To Modify):当A事务访问数据100,并且修改为100-1=99,同时B事务读取数据也是100,修改数据100-1=99,最终两个事务的修改结果为99,但是实际是98。事务A修改的数据被丢失了。
  • 不可重复读 (Unrepeatable Read):指A事务在读取数据X=100的时候,B事务把数据X=100修改为X=200,这个时候A事务第二次读取数据X的时候,发现X=200了,导致了在整个A事务期间,两次读取数据X不一致了,这就是不可重复读。
  • 幻读 (Phantom Read):幻读和不可重复读类似。幻读表现在,当A事务读取表数据时候,只有3条数据,这个时候B事务插入了2条数据,当A事务再次读取的时候,发现有5条记录了,平白无故多了2条记录,就像幻觉一样。

不可重复读 VS 幻读

不可重复读的重点是修改 :同样的条件 , 你读取过的数据 , 再次读取出来发现值不一样了,重点在更新操作。
幻读的重点在于新增或者删除:同样的条件 , 第 1 次和第 2 次读出来的记录数不一样,重点在增删操作。

所以,为了避免上述的问题,事务中就有了隔离级别的概念,在Spring中定义了五种表示隔离级别的常量 TransactionDefinition:

  • ISOLATION_DEFAULT:数据库默认的隔离级别,MySQL默认采用的 REPEATABLE_READ 隔离级别。
  • ISOLATION_READ_UNCOMMITTED:最低的隔离级别,允许读取未提交的数据变更,可能会导致脏读、幻读或不可重复读。
  • ISOLATION_READ_COMMITTED:允许读取并发事务已经提交的数据,可以阻止脏读,但是幻读或不可重复读仍有可能发生。
  • ISOLATION_REPEATABLE_READ:对同一字段的多次读取结果都是一致的,除非数据是被本身事务自己所修改,可以阻止脏读和不可重复读,但幻读仍有可能发生。MySQL中通过MVCC解决了该隔离级别下出现幻读的可能。
  • ISOLATION_SERIALIZABLE:串行化隔离级别,该级别可以防止脏读、不可重复读以及幻读,但是串行化会影响性能。

1.2. Spring中事务的传播机制

为什么Spring中要搞一套事务的传播机制呢?这是Spring给我们提供的事务增强工具,主要是解决方法之间调用,事务如何处理的问题。比如有方法A、方法B和方法C,在A中调用了方法B和方法C。伪代码如下:

MethodA() {
 MethodB();
 MethodC();
}

假设三个方法中都开启了自己的事务,那么他们之间是什么关系呢?MethodA的回滚会影响MethodB和MethodC吗?Spring中的事务传播机制就是解决这个问题的。
Spring中定义了七种事务传播行为:

  • PROPAGATION_REQUIRED: 如果存在一个事务,则支持当前事务。如果没有事务则开启一个新的事务。
  • PROPAGATION_SUPPORTS: 如果存在一个事务,支持当前事务。如果没有事务,则非事务的执行。但是对于事务同步的事务管理器,PROPAGATION_SUPPORTS与不使用事务有少许不同。
  • PROPAGATION_MANDATORY: 如果已经存在一个事务,支持当前事务。如果没有一个活动的事务,则抛出异常。
  • PROPAGATION_REQUIRES_NEW: 总是开启一个新的事务。如果一个事务已经存在,则将这个存在的事务挂起。
  • PROPAGATION_NOT_SUPPORTED: 总是非事务地执行,并挂起任何存在的事务。
  • PROPAGATION_NEVER: 总是非事务地执行,如果存在一个活动事务,则抛出异常。
  • PROPAGATION_NESTED: 如果一个活动的事务存在,则运行在一个嵌套的事务中。 如果没有活动事务, 则按 TransactionDefinition.PROPAGATION_REQUIRED 属性执行。

1.3. Spring中事务如何实现异常回滚的

回顾完了事务的相关知识,接下来我们正式来研究下 Spring Boot 中如何通过 @Transactional 来管理事务的,我们重点看看它是如何实现回滚的。
在 Spring 中 TransactionInterceptor 和 PlatformTransactionManager 这两个类是整个事务模块的核心,我们重点研究下这两个类的源码。

  • TransactionInterceptor 负责拦截方法执行,进行判断是否需要提交或者回滚事务。
  • PlatformTransactionManager 是 Spring 中的事务管理接口,真正定义了事务如何回滚和提交。

TransactionInterceptor 类中的代码有很多,我简化一下逻辑,方便说明:

 // 以下代码省略部分内容
 public Object invoke(MethodInvocation invocation) throws Throwable {
  // 获取事务调用的目标方法
  Class<?> targetClass = (invocation.getThis() != null ? AopUtils.getTargetClass(invocation.getThis()) : null);
  // 执行带事务调用
  return invokeWithinTransaction(invocation.getMethod(), targetClass, invocation::proceed);
 }

invokeWithinTransaction 简化逻辑如下:

 // 以下代码省略部分内容
 protected Object invokeWithinTransaction(Method method, @Nullable Class<?> targetClass, final InvocationCallback invocation) throws Throwable {
  Object retVal;
  try {
   // 调用真正的方法体
   retVal = invocation.proceedWithInvocation();
  }
  catch (Throwable ex) {
   // 如果出现异常,执行事务异常处理
   completeTransactionAfterThrowing(txInfo, ex);
   throw ex;
  }
  finally {
   // 最后做一下清理工作,主要是缓存和状态等
   cleanupTransactionInfo(txInfo);
  }
  // 如果没有异常,直接提交事务
  commitTransactionAfterReturning(txInfo);
  return retVal;
 }

事务出现异常回滚的逻辑 completeTransactionAfterThrowing 如下:

 // 以下代码省略部分内容
 protected void completeTransactionAfterThrowing(@Nullable TransactionInfo txInfo, Throwable ex) {
  // 判断是否需要回滚,判断的逻辑就是看有没有声明事务属性,同时判断是不是在目前的这个异常中执行回滚
  if (txInfo.transactionAttribute != null && txInfo.transactionAttribute.rollbackOn(ex)) {
   // 执行回滚
   txInfo.getTransactionManager().rollback(txInfo.getTransactionStatus());
  }
  else {
   // 否则不需要回滚,直接提交即可
   txInfo.getTransactionManager().commit(txInfo.getTransactionStatus());
  }
 }

上面的代码已经把 Spring 的事务的基本原理说清楚了,如何进行判断执行事务,如何回滚。下面到了真正执行回滚逻辑的代码中 PlatformTransactionManager 接口的子类,我们以 JDBC 的事务为例,DataSourceTransactionManager 就是 jdbc 的事务管理类。跟踪上面的代码rollback(txInfo.getTransactionStatus()) 可以发现最终执行的代码如下:

 @Override
 protected void doRollback(DefaultTransactionStatus status) {
  DataSourceTransactionObject txObject = (DataSourceTransactionObject) status.getTransaction();
  Connection con = txObject.getConnectionHolder().getConnection();
  if (status.isDebug()) {
   logger.debug("Rolling back JDBC transaction on Connection [" + con + "]");
  }
  try {
   // 调用jdbc的 rollback进行回滚事务
   con.rollback();
  }
  catch (SQLException ex) {
   throw new TransactionSystemException("Could not roll back JDBC transaction", ex);
  }
 }

这里小结下 Spring 中事务的实现思路,Spring 主要依靠 TransactionInterceptor 来拦截执行方法体,判断是否开启事务,然后执行事务方法体,方法体中 catch 住异常,接着判断是否需要回滚,如果需要回滚就委托真正的 TransactionManager 比如 JDBC 中的 DataSourceTransactionManager 来执行回滚逻辑。提交事务也是同样的道理。
这里用个流程图展示下思路:

2. 手写注解实现事务回滚

我们弄清楚了 Spring 的事务执行流程,那我们可以模仿着自己写一个注解,实现遇到指定异常就回滚的功能。这里持久层就以最简单的 JDBC 为例。我们先梳理下需求,首先注解我们可以基于 Spring 的 AOP 来实现,接着既然是 JDBC,那么我们需要一个类来帮我们管理连接,用来判断异常是否回滚或者提交。

2.1. Maven 加入依赖

  <dependency>
   <groupId>org.springframework.boot</groupId>
   <artifactId>spring-boot-starter-aop</artifactId>
  </dependency>
  <dependency>
   <groupId>org.springframework.boot</groupId>
   <artifactId>spring-boot-starter-jdbc</artifactId>
  </dependency>

2.2. 新建一个注解

@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
public @interface MyTransaction {
 // 指定异常回滚
 Class<? extends Throwable>[] rollbackFor() default {};
}

2.3. 新建连接管理器

该类帮助我们管理连接,该类的核心功能是把取出的连接对象绑定到线程上,方便在 AOP 处理中取出,进行提交或者回滚操作。

@Component
public class DataSourceConnectHolder {

 @Autowired
 private DataSource dataSource;
 /**
  * 线程绑定对象
  */
 ThreadLocal<Connection> resources = new NamedThreadLocal<>("Transactional resources");

 public Connection getConnection() {
  Connection con = resources.get();
  if (con != null) {
   return con;
  }
  try {
   con = dataSource.getConnection();
   // 为了体现事务,全部设置为手动提交事务
   con.setAutoCommit(false);
  } catch (SQLException e) {
   e.printStackTrace();
  }
  resources.set(con);
  return con;
 }

 public void cleanHolder() {
  Connection con = resources.get();
  if (con != null) {
   try {
    con.close();
   } catch (SQLException e) {
    e.printStackTrace();
   }
  }
  resources.remove();
 }
}

2.4. 新建一个切面

这部分是事务处理的核心,先获取注解上的异常类,然后捕获住执行的异常,判断异常是不是注解上的异常或者其子类,如果是就回滚,否则就提交。

@Aspect
@Component
public class MyTransactionAopHandler {

 @Autowired
 private DataSourceConnectHolder connectHolder;

 Class<? extends Throwable>[] es;

 // 拦截所有MyTransaction注解的方法
 @org.aspectj.lang.annotation.Pointcut("@annotation(你的包路径.MyTransaction)")
 public void Transaction() {

 }

 @Around("Transaction()")
 public Object TransactionProceed(ProceedingJoinPoint proceed) throws Throwable {
  Object result = null;
  Signature signature = proceed.getSignature();
  MethodSignature methodSignature = (MethodSignature) signature;
  Method method = methodSignature.getMethod();
  if (method == null) {
   return result;
  }
  MyTransaction transaction = method.getAnnotation(MyTransaction.class);
  if (transaction != null) {
   es = transaction.rollbackFor();
  }
  try {
   result = proceed.proceed();
  } catch (Throwable throwable) {
   // 异常处理
   completeTransactionAfterThrowing(throwable);
   throw throwable;
  }
  // 直接提交
  doCommit();
  return result;
 }

 /**
  * 执行回滚,最后关闭连接和清理线程绑定
  */
 private void doRollBack() {
  try {
   connectHolder.getConnection().rollback();
  } catch (SQLException e) {
   e.printStackTrace();
  } finally {
   connectHolder.cleanHolder();
  }

 }

 /**
  * 执行提交,最后关闭连接和清理线程绑定
  */
 private void doCommit() {
  try {
   connectHolder.getConnection().commit();
  } catch (SQLException e) {
   e.printStackTrace();
  } finally {
   connectHolder.cleanHolder();
  }
 }

 /**
  * 异常处理,捕获的异常是目标异常或者其子类,就进行回滚,否则就提交事务。
  */
 private void completeTransactionAfterThrowing(Throwable throwable) {
  if (es != null && es.length > 0) {
   for (Class<? extends Throwable> e : es) {
    if (e.isAssignableFrom(throwable.getClass())) {
     doRollBack();
    }
   }
  }
  doCommit();
 }

}

2.4. 编写一个 Service

saveTest 方法调用了2个插入语句,同时声明了 @MyTransaction 事务注解,遇到 Exception 就进行回滚。

@Service
public class MyTransactionTest {

 @Autowired
 private DataSourceConnectHolder holder;

 // 一个事务中执行两个sql插入
 @MyTransaction(rollbackFor = NullPointerException.class)
 public void saveTest(int id) {
  save(id, "白菜Java自习室");
  save(id + 10, "白菜Java自习室");
  throw new RuntimeException();
 }

 // 执行sql
 private void save(int id, String value) {
  String sql = "insert into test values(?,?)";
  Connection connection = holder.getConnection();
  PreparedStatement stmt = null;
  try {
   stmt = connection.prepareStatement(sql);
   stmt.setInt(1, id);
   stmt.setString(2, value);
   stmt.executeUpdate();
  } catch (SQLException e) {
   e.printStackTrace();
  }
 }

}

我们自己通过 JDBC 结合 Spring 的 AOP 自己写了个 @MyTransactional 的注解,实现了遇到指定异常回滚的功能。

到此这篇关于五分钟教你手写 SpringBoot 本地事务管理实现的文章就介绍到这了,更多相关SpringBoot 本地事务管理内容请搜索我们以前的文章或继续浏览下面的相关文章希望大家以后多多支持我们!

(0)

相关推荐

  • Spring boot jpa 删除数据和事务管理的问题实例详解

    今天我们介绍的是jpa删除和事务的一些坑,接下来看看具体内容. 业务场景(这是一个在线考试系统)和代码:根据问题的id删除答案 repository层: int deleteByQuestionId(Integer questionId); service 层: public void deleteChoiceAnswerByQuestionId(Integer questionId) { choiceAnswerRepository.deleteByQuestionId(questionId)

  • 详解Springboot事务管理

    在Spring Boot事务管理中,实现自接口PlatformTransactionManager. public interface PlatformTransactionManager { org.springframework.transaction.TransactionStatus getTransaction(org.springframework.transaction.TransactionDefinition transactionDefinition) throws org.

  • 详解SpringBoot的事务管理

    Springboot内部提供的事务管理器是根据autoconfigure来进行决定的. 比如当使用jpa的时候,也就是pom中加入了spring-boot-starter-data-jpa这个starter之后. Springboot会构造一个JpaTransactionManager这个事务管理器. 而当我们使用spring-boot-starter-jdbc的时候,构造的事务管理器则是DataSourceTransactionManager. 这2个事务管理器都实现了spring中提供的Pl

  • Spring Boot多数据源及其事务管理配置方法

    准备工作 先给我们的项目添加Spring-JDBC依赖和需要访问数据库的驱动依赖. 配置文件 spring.datasource.prod.driverClassName=com.mysql.jdbc.Driver spring.datasource.prod.url=jdbc:mysql://127.0.0.1:3306/prod spring.datasource.prod.username=root spring.datasource.prod.password=123456 spring

  • springboot中事务管理@Transactional的注意事项与使用场景

    前言:在service层的方法上使用@Transactional 即可实现处理数据库发生错误时触发事务回滚机制. 注意: Spring 基于注解的声明式事物 @Transactional 默认情况下只会对运行期异常(java.lang.RuntimeException及其子类)和 Error 进行回滚. 数据库引擎要支持事物,使用InnoDB. @Transactional 只能被应用到public方法上, 对于其它非public的方法,如果标记了@Transactional也不会报错,但方法没

  • 五分钟教你手写 SpringBoot 本地事务管理实现

    白菜Java自习室 涵盖核心知识 1. SpringBoot 事务 一直在用 SpringBoot 中的 @Transactional 来做事务管理,但是很少没想过 SpringBoot 是如何实现事务管理的,今天从源码入手,看看 @Transactional 是如何实现事务的,最后我们结合源码的理解,自己动手写一个类似的注解来实现事务管理,帮助我们加深理解. 1.1. 事务的隔离级别 事务为什么需要隔离级别呢?这是因为在并发事务情况下,如果没有隔离级别会导致如下问题: 脏读 (Dirty Re

  • 五分钟教你了解一下react路由知识

    目录 什么是路由 纯组件的基本使用 纯组件使用的注意点 路由的基本初体验 HashRouter和BrowserRouter Link组件和NavLink组件 Route和Switch组件 路由传参 什么是路由 简单的说就是根据不同的地址,web服务器处理不同的业务以及逻辑. 以下代码全部运行在react脚手架中 纯组件的基本使用 // 组件更新机制: // 只要父组件重新渲染了, 所有的组件子树, 也会更新 // 性能优化 // 1. 减轻state // 2. 避免不必要的重新渲染 (性能优化

  • 五分钟教你使用vue-cli3创建项目(新手入门)

    目录 一.搭建vue环境 二.Vue脚手架工具 三.创建项目 四.选择manually select (enter键确认,并进入下一步) 五.选择完成之后回车 这里我们选择3.x的 六.完成之后回车 出现以下界面 七.回车 出现以下界面 八.回车出现以下界面 九.回车出现以下界面 十.回车出现以下界面 十一.回车出现以下界面 十二.根据提示,启动项目 一.搭建vue环境 安装Nodejs 官网下载Nodejs,如果希望稳定的开发环境则下LTS(Long Time Support)长期支持版,稳定

  • 教你如何写springboot接口 

    首先要明白数据的流通方向: 数据的触发是前端请求后端引起的,遵循传统的mvc规范的话 我们需要pojo mapper service controller 四个层次,Pojo 是于数据库中字段直接对应的 在线搭建一个springboot项目 https://start.spring.io/ 其中需要加入的四个依赖 点击确定 把没有用的文件删除 最后保留一下两个: 在此处添加jdk的版本: 开始编写接口实现 pon.xml <?xml version="1.0" encoding=

  • 五分钟教你Android-Kotlin项目编写

    背景 之前就看到过Kotlin这一门语言,也有不少大神和愿意走在知识最前沿的哥哥姐姐们说这一门语言有多么多么的好,但是本人并没有去了解他,直到前段时间Google大会直接说会支持Kotlin语言,所以我就抽出了一点时间准备学习一下,个人觉得到目前为止这个东西并不是什么刚需,有兴趣可以学习,不想学影响也不是很大,好了关于这门语言有多好,有多叼,我就不多少了,想要了解的出门百度,这里给上源码链接Kotlin-Android项目. 插件安装 环境搭建当然是第一步,也是最重要的一步,但是这个Kotlin

  • SpringCloud手写Ribbon实现负载均衡

    前言 前面我们学习了 SpringCloud整合Consul,在此基础上我们手写本地客户端实现类似Ribbon负载均衡的效果. 注: order 模块调用者 记得关闭 @LoadBalanced 注解. 我们这里只演示 注册中心 consul,至于 zookeeper 也是一模一样. 生产者 member模块 member 服务需要集群,所以我们copy application-consul.yml 文件命名为 application-consul2.yml 服务别名一致,只需要修改端口号即可.

  • 10分钟教你本地配置多个git ssh连接的方法

    前言 你最近换电脑了吗?还记得如何在本地配置多个 git ssh 连接吗?一般公司用的是自己内网部署的 gitlab 服务器进行代码管理,开发者使用的是公司的用户名和公司的邮箱,而在个人的开源项目中,我们的代码托管于 github,这个时候就需要两个或多个以上的 SSH-Key 去进行登录,方便代码的拉取与推送. 文章大纲 查看所有 ssh key 分别配置 gitlab 内网 和 github 外网 ssh 进行测试 第一步:查看所有 SSH-Key 打开 bash/zsh 终端:执行以下命令

  • 五分钟解锁springboot admin监控新技巧

    最近这一个月由于项目进度紧张,将近一个月没有动静.分享一下最近体会的springboot监控的一些心得体会,供一些规模不是很大的团队做一些监控. 适用场景: 1.项目规模不大 2.用户量不是很大.并发要求不强 3.无专门运维力量 4.精致的团队规模 对于一些常规的项目,或者企业职责分工不是非常明确的单位来说.往往一个系统从需求到设计,开发,测试到最终上线,运维.往往80%的任务由开发团队来完成.由此,开发人员除了要实现系统的功能,还要为客户进行问题咨询答疑以及生产问题解决. 试想,一个应用上线后

随机推荐