在讲多数据源事务之前,我们先来回顾一下单数据源的情况!
单数据源事务
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>
-
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>
-
TeacherMapper.xml
<insert id="insert">
insert into student(name, age, create_time)
values (#{name}, #{age}, #{createTime})
</insert>
-
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. 动手实验
-
分别单独执行一下addStudent()和addTeacher()
-
再执行一下addAll()
由于addTeacher()和addStudent()中间添加了一个 int a = 1/0 报了异常
java.lang.ArithmeticException: / by zero
at com.tyrone.multidatasource.service.MultiDatasourceService.addAll(MultiDatasourceService.java:47) ~[classes/:na]
我们再来看看数据库的数据情况数据库中teacher表的数据还是那么多,说到事务生效了!接下来看看多数据源的情况!
多数据源情况
在企业级开发中是存在一个项目里面维护了多个数据源的情况,有些情况还会存在一个方法里面调用不同数据源,对于一些情景就可能需要的控制事务。比如说迁库迁表的情况的下,我们可能采用双写的方案。通常情况下我们要求多个数据源必须都写入了,才能正常结束方法,一旦出现异常,多个数据源都需要回滚
-
修改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 只支持单个的事务管理器
我们可以采用以下这两种方案去处理:
-
使用编程式事务
try {
tc1.commit();
tc2.commit();
method();
}catch (Exception e){
tc2.rollback();
tc1.rollback();
}
使用编程式事务,对于业务代码倾入性太强,比较繁琐的同时还需要注意事务回滚的顺序,不太推荐。
-
自己实现多数据源事务注解
既然 @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)
先来看一下调用方法之前的一个数据库情况:然后我们调用接口,不出意外的出现了异常,然后看一下数据库里面的数据是否发生变化,来验证我们的方案是否有效
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+自定义注解的方案去处理,而关于切面的相关逻辑思想就是获取到方法的事务管理器,并且将其丢入一个栈里面,保证一个先进后出的一个效果,捕获到异常之后,再依次出栈,进行rollback
写在最后
当然啦,登山的路有很多,这只是其中一条。如果以后我能找到更好的方案,也会再share一下。
我仍然是废物,请多指教!
原文始发于微信公众号(Hephaestuses):多数据源事务-自定义注解+AOP实现
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。
文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/45030.html