Springboot+Mybatis-plus多数据源以及实现事务一致性
在实际项目开发中,会同时连接2个或者多个数据库进行开发,因此我们需要配置多数据源,在使用多数据源的时候,在业务中可能会对2个不同的数据库进行插入、修改等操作,如何保证多数据源的事务一致性问题?主要解决如下问题:
- 如何配置多数据源
- 如何保证事务一致性
1.多数据源配置
如果只是配置多数据可以使用mybatis-plus的注解@DS,@DS 可以注解在方法上或类上,同时存在就近原则 方法上注解 优先于 类上注解。
官方文档: https://baomidou.com/pages/a61e1b/#文档-documentation
2.事务一致性
现在有2个数据库,需要同时对2个数据库中的表都进行插入操作,此时如果使用注解@Transactional就不行了。
通过配置不同的Mapper接口扫描路径使用不同的SqlSessionTemplate来实现。不同的SqlSessionTemplate就是不同的SqlSessionFactory,也就是不同的DataSource。
2.1添加POM文件
<!-- MyBatis Plus-->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.4.2</version>
</dependency>
<!-- 多数据源-->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>dynamic-datasource-spring-boot-starter</artifactId>
<version>3.3.2</version>
</dependency>
2.2 配置2个不同的数据源
spring:
datasource:
dynamic:
primary: master
datasource:
master:
jdbc-url: jdbc:mysql://xxxx?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true&verifyServerCertificate=false&useSSL=false
username: root
password: root
slave:
jdbc-url: jdbc:mysql://xxx?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true&verifyServerCertificate=false&useSSL=false
username: root
password: 123
2.3 创建2个mapper包
2个mapper包分别对应存放2个数据源对应的mapper文件,这个里面没有什么特殊的,和之前怎么做现在还是怎么做
-
创建MasterDataSourceConfig配置文件
import com.baomidou.mybatisplus.annotation.IdType; import com.baomidou.mybatisplus.core.MybatisConfiguration; import com.baomidou.mybatisplus.core.config.GlobalConfig; import com.baomidou.mybatisplus.extension.spring.MybatisSqlSessionFactoryBean; import com.qz.soft.sampling.config.MybatisPlusConfig; import org.apache.ibatis.plugin.Interceptor; import org.apache.ibatis.session.SqlSessionFactory; import org.apache.ibatis.type.JdbcType; import org.mybatis.spring.SqlSessionFactoryBean; import org.mybatis.spring.SqlSessionTemplate; import org.mybatis.spring.annotation.MapperScan; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.boot.jdbc.DataSourceBuilder; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Primary; import org.springframework.core.io.support.PathMatchingResourcePatternResolver; import org.springframework.jdbc.datasource.DataSourceTransactionManager; import org.springframework.transaction.PlatformTransactionManager; import javax.annotation.Resource; import javax.sql.DataSource; /** * @author sean * @date 2021/12/23 */ @Configuration @MapperScan(basePackages = "com.sean.soft.sampling.mapper.master",sqlSessionFactoryRef = "masterSqlSessionFactory") public class MasterDataSourceConfig { @Resource private MybatisPlusConfig mybatisPlusConfig; @Primary @Bean("masterDataSource") @ConfigurationProperties(prefix = "spring.datasource.dynamic.datasource.master") public DataSource masterDataSource() { return DataSourceBuilder.create().build(); } @Primary @Bean("masterSqlSessionFactory") public SqlSessionFactory masterSqlSessionFactory(@Qualifier("masterDataSource") DataSource dataSource) throws Exception { //如果要使用mybatis-plus的功能的话需要使用MybatisSqlSessionFactoryBean,不要使用SqlSessionFactoryBean,否则使用mybatis-plus里面的方法会报错找不到该方法 MybatisSqlSessionFactoryBean bean = new MybatisSqlSessionFactoryBean(); bean.setDataSource(dataSource); MybatisConfiguration configuration = new MybatisConfiguration(); configuration.setJdbcTypeForNull(JdbcType.NULL); configuration.setMapUnderscoreToCamelCase(true); configuration.setCacheEnabled(false); bean.setConfiguration(configuration); //添加分页功能 Interceptor[] plugins = {mybatisPlusConfig.mybatisPlusInterceptor()}; bean.setPlugins(plugins); //设置全局配置 GlobalConfig globalConfig = new GlobalConfig(); globalConfig.setDbConfig(new GlobalConfig.DbConfig().setIdType(IdType.ASSIGN_ID)); globalConfig.setBanner(false); bean.setGlobalConfig(globalConfig); bean.setTypeAliasesPackage("com.qz.soft.sampling.entity"); bean.setMapperLocations(new PathMatchingResourcePatternResolver().getResources("classpath*:mapper/master/*.xml")); return bean.getObject(); } @Primary @Bean("masterSqlSessionTemplate") public SqlSessionTemplate masterSqlSessionTemplate(@Qualifier("masterSqlSessionFactory")SqlSessionFactory sqlSessionFactory) { return new SqlSessionTemplate(sqlSessionFactory); } @Primary @Bean("masterTransactionManager") public PlatformTransactionManager masterTransactionManager(@Qualifier("masterDataSource")DataSource dataSource) { return new DataSourceTransactionManager(dataSource); } }
-
创建SlaveDataSourceConfig配置文件
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.core.MybatisConfiguration;
import com.baomidou.mybatisplus.core.config.GlobalConfig;
import com.baomidou.mybatisplus.extension.spring.MybatisSqlSessionFactoryBean;
import com.qz.soft.sampling.config.MybatisPlusConfig;
import org.apache.ibatis.plugin.Interceptor;
import org.apache.ibatis.session.SqlSessionFactory;
import org.apache.ibatis.type.JdbcType;
import org.mybatis.spring.SqlSessionFactoryBean;
import org.mybatis.spring.SqlSessionTemplate;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.jdbc.DataSourceBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
import org.springframework.transaction.PlatformTransactionManager;
import javax.annotation.Resource;
import javax.sql.DataSource;
/**
* @author sean
* @date 2021/12/23
*/
@Configuration
@MapperScan(basePackages = "com.sean.soft.sampling.mapper.slave",sqlSessionFactoryRef = "slaveSqlSessionFactory")
public class SlaveDataSourceConfig {
@Resource
private MybatisPlusConfig mybatisPlusConfig;
@Bean("slaveDataSource")
@ConfigurationProperties(prefix = "spring.datasource.dynamic.datasource.slave")
public DataSource masterDataSource()
{
return DataSourceBuilder.create().build();
}
@Bean("slaveSqlSessionFactory")
public SqlSessionFactory slaveSqlSessionFactory(@Qualifier("slaveDataSource") DataSource dataSource) throws Exception {
MybatisSqlSessionFactoryBean bean = new MybatisSqlSessionFactoryBean();
bean.setDataSource(dataSource);
MybatisConfiguration configuration = new MybatisConfiguration();
configuration.setJdbcTypeForNull(JdbcType.NULL);
configuration.setMapUnderscoreToCamelCase(true);
configuration.setCacheEnabled(false);
bean.setConfiguration(configuration);
//添加分页功能
Interceptor[] plugins = {mybatisPlusConfig.mybatisPlusInterceptor()};
bean.setPlugins(plugins);
//全局配置
GlobalConfig globalConfig = new GlobalConfig();
globalConfig.setDbConfig(new GlobalConfig.DbConfig().setIdType(IdType.ASSIGN_ID));
globalConfig.setBanner(false);
bean.setGlobalConfig(globalConfig);
bean.setMapperLocations(new PathMatchingResourcePatternResolver().getResources("classpath*:mapper/slave/*.xml"));
return bean.getObject();
}
@Bean("slaveSqlSessionTemplate")
public SqlSessionTemplate slaveSqlSessionTemplate(@Qualifier("slaveSqlSessionFactory")SqlSessionFactory sqlSessionFactory)
{
return new SqlSessionTemplate(sqlSessionFactory);
}
@Bean("slaveTransactionManager")
public PlatformTransactionManager slaveTransactionManager(@Qualifier("slaveDataSource")DataSource dataSource)
{
return new DataSourceTransactionManager(dataSource);
}
}
2.4 创建自定义注解@CustomTransaction
/**
* @author sean
* @date 2021/12/23
*/
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.PARAMETER,ElementType.METHOD})
public @interface CustomTransaction {
String[] value() default {};
}
2.5 创建AOP切面,解析自定义注解
import cn.hutool.core.util.ArrayUtil;
import com.qz.soft.sampling.util.BeanUtil;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.context.annotation.Configuration;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
import org.springframework.transaction.TransactionStatus;
import org.springframework.transaction.support.DefaultTransactionDefinition;
import java.util.Stack;
/**
* @author sean
* @date 2021/12/23
*/
@Slf4j
@Aspect
@Configuration
public class TransactionAop {
@Pointcut("@annotation(com.qz.soft.sampling.annotation.CustomTransaction)")
public void CustomTransaction() {
}
@Around(value = "CustomTransaction() && @annotation(annotation)")
public Object syncLims(ProceedingJoinPoint joinPoint, CustomTransaction annotation) throws Throwable {
Stack<DataSourceTransactionManager> dataSourceTransactionManagerStack = new Stack<>();
Stack<TransactionStatus> transactionStatusStack = new Stack<>();
try {
if (!openTransaction(dataSourceTransactionManagerStack, transactionStatusStack, annotation)) {
return null;
}
Object ret = joinPoint.proceed();
commit(dataSourceTransactionManagerStack,transactionStatusStack);
return ret;
}catch (Throwable e)
{
rollback(dataSourceTransactionManagerStack,transactionStatusStack);
log.error(String.format("MultTransactionAspect, method:%s-%s occors error:",joinPoint.getTarget().getClass().getSimpleName(),
joinPoint.getSignature().getName()),e);
throw e;
}
}
/**
* 开启事务处理方法
*
* @param dataSourceTransactionManagerStack
* @param transactionStatusStack
* @param multiTransaction
* @return
*/
public Boolean openTransaction(Stack<DataSourceTransactionManager> dataSourceTransactionManagerStack,
Stack<TransactionStatus> transactionStatusStack, CustomTransaction multiTransaction) {
String[] transactionManagerNames = multiTransaction.value();
if (ArrayUtil.isEmpty(transactionManagerNames)) {
return false;
}
for (String beanName : transactionManagerNames) {
DataSourceTransactionManager dataSourceTransactionManager = (DataSourceTransactionManager) BeanUtil.getBean(beanName);
TransactionStatus transactionStatus = dataSourceTransactionManager.getTransaction(new DefaultTransactionDefinition());
transactionStatusStack.push(transactionStatus);
dataSourceTransactionManagerStack.push(dataSourceTransactionManager);
}
return true;
}
/**
* 提交处理方法
*
* @param dataSourceTransactionManagerStack
* @param transactionStatusStack
*/
private void commit(Stack<DataSourceTransactionManager> dataSourceTransactionManagerStack,
Stack<TransactionStatus> transactionStatusStack) {
while (!dataSourceTransactionManagerStack.isEmpty()) {
dataSourceTransactionManagerStack.pop().commit(transactionStatusStack.pop());
}
}
/**
* 回滚处理方法
* @param dataSourceTransactionManagerStack
* @param transactionStatusStack
*/
private void rollback(Stack<DataSourceTransactionManager> dataSourceTransactionManagerStack,
Stack<TransactionStatus> transactionStatusStack) {
while (!dataSourceTransactionManagerStack.isEmpty()) {
dataSourceTransactionManagerStack.pop().rollback(transactionStatusStack.pop());
}
}
}
这样我们就完成了整个代码的编写,下面就进行测试,测试的时候只需要在方法上使用自定义注解@CustomTransaction(value = {"masterTransactionManager","slaveTransactionManager"})
参考文档:
https://www.cnblogs.com/red-star/p/12535919.html
https://blog.csdn.net/qq_31142553/article/details/102768696