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

网友投稿 237 2022-11-02


五分钟教你手写 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 加入依赖

org.springframework.boot

spring-boot-starter-aop

org.springframework.boot

spring-boot-starter-jdbc

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 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 的注解,实现了遇到指定异常回滚的功能。


版权声明:本文内容由网络用户投稿,版权归原作者所有,本站不拥有其著作权,亦不承担相应法律责任。如果您发现本站中有涉嫌抄袭或描述失实的内容,请联系我们jiasou666@gmail.com 处理,核实后本网站将在24小时内删除侵权内容。

上一篇:Linux 为什么CPU访问硬盘的速度巨慢
下一篇:ELK logstash 过滤插件:JSON
相关文章

 发表评论

暂时没有评论,来抢沙发吧~