开启掘金成长之旅!这是我参与「掘金日新计划 · 2 月更文挑战」的第 13 天,点击查看活动详情
以前写过基于 AbstractRoutingDataSource 接口实现多数据源切换的。连接。这个没法保证事务。
简介
在看公司项目的时候,其中多数据源切换的实现,仔细看了下:
具体是实现 SqlSessionFactory 、DataSourceTransactionManager、SqlSessionTemplate Bean实例,而且还能保证跨数据源的事务。
配置
基于 Druid 管理数据源:master、slave 是自己命名的,加载事务管理器的时候会使用到。
spring:
datasource:
type: com.alibaba.druid.pool.DruidDataSource
driver-class-name: com.mysql.cj.jdbc.Driver
druid:
master:
url: jdbc:mysql://xxx:3306/ms_031_pm?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai&useSSL=false&zeroDateTimeBehavior=convertToNull
username: root
password: xxx
slave:
url: jdbc:mysql://xxx:3306/lottery?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai&useSSL=false&zeroDateTimeBehavior=convertToNull
username: root
password: xxx
注入两个数据源
其中另外的主数据源的 Bean name 是 masterDataSource。并且在下面的配置上会多一个 @Primary 注解,表示是默认的数据源。
public DataSource dataSource() {
return new DruidDataSource();
}
@Configuration
@MapperScan(basePackages = "com.practice.thinkindynamicsource.mapper.slave", sqlSessionTemplateRef = "slaveSqlSessionTemplate")
public class SlaveDataSourceConfiguration {
@Bean("slaveDataSource")
@ConfigurationProperties(prefix = "spring.datasource.druid.slave")
public DataSource dataSource() {
return new DruidDataSource();
}
@Bean(name = "slaveSqlSessionFactory")
@Primary
public SqlSessionFactory sqlSessionFactory(@Qualifier("slaveDataSource") DataSource dataSource) throws Exception {
SqlSessionFactoryBean bean = new SqlSessionFactoryBean();
bean.setDataSource(dataSource);
bean.setMapperLocations(new PathMatchingResourcePatternResolver().getResources("classpath:slave/*.xml"));
return bean.getObject();
}
@Bean(name = "slaveTransactionManager")
@Primary
public DataSourceTransactionManager transactionManager(@Qualifier("slaveDataSource") DataSource dataSource) {
return new DataSourceTransactionManager(dataSource);
}
@Bean(name = "slaveSqlSessionTemplate")
@Primary
public SqlSessionTemplate sqlSessionTemplate(@Qualifier("slaveSqlSessionFactory") SqlSessionFactory sqlSessionFactory) {
return new SqlSessionTemplate(sqlSessionFactory);
}
}
数据源切换
到这里,其实已经实现了数据源的切换。如下所示的代码,就会执行不同的数据源。
public Map testDataSource() {
Map slaveMap = slaveTest.slaveTest();
Map masterMap = masterTest.masterTest();
int i=1/0;
Map map = new HashMap();
map.put("masterMap", masterMap);
map.put("slaveMap", slaveMap);
return map;
}
但是和事务回滚没一毛钱的关系。我们可以加上如下的代码,但是只对配置的数据源有效。
默认的 Transactional 注解不支持多个数据库事务。
@Transactional(transactionManager = "masterTransactionManager")
因此我们可以实现一个支持多个数据源的事务的注解 。
多数据事务注解
注解
就是一个注解:一个数组属性
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
public @interface MultiDataSourceTransactional {
String[] transactionManagers();
}
切面实现
这里很巧妙的将 AOP 结合起来,手动声明事务管理器,然后关闭。
- 声明事务
- 执行业务代码
- 提交或者回退
@Component
@Aspect
public class MultiTransactionAop {
/**
* 1.使用 ThreadLocal 保证线程安全
* 2.Stack 先进先出,事务的声明与提交/回滚对应
*/
private static final ThreadLocal<Stack<Map<DataSourceTransactionManager, TransactionStatus>>> THREAD_LOCAL = new ThreadLocal<>();
/**
* Spring 上下文,用于获取事务管理器
*/
@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.practice.thinkindynamicsource.annotation.MultiDataSourceTransactional)")
public void pointcut() {
}
/**
* @Around 环绕
* @param joinPoint
* @param transactional
*/
@Around("@annotation(transactional)")
public void around(ProceedingJoinPoint joinPoint,MultiDataSourceTransactional transactional) {
String[] transactionManagerNames = transactional.transactionManagers();
Stack<Map<DataSourceTransactionManager, TransactionStatus>> pairStack = new Stack<>();
for (String transactionName : transactionManagerNames) {
DataSourceTransactionManager manager = applicationContext.getBean(transactionName, DataSourceTransactionManager.class);
TransactionStatus status = manager.getTransaction(def);
Map<DataSourceTransactionManager, TransactionStatus> transactionMap = new HashMap<>();
transactionMap.put(manager, status);
pairStack.push(transactionMap);
}
THREAD_LOCAL.set(pairStack);
try{
joinPoint.proceed();
while (!THREAD_LOCAL.get().isEmpty()) {
Map<DataSourceTransactionManager, TransactionStatus> pair = pairStack.pop();
pair.forEach((key, value) -> key.commit(value));
}
} catch (Throwable e) {
while (!THREAD_LOCAL.get().isEmpty()) {
Map<DataSourceTransactionManager, TransactionStatus> pair = pairStack.pop();
pair.forEach((key, value) -> key.rollback(value));
}
throw new RuntimeException(e);
} finally {
THREAD_LOCAL.remove();
}
}
}
这里的 Around 注解,可以拆分为 Before/AfterReturning/AfterThrowing 三个注解实现。
可以参考我的AOP实战文章。
注解使用
很简单,就是把定义的注解加在方法上即可
@MultiDataSourceTransactional(transactionManagers = {"masterTransactionManager", "slaveTransactionManager"})
public Map testDataSource1() {
Map slaveMap = slaveTest.slaveTest();
Map masterMap = masterTest.masterTest();
// int i=1/0;
Map map = new HashMap();
map.put("masterMap", masterMap);
map.put("slaveMap", slaveMap);
return map;
}
跨数据源事务总结
- 这里使用栈,先进先出的结构,声明事务和提交事务/回滚事务的顺序应该相反的
- 使用 ThreaLocal 可以保证线程安全,但是清除一定要放在 finally 保证一定执行,防止内存溢出
总结
多数据源事物的保证:其实就是手动管理事务,不交给 Spring 管理,只不过结合 AOP、注解 ,可以增加代码复用性。
现在有点感觉学习多种点、线的知识,怎么把它组装成一个面,也是质的一步!