多数据源事务-自定义注解+AOP实现

在讲多数据源事务之前,我们先来回顾一下单数据源的情况!

单数据源事务

1. 前期准备

  1. pom.xml:

引入数据库驱动和Mybatis依赖

<dependency>
  <groupId>javax.persistence</groupId>
  <artifactId>javax.persistence-api</artifactId>
</dependency>
<dependency>
  <groupId>mysql</groupId>
  <artifactId>mysql-connector-java</artifactId>
</dependency>
<dependency>
  <groupId>org.projectlombok</groupId>
  <artifactId>lombok</artifactId>
</dependency>
<dependency>
  <groupId>org.mybatis.spring.boot</groupId>
  <artifactId>mybatis-spring-boot-starter</artifactId>
  <version>2.1.3</version>
</dependency>
  1. application.yml:

配置数据源

spring:
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    username: root
    password: xieyelong
    url: jdbc:mysql://localhost:3306/wangshe?serverTimezone=GMT%2B8&allowMultiQueries=true&characterEncoding=UTF8&autoReconnect=true


  main:
    allow-bean-definition-overriding: true
mybatis:
  configuration:
    mapUnderscoreToCamelCase: true
  mapper-locations: classpath:mapper/*.xml

3.StudentMapper.xml mapper就是一个很简单的数据插入

<insert id="insert">
  insert into student(name, age, create_time)
  values (#{name}, #{age}, #{createTime})
</insert>
  1. TeacherMapper.xml
<insert id="insert">
  insert into student(name, age, create_time)
  values (#{name}, #{age}, #{createTime})
</insert>
  1. Service:

service层我们提供三个方法用来测试分别是addStudent(),addTeacher(),addAll() 

其中addAll()调用了addStudent()和addTeacher() 

并且有@Transactional注解修饰,表明开启了事务 

并且其中有 **int i = 1/0 **这里的话肯定是会报一个空指针的异常的

@Autowired
private StudentMapper studentMapper;

@Autowired
private TeacherMapper teacherMapper;

public void addStudent() {
    Student student = new Student();
    student.setAge(RandomUtils.nextInt());
    student.setCreateTime(new Date());
    student.setName("name" + RandomUtils.nextInt());
    studentMapper.insert(student);
}

public void addTeacher() {
    Teacher teacher = new Teacher();
    teacher.setAge(RandomUtils.nextInt());
    teacher.setCreateTime(new Date());
    teacher.setName("name" + RandomUtils.nextInt());
    teacherMapper.insert(teacher);
}

@Transactional
public void addAll() {
    addTeacher();
    int a = 1 / 0;
    addStudent();
}

2. 动手实验

  1. 分别单独执行一下addStudent()和addTeacher()

多数据源事务-自定义注解+AOP实现可以看到数据是成功加入

  1. 再执行一下addAll()

由于addTeacher()和addStudent()中间添加了一个 int a = 1/0 报了异常

java.lang.ArithmeticException: / by zero
 at com.tyrone.multidatasource.service.MultiDatasourceService.addAll(MultiDatasourceService.java:47) ~[classes/:na]

我们再来看看数据库的数据情况多数据源事务-自定义注解+AOP实现数据库中teacher表的数据还是那么多,说到事务生效了!接下来看看多数据源的情况!

多数据源情况

在企业级开发中是存在一个项目里面维护了多个数据源的情况,有些情况还会存在一个方法里面调用不同数据源,对于一些情景就可能需要的控制事务。比如说迁库迁表的情况的下,我们可能采用双写的方案。通常情况下我们要求多个数据源必须都写入了,才能正常结束方法,一旦出现异常,多个数据源都需要回滚


  1. 修改application中的数据库配置,从单数据源切换至多数据源
spring:
  datasource:
    wangshe:
      driver-class-name: com.mysql.cj.jdbc.Driver
      username: root
      password: xieyelong
      url: jdbc:mysql://localhost:3306/wangshe?serverTimezone=GMT%2B8&allowMultiQueries=true&characterEncoding=UTF8&autoReconnect=true
    test:
      driver-class-name: com.mysql.cj.jdbc.Driver
      username: root
      password: xieyelong
      url: jdbc:mysql://localhost:3306/test?serverTimezone=GMT%2B8&allowMultiQueries=true&characterEncoding=UTF8&autoReconnect=true

切完多数据源的之后,如果直接运行会报如下的错误:

Description:

Failed to configure a DataSource: 'url' attribute is not specified and no embedded datasource could be configured.

Reason: Failed to determine a suitable driver class


Action:

Consider the following:
 If you want an embedded database (H2, HSQL or Derby), please put it on the classpath.
 If you have database settings to be loaded from a particular profile you may need to activate it (no profiles are currently active).


进程已结束,退出代码 1

原因是springboot内部默认就是单个配置,你配置多个数据源,springboot 自动注入的时候当然不知道你要注入的是哪个,而且正常来说是获取spring.datasource.XXX 现在多了一个层级,自然就拿不到配置。所以我们需要配置一下

解决方法如下:步骤看起来繁琐,但是实际上总结的话就几步,对你来说肯定敢敢单单啦

  • 注入DataSoucre
  • 注入SqlSessionFactory
  • 注入DataSourceTransactionManager
  • 注入SqlSessionTemplate
/**
@author Tyrone
@date 2022/7/24 13:06
*/

@Configuration
@MapperScan(basePackages = "com.tyrone.multidatasource.mapper.test", sqlSessionTemplateRef = "testSqlSessionTemplate")
public class TestDataSourceConfig {
    //1.注入数据源
    @Bean(name = "testDataSource")
    @ConfigurationProperties(prefix = "spring.datasource.test")
    public DataSource testDataSource() {
        return DataSourceBuilder.create().build();
    }
    
    //2.注入SqlSessionFactory
    @Bean(name = "testSqlSessionFactory")
    public SqlSessionFactory testSqlSessionFactory(@Qualifier("testDataSource") DataSource dataSource) throws Exception {
        SqlSessionFactoryBean bean = new SqlSessionFactoryBean();
        bean.setDataSource(dataSource);
        bean.setMapperLocations(new PathMatchingResourcePatternResolver().getResources("classpath*:mapper/test/*.xml"));
        return bean.getObject();
    }
    
    //3.注入事务管理器
    @Bean("testTransactionManager")
    public DataSourceTransactionManager testTransactionManager(@Qualifier("testDataSource") DataSource dataSource) {
        return new DataSourceTransactionManager(dataSource);
    }
    //4.注入SqlSessionTemplate
    @Bean("testSqlSessionTemplate")
    public SqlSessionTemplate testSqlSessionTemplate(@Qualifier("testSqlSessionFactory") SqlSessionFactory sqlSessionFactory){
        return new SqlSessionTemplate(sqlSessionFactory);
    }
}

/**
 * @author Tyrone
 * @date 2022/7/24 13:06
 */

@Configuration
@MapperScan(basePackages = "com.tyrone.multidatasource.mapper.wangshe", sqlSessionTemplateRef = "wangsheSqlSessionTemplate")
public class WangsheDataSourceConfig {

    //1.注入DataSource
    @Bean("wangsheDataSource")
    @ConfigurationProperties(prefix = "spring.datasource.wangshe")

    public DataSource wangsheDataSource() {
        return DataSourceBuilder.create().build();
    }

    //2.注入SqlSessionFactory
    @Bean("wangsheSqlSessionFactory")

    public SqlSessionFactory wangsheSqlSessionFactory(@Qualifier("wangsheDataSource") DataSource dataSource) throws Exception {
        SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();
        sqlSessionFactoryBean.setDataSource(dataSource);
        sqlSessionFactoryBean.setMapperLocations(
                new PathMatchingResourcePatternResolver().getResources("classpath*:mapper/wangshe/*.xml")
        );
        return sqlSessionFactoryBean.getObject();
    }

    //3.注入DataSourceTransactionManager
    @Bean("wangsheTransactionManger")

    public DataSourceTransactionManager wangsheTransactionManger(@Qualifier("wangsheDataSource") DataSource dataSource) {
        return new DataSourceTransactionManager(dataSource);
    }

    //4.注入SqlSessionTemplate
    @Bean("wangsheSqlSessionTemplate")

    public SqlSessionTemplate wangsheSqlSessionTemplate(@Qualifier("wangsheSqlSessionFactory") SqlSessionFactory sqlSessionFactory) {
        return new SqlSessionTemplate(sqlSessionFactory);
    }

}

注意:一定要添加@Primary注解,不然就会报以下的错误:

org.springframework.beans.factory.NoUniqueBeanDefinitionException: No qualifying bean of type ‘org.springframework.transaction.TransactionManager’ available: expected single matching bean but found 2: testTransactionManager,wangsheTransactionManger


接下看一下效果

2022-07-24 15:42:24.167  INFO 18748 --- [           main] org.apache.catalina.core.StandardEngine  : Starting Servlet engine: [Apache Tomcat/9.0.29]
2022-07-24 15:42:24.213  INFO 18748 --- [           main] o.a.c.c.C.[Tomcat].[localhost].[/]       : Initializing Spring embedded WebApplicationContext
2022-07-24 15:42:24.213  INFO 18748 --- [           main] o.s.web.context.ContextLoader            : Root WebApplicationContext: initialization completed in 634 ms
2022-07-24 15:42:24.453  INFO 18748 --- [           main] o.s.s.concurrent.ThreadPoolTaskExecutor  : Initializing ExecutorService 'applicationTaskExecutor'
2022-07-24 15:42:24.617  INFO 18748 --- [           main] o.s.b.w.embedded.tomcat.TomcatWebServer  : Tomcat started on port(s): 8080 (http) with context path ''
2022-07-24 15:42:24.618  INFO 18748 --- [           main] com.tyrone.multidatasource.Application   : Started Application in 1.29 seconds (JVM running for 1.735)

项目已经是成功启动了

但要注意的是在这个时候多数据源是还没有生效的。

在单数据源的情况下,我们可以通过在方法添加@Trancational注解去开启一个事务,所以我们理所当然的以为在多数据源的情况下,也可以通过在方法上添加@Trancational 注解去开启事务。如果你真的这么以为,那也就说明你是真滴天真!最后效果怎么样呢?我们可以看来一下

@Transactional
public void addAll() {
    addTeacher();
    int a = 1 / 0;
    addStudent();
}

当我们运行这个方法的时候就会报下面的异常:

org.springframework.beans.factory.NoUniqueBeanDefinitionException: No qualifying bean of type 'org.springframework.transaction.TransactionManager' available: expected single matching bean but found 2: testTransactionManager,wangsheTransactionManger

原因就是 @Trancational 只支持单个的事务管理器多数据源事务-自定义注解+AOP实现

我们可以采用以下这两种方案去处理:

  1. 使用编程式事务
        try {
            tc1.commit();
            tc2.commit();
            method();
        }catch (Exception e){
            tc2.rollback();
            tc1.rollback();
        }

使用编程式事务,对于业务代码倾入性太强,比较繁琐的同时还需要注意事务回滚的顺序,不太推荐。

  1. 自己实现多数据源事务注解

既然 @Trancational 只能支持单数据源,那我们就自定义个能够支持多数据源的注解就完事了。所有我们的解决方案就是 aop+自定义注解

  • 引入相关aop依赖
<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-aop</artifactId>
</dependency>
<dependency>
    <groupId>org.aspectj</groupId>
    <artifactId>aspectjweaver</artifactId>
    <version>1.9.4</version>
</dependency>
  • 实现自定义注解
/**
 * @author Tyrone
 * @date 2022/7/26 0:02
 */

@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
public @interface MultiDataSourceTransactional {
    /**
     * 事务管理器
     *
     * @return
     */

    String[] transactionManagers();
}
  • 实现切面
/**
@author Tyrone
@date 2022/7/26 0:03
*/

@Component
@Aspect
public class MultiDataSourceTransactionAspect {
    /**
    * 为什么使用栈为了达到后进先出的效果
    */

    private static final ThreadLocal<Stack<Pair<DataSourceTransactionManager, TransactionStatus>>> THREAD_LOCAL = new ThreadLocal<>();
    
    /**
    * 用于获取事务管理器
    */

    @Autowired
    private ApplicationContext applicationContext;
    
    /**
    * 事务声明
    */

    private DefaultTransactionDefinition def = new DefaultTransactionDefinition();
    {
        // 非只读模式
        def.setReadOnly(false);
        // 事务隔离级别:采用数据库的
        def.setIsolationLevel(TransactionDefinition.ISOLATION_DEFAULT);
        // 事务传播行为
        def.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRED);
    }
    
    /**
    * 切面
    */

    @Pointcut("@annotation(com.tyrone.multidatasource.config.MultiDataSourceTransactional)")
    public void pointcut() {
    }
    
    /**
    * 声明事务
    *
    * @param transactional 注解
    */

    @Before("pointcut() && @annotation(transactional)")
    public void before(MultiDataSourceTransactional transactional) {
        // 根据设置的事务名称按顺序声明,并放到ThreadLocal里
        String[] transactionManagerNames = transactional.transactionManagers();
        Stack<Pair<DataSourceTransactionManager, TransactionStatus>> pairStack = new Stack<>();
        for (String transactionManagerName : transactionManagerNames) {
            DataSourceTransactionManager transactionManager = applicationContext.getBean(transactionManagerName, DataSourceTransactionManager.class);
            TransactionStatus transactionStatus = transactionManager.getTransaction(def);
            pairStack.push(new Pair(transactionManager, transactionStatus));
        }
        THREAD_LOCAL.set(pairStack);
    }
    
    /**
    * 提交事务
    */

    @AfterReturning("pointcut()")
    public void afterReturning() {
        // ※栈顶弹出(后进先出)
        Stack<Pair<DataSourceTransactionManager, TransactionStatus>> pairStack = THREAD_LOCAL.get();
        while (!pairStack.empty()) {
            Pair<DataSourceTransactionManager, TransactionStatus> pair = pairStack.pop();
            pair.getKey().commit(pair.getValue());
        }
        THREAD_LOCAL.remove();
    }
    
    /**
    * 回滚事务
    */

    @AfterThrowing(value = "pointcut()")
    public void afterThrowing() {
        // ※栈顶弹出(后进先出)
        Stack<Pair<DataSourceTransactionManager, TransactionStatus>> pairStack = THREAD_LOCAL.get();
        while (!pairStack.empty()) {
            Pair<DataSourceTransactionManager, TransactionStatus> pair = pairStack.pop();
            pair.getKey().rollback(pair.getValue());
        }
        THREAD_LOCAL.remove();
    }
}
  • 改造一下业务代码
@MultiDataSourceTransactional(transactionManagers = {"wangsheTransactionManger","testTransactionManager"})
public void addAll() {
    addTeacher();
    addStudent();
    int a = 1 / 0;
    }

然后我们看一下实际效果

2022-07-26 00:08:33.304  INFO 14316 --- [           main] o.s.b.w.embedded.tomcat.TomcatWebServer  : Tomcat initialized with port(s): 8080 (http)
2022-07-26 00:08:33.309  INFO 14316 --- [           main] o.apache.catalina.core.StandardService   : Starting service [Tomcat]
2022-07-26 00:08:33.309  INFO 14316 --- [           main] org.apache.catalina.core.StandardEngine  : Starting Servlet engine: [Apache Tomcat/9.0.29]
2022-07-26 00:08:33.354  INFO 14316 --- [           main] o.a.c.c.C.[Tomcat].[localhost].[/]       : Initializing Spring embedded WebApplicationContext
2022-07-26 00:08:33.354  INFO 14316 --- [           main] o.s.web.context.ContextLoader            : Root WebApplicationContext: initialization completed in 682 ms
2022-07-26 00:08:33.632  INFO 14316 --- [           main] o.s.s.concurrent.ThreadPoolTaskExecutor  : Initializing ExecutorService 'applicationTaskExecutor'
2022-07-26 00:08:33.754  INFO 14316 --- [           main] o.s.b.w.embedded.tomcat.TomcatWebServer  : Tomcat started on port(s): 8080 (http) with context path ''
2022-07-26 00:08:33.756  INFO 14316 --- [           main] com.tyrone.multidatasource.Application   : Started Application in 1.317 seconds (JVM running for 1.75)

先来看一下调用方法之前的一个数据库情况:多数据源事务-自定义注解+AOP实现然后我们调用接口,不出意外的出现了异常,然后看一下数据库里面的数据是否发生变化,来验证我们的方案是否有效

java.lang.ArithmeticException: / by zero
 at com.tyrone.multidatasource.service.MultiDatasourceService.addAll(MultiDatasourceService.java:48) ~[classes/:na]
 at com.tyrone.multidatasource.service.MultiDatasourceService$$FastClassBySpringCGLIB$$34738cc8.invoke(<generated>) ~[classes/:na]
 at org.springframework.cglib.proxy.MethodProxy.invoke(MethodProxy.java:218) ~[spring-core-5.2.2.RELEASE.jar:5.2.2.RELEASE]
 at org.springframework.aop.framework.CglibAopProxy$CglibMethodInvocation.invokeJoinpoint(CglibAopProxy.java:769) ~[spring-aop-5.2.2.RELEASE.jar:5.2.2.RELEASE]
 at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:163) ~[spring-aop-5.2.2.RELEASE.jar:5.2.2.RELEASE]
 at org.springframework.aop.framework.CglibAopProxy$CglibMethodInvocation.proceed(CglibAopProxy.java:747) ~[spring-aop-5.2.2.RELEASE.jar:5.2.2.RELEASE]
 at org.springframework.aop.framework.adapter.MethodBeforeAdviceInterceptor.invoke(MethodBeforeAdviceInterceptor.java:56) ~[spring-aop-5.2.2.RELEASE.jar:5.2.2.RELEASE]
 at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:175) ~[spring-aop-5.2.2.RELEASE.jar:5.2.2.RELEASE]

多数据源事务-自定义注解+AOP实现可以看到我们数据库里面的数据是没有变更的,所以说事务就是生效,方案可行

总结

在多数据源事务的实现过程中,我们采用aop+自定义注解的方案去处理,而关于切面的相关逻辑思想就是获取到方法的事务管理器,并且将其丢入一个栈里面,保证一个先进后出的一个效果,捕获到异常之后,再依次出栈,进行rollback

写在最后

当然啦,登山的路有很多,这只是其中一条。如果以后我能找到更好的方案,也会再share一下。

我仍然是废物,请多指教!


原文始发于微信公众号(Hephaestuses):多数据源事务-自定义注解+AOP实现

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。

文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/45030.html

(0)
小半的头像小半

相关推荐

发表回复

登录后才能评论
极客之音——专业性很强的中文编程技术网站,欢迎收藏到浏览器,订阅我们!