SpringBoot多数据源环境下事务失效的问题


最近在做毕业设计,其中涉及到一些知识图谱的内容,打算用Neo4j来存储知识图谱内容,与知识图谱无关的数据打算使用MySQL存储,为了后需方便开发,在一开始创建项目的时候就把依赖全部添加进来了,配置文件也都写好了。

就在系统的基本功能快完成的时候,打算测试一下对于MySQL操作的部分有没有bug,一番折腾下来,发现SpringBoot的事务没有生效,百度+Google找了一堆解决方法,都不起作用,于是拿出大杀器——单步调试,看看到底是执行到哪里出了问题。最终执行到下面图片上这行代码,恍然大悟。
202204081646651.png

没有走MySQL的事务,走的是Neo4j的事务,那MySQL里的数据肯定不会回滚。说白了就是SpringBoot的事务还不够智能,没法识别多个数据源。

问题找到了,那就好解决了,但万万没想到网上能搜到的MySQL+Neo4j多数据源事务有关的内容很旧,现在Spring Data Neo4j早就更新了,里面的API变化了很多,所以只能去查看官方文档,找找事务相关的内容。虽然网上现有的示例代码不能用,但其解决方案的思路还是可以用的,最后这个问题终于是解决了,下面记录一下解决方法。

解决方法

思路很简单,自定义一个事务管理器,然后在里面维护多个数据源的事务就好了。下面以我目前的环境为例用代码实现,其他多数据源情况大同小异。

TransactionConfig.java

import org.neo4j.driver.Driver;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.neo4j.core.DatabaseSelectionProvider;
import org.springframework.data.neo4j.core.transaction.Neo4jTransactionManager;
import org.springframework.orm.jpa.JpaTransactionManager;

import javax.persistence.EntityManagerFactory;

@Configuration
public class TransactionConfig {
    /**
     * 定义neo4j事务管理器
     *
     * @param databaseNameProvider
     * @param driver
     * @return
     */
    @Bean("neo4jTransactionManager")
    public Neo4jTransactionManager neo4jTransactionManager(Driver driver,
                                                           DatabaseSelectionProvider databaseNameProvider) {
        return new Neo4jTransactionManager(driver, databaseNameProvider);
    }

    /**
     * 定义mysql事务管理器,必须有transactionManager作为默认事务管理器
     *
     * @param emf
     * @return
     */
    @Bean("transactionManager")
    public JpaTransactionManager jpaTransactionManager(EntityManagerFactory emf) {
        return new JpaTransactionManager(emf);
    }
}

MultiTransaction.java

自定义注解,方便后续使用,因为我有些地方只需要操作MySQL,有些地方只需要操作Neo4j,还有些地方要两个数据库同时操作,所以我自定义了三个注解,分别是MultiTransaction、MySQLTransaction、Neo4jTransaction,三个注解是西安方法一样,这里只给出MultiTransaction的实现。

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface MultiTransaction {
}

TransactionAspect.java

import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.orm.jpa.JpaTransactionManager;
import org.springframework.data.neo4j.core.transaction.Neo4jTransactionManager;
import org.springframework.stereotype.Component;
import org.springframework.transaction.TransactionStatus;
import org.springframework.transaction.support.DefaultTransactionDefinition;

@Aspect
@Component
public class TransactionAspect {
    @Autowired
    @Qualifier("neo4jTransactionManager")
    Neo4jTransactionManager neo4jTransactionManager;
    @Autowired
    @Qualifier("transactionManager")
    JpaTransactionManager jpaTransactionManager;

    @Around("@annotation(MultiTransaction)")
    public Object multiTransaction(ProceedingJoinPoint proceedingJoinPoint) {
        TransactionStatus neo4jTransactionStatus = neo4jTransactionManager
                .getTransaction(new DefaultTransactionDefinition());
        TransactionStatus jpaTransactionStatus = jpaTransactionManager
                .getTransaction(new DefaultTransactionDefinition());
        try {
            Object obj = proceedingJoinPoint.proceed();
            // 事务之间必须是包含关系,不能交叉
            jpaTransactionManager.commit(jpaTransactionStatus);
            neo4jTransactionManager.commit(neo4jTransactionStatus);
            return obj;
        } catch (Throwable throwable) {
            jpaTransactionManager.rollback(jpaTransactionStatus);
            neo4jTransactionManager.rollback(neo4jTransactionStatus);
            throw new RuntimeException(throwable);
        }
    }

    @Around("@annotation(MySQLTransaction)")
    public Object mySQLTransaction(ProceedingJoinPoint proceedingJoinPoint) {
        TransactionStatus jpaTransactionStatus = jpaTransactionManager
                .getTransaction(new DefaultTransactionDefinition());
        try {
            Object obj = proceedingJoinPoint.proceed();
            jpaTransactionManager.commit(jpaTransactionStatus);
            return obj;
        } catch (Throwable throwable) {
            jpaTransactionManager.rollback(jpaTransactionStatus);
            throw new RuntimeException(throwable);
        }
    }

    @Around("@annotation(Neo4jTransaction)")
    public Object neo4jTransaction(ProceedingJoinPoint proceedingJoinPoint) {
        TransactionStatus neo4jTransactionStatus = neo4jTransactionManager
                .getTransaction(new DefaultTransactionDefinition());
        try {
            Object obj = proceedingJoinPoint.proceed();
            neo4jTransactionManager.commit(neo4jTransactionStatus);
            return obj;
        } catch (Throwable throwable) {
            neo4jTransactionManager.rollback(neo4jTransactionStatus);
            throw new RuntimeException(throwable);
        }
    }
}

这样只要在需要开启事务的方法上添加相应的注解,就可以确保发生异常时回滚了。比如:

@MultiTransaction
public Object insert(Object obj) throws Exception {
    // do somthing...
}

声明:迟於|版权所有,违者必究|如未注明,均为原创|本网站采用BY-NC-SA协议进行授权

转载:转载请注明原文链接 - SpringBoot多数据源环境下事务失效的问题


栖迟於一丘