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.setIdentifierGenerator(new CustomIdGenerator()); 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.setIdentifierGenerator(new CustomIdGenerator());
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());
}
}
}
需要用到的工具類:BeanUtil
@Component
public class BeanUtil implements ApplicationContextAware {
protected static ApplicationContext context;
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
context = applicationContext;
}
public static Object getBean(String name) {
return context.getBean(name);
}
public static <T> T getBean(Class<T> c){
return context.getBean(c);
}
}
MybatisPlusConfig配置類
@Configuration
@EnableTransactionManagement
public class MybatisPlusConfig {
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
PaginationInnerInterceptor paginationInnerInterceptor = new PaginationInnerInterceptor();
paginationInnerInterceptor.setDbType(DbType.MYSQL);
paginationInnerInterceptor.setOverflow(true);
interceptor.addInnerInterceptor(paginationInnerInterceptor);
return interceptor;
}
}
自定義主鍵生成策略
@Slf4j
@Component
public class CustomIdGenerator implements IdentifierGenerator {
@Override
public Long nextId(Object entity) {
return UIDGenerator.getUID();
}
}
@Slf4j
public class UIDGenerator {
/** 開始時間截 (2017-11-06) */
private final long twepoch = 1509976472321L;
private final long workerIdBits = 3L;
//最大為7
private final long maxWorkerId = -1L ^ (-1L << workerIdBits);
private final long timestampLeftShift = workerIdBits;
private long workerId;
/** 上次生成ID的時間截 */
private long lastTimestamp = -1L;
private static class UIDGeneratorHolder {
private static final UIDGenerator instance = new UIDGenerator();
}
private static UIDGenerator get(){
return UIDGeneratorHolder.instance;
}
public static long getUID() {
return getUID(null);
}
public static long getUID(Long workerId) {
UIDGenerator generator = get();
if(workerId == null){
workerId = 0l;
}else if (workerId.longValue() > generator.maxWorkerId || workerId.longValue() < 0) {
throw new IllegalArgumentException(String.format("workId不能大於%d或小於0", generator.maxWorkerId));
}
generator.workerId = workerId;
return generator.nextId();
}
/**
* 獲得下一個ID (該方法是線程安全的)
* @return SnowflakeId
*/
private synchronized long nextId() {
long timestamp = timeGen();
//如果當前時間小於上一次ID生成的時間戳,說明系統時鍾回退過這個時候應當拋出異常
if (timestamp < lastTimestamp) {
// throw new RuntimeException(
// String.format("時間被回退,生成的無效時間戳%d", lastTimestamp - timestamp));
log.error("時間被回退,生成的無效時間戳{}", lastTimestamp - timestamp);
}
//如果是同一時間生成的,則重新獲取
if (lastTimestamp == timestamp) {
//阻塞到下一個毫秒,獲得新的時間戳
timestamp = tilNextMillis(lastTimestamp);
}
//上次生成ID的時間截
lastTimestamp = timestamp;
return ((timestamp - twepoch) << timestampLeftShift) | workerId;
}
/**
* 阻塞到下一個毫秒,直到獲得新的時間戳
* @param lastTimestamp 上次生成ID的時間截
* @return 當前時間戳
*/
private long tilNextMillis(long lastTimestamp) {
long timestamp = timeGen();
while (timestamp <= lastTimestamp) {
timestamp = timeGen();
}
return timestamp;
}
/**
* 返回以毫秒為單位的當前時間
* @return 當前時間(毫秒)
*/
private long timeGen() {
return System.currentTimeMillis();
}
}
這樣我們就完成了整個代碼的編寫,下面就進行測試,測試的時候只需要在方法上使用自定義注解@CustomTransaction(value = {"masterTransactionManager","slaveTransactionManager"})
參考文檔: