提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档
文章目录
关于我踩到的坑…
前几天公司开了一个数据迁移的小项目交给我做,当时我想直接查出来插进去不就好,CRUD连UD都省了 哈哈哈…
结果做起来发现,事务这出了问题了…
@Transactional事务注解只能指定一个事务管理器,它的接收参数是String类型。那么我上网疯狂搜索资料,综合几篇文章整理出如下内容。
提示:以下是本篇文章正文内容,下面案例可供参考
一、实现方案
自定义一个我们自己的多数据源事务注解 @MultiDataSourceTransactional
基于两阶段提交和多事务管理器的事务切换来解决上面我遇到的问题。
1. 说一下两阶段提交
在执行被注解标注的方法前,将value()指定的所有事务管理器的事务开启,为每一个事务关闭自动提交。
各事务去执行对应的自己负责的SQL,这是第一段提交。
方法运行结束,全部提交,如果中途报错则全部事务进行回滚,这是第二段。
二、多说无益上操作
1.创建不同数据源的事务管理器
Mysql事务管理代码如下(示例):
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.support.TransactionTemplate;
import javax.sql.DataSource;
/**
* @Description:
* @author: laoge
* @since: 2022/12/16 10:40
*/
@Configuration
public class MultiDataSourceTransactionManager {
@Bean
@Primary
@ConfigurationProperties(prefix = SystemDefaultConstant.DB_1)
public DataSource dataSource1() {
return DataSourceBuilder.create().build();
}
@Bean
public PlatformTransactionManager db1TransactionManager(DataSource dataSource1) {
return new DataSourceTransactionManager(dataSource1);
}
@Bean("db1SqlSessionFactory")
@Primary
public SqlSessionFactory db1SqlSessionFactory(DataSource dataSource1) throws Exception {
SqlSessionFactoryBean bean = new SqlSessionFactoryBean();
bean.setDataSource(dataSource1);
return bean.getObject();
}
@Bean(SystemDefaultConstant.DB_2)
@ConfigurationProperties(prefix = SystemDefaultConstant.DB_2)
public DataSource dataSource2() {
return DataSourceBuilder.create().build();
}
@Bean
public PlatformTransactionManager db1TransactionManager(@Qualifier(SystemDefaultConstant.DB_2) DataSource dataSource2) {
return new DataSourceTransactionManager(dataSource2);
}
@Bean("db2SqlSessionFactory")
public SqlSessionFactory db2SqlSessionFactory(@Qualifier(SystemDefaultConstant.DB_2) DataSource dataSource2) throws Exception {
SqlSessionFactoryBean bean = new SqlSessionFactoryBean();
bean.setDataSource(dataSource2);
return bean.getObject();
}
}
2.自定义 @MultiDataSourceTransactional 注解
代码如下(示例):
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Inherited;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* 多数据源事务注解
*
*/
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
public @interface MultiDataSourceTransactional {
/**
* 事务管理器数组
*/
String[] transactionManagers();
}
2.事务管理实现
很多小伙伴会被面试官问到@Transactional的失效场景,其中心思想就是考查@Transactional是如何实现的,为何此注解会失效?
因为他是靠AOP切面去实现的,切面的前提是基于动态代理。那么 我们也靠AOP去做中心实现。写一个AOP切面类:MultiTransactionManage.class
对于mongodb的事务需要版本为4.2或以上并且非单例开启副本集才支持,由于项目之前未考虑这些不可以轻易更换版本,我采用了虚拟事务
(每次mongo插入向map存储一个数据key为唯一ID,value为DO实体),在切面中通过DO实体的表注解反射拿到哪一张表,再用ID进行删除。
MultiTransactionManage代码如下(示例):
import java.util.HashMap;
import java.util.Map;
import java.util.Optional;
import java.util.Stack;
import com.migration.api.annotations.MultiDataSourceTransactional;
import org.aspectj.lang.annotation.AfterReturning;
import org.aspectj.lang.annotation.AfterThrowing;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
import org.springframework.stereotype.Component;
import org.springframework.transaction.TransactionDefinition;
import org.springframework.transaction.TransactionStatus;
import org.springframework.transaction.support.AbstractPlatformTransactionManager;
import org.springframework.transaction.support.DefaultTransactionDefinition;
/**
* 多数据源事务切面
* ※采用Around似乎不行※
*/
@Component
@Aspect
public class MultiTransactionAop {
/**
* 线程本地变量:为什么使用栈?※为了达到后进先出的效果※
*/
private static final ThreadLocal<Stack<Map<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(demdm.migration.api.annotations.MultiDataSourceTransactional)")
public void pointcut() {
}
/**
* 声明事务
*
* @param transactional 注解
*/
@Before("pointcut() && @annotation(transactional)")
public void before(MultiDataSourceTransactional transactional) {
// 根据设置的事务名称按顺序声明,并放到ThreadLocal里
String[] transactionManagerNames = transactional.transactionManagers();
Stack<Map<DataSourceTransactionManager, TransactionStatus>> pairStack = this.getStack();
Map<String, Map.Entry<DataSourceTransactionManager, TransactionStatus>> ownTransactionMap = this.getTransactionMap();
for (String transactionManagerName : transactionManagerNames) {
// 如果当前线程请求中此数据源事务已经存在则,则不再新建
if (ownTransactionMap.containsKey(transactionManagerName)) {
continue;
}
DataSourceTransactionManager transactionManager = applicationContext.getBean(transactionManagerName, DataSourceTransactionManager.class);
TransactionStatus transactionStatus = transactionManager.getTransaction(def);
Map<DataSourceTransactionManager, TransactionStatus> transactionMap = new HashMap<>();
transactionMap.put(transactionManager, transactionStatus);
pairStack.push(transactionMap);
}
THREAD_LOCAL.set(pairStack);
}
/**
* 提交事务
*/
@AfterReturning("pointcut()")
public void afterReturning() {
// ※栈顶弹出(后进先出)
Stack<Map<DataSourceTransactionManager, TransactionStatus>> pairStack = THREAD_LOCAL.get();
while (!pairStack.empty()) {
Map<DataSourceTransactionManager, TransactionStatus> pair = pairStack.pop();
pair.forEach(AbstractPlatformTransactionManager::commit);
}
THREAD_LOCAL.remove();
}
/**
* 回滚事务
*/
@AfterThrowing(value = "pointcut()")
public void afterThrowing() {
// ※栈顶弹出(后进先出)
Stack<Map<DataSourceTransactionManager, TransactionStatus>> pairStack = THREAD_LOCAL.get();
while (!pairStack.empty()) {
Map<DataSourceTransactionManager, TransactionStatus> pair = pairStack.pop();
pair.forEach(AbstractPlatformTransactionManager::rollback);
}
THREAD_LOCAL.remove();
}
/**
* 获取当前线程的栈
*/
private Stack<Map<DataSourceTransactionManager, TransactionStatus>> getStack() {
return Optional.ofNullable(THREAD_LOCAL.get()).orElse(new Stack<>());
}
/**
* 获取存在的事务
*
* @return 事务管理器BeanNam为Key 栈内容为Value
*/
private Map<String, Map.Entry<DataSourceTransactionManager, TransactionStatus>> getTransactionMap() {
Stack<Map<DataSourceTransactionManager, TransactionStatus>> pairStack = this.getStack();
Map<String, Map.Entry<DataSourceTransactionManager, TransactionStatus>> result = new HashMap<>(pairStack.size());
for (Map<DataSourceTransactionManager, TransactionStatus> transactionStatusMap : pairStack) {
for (Map.Entry<DataSourceTransactionManager, TransactionStatus> entry : transactionStatusMap.entrySet()) {
result.put(entry.getKey().getClass().getSimpleName(), entry);
}
}
return result;
}
}
有小伙伴看完可能出现疑问了🤐
@Component默认为单例Spring容器初始化加载放在容器中管理,所有请求都是共用这一个了Bean那多个请求如何保证事务管理的数据不会串用?
答案:虽然共用了同一个Bean,但是不要忽略了ThreadLocal这员大将!
多个方法标注了@MultiDataSourceTransactional注解,那同一线程之间如何做到相互调用保证同数据源被统一管理?
答案:通过调用getTransactionMap()获取当前的事务,已经存在的则不新建。
ThreadLocal简单介绍
ThreadLocal实现线程范围内的共享变量,当多线程过沉重,容易出现共享数据被别的线程操作,导致脏数据发生,ThreadLocal本质上讲是提供了一个“线程级”变量作用域。
线程对应的有一个属于自己的 ThreadLocalMap (ThreadLocalMap对应thread.threadLocals字段)。
但是要特别注意!!!ThreadLocalMap是一个比较特殊的Map,每一个Entry的key都为弱引用,当没有对象再对这个变量进行使用时GC会自动回收这个变量。
但是!!!!它的Value依然是强引用,只要这个线程一直没有退出也就是不会被自动回收!那对于线程池的项目来说大部分线程会一直存在整个系统的生命周期。
ThreadLocalMap在进行set() 、 romove()的时候都做了自动删除Value。但是get没有自动删除,为了解决这个问题,在使用ThreadLocal的时候一定要手动 remove()防止内存泄漏!!!