多數據源下要保證事務,實際上就是分佈式事務,現在已經有阿里開源的seata來實現分佈式事務了,不用自己造輪子,如果想自己實現,下邊是一套方案.
我的項目是基於mybatis-plus實現的,在因爲mubatis-plus只是在mybatis上面做了封裝,這套方案用於myabtis也是沒問題的
如果只是重寫了AbstractRoutingDataSource方法,那麼在事務下數據源是切換不了的,還需要重寫事務方法
1. 先實現AbstractRoutingDataSource類
public class DynamicDataSource extends AbstractRoutingDataSource {
/**
* 取得當前使用哪個數據源
* @return
*/
@Override
protected Object determineCurrentLookupKey() {
return DbContextHolder.getDbType();
}
public DataSource getAcuallyDataSource() {
Object lookupKey = determineCurrentLookupKey();
if(null == lookupKey) {
return this;
}
DataSource determineTargetDataSource = this.determineTargetDataSource();
return determineTargetDataSource==null ? this : determineTargetDataSource;
}
}
2. DbContextHolder 用於獲取當前線程綁定的數據源
public class DbContextHolder {
private static final ThreadLocal contextHolder = new ThreadLocal<>();
/**
* 設置數據源
* @param dbTypeEnum
*/
public static void setDbType(DBTypeEnum dbTypeEnum) {
contextHolder.set(dbTypeEnum.getValue());
}
/**
* 取得當前數據源
* @return
*/
public static String getDbType() {
return (String) contextHolder.get();
}
/**
* 清除上下文數據
*/
public static void clearDbType() {
contextHolder.remove();
}
}
3. 數據源切換註解/切面/數據源枚舉類
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD})
public @interface DataSourceSwitch {
DBTypeEnum value() default DBTypeEnum.db1;
}
@Component
@Aspect
@Order(-100)
@Slf4j
public class DataSourceSwitchAspect {
@Pointcut("execution(* com.polycis.main.service.db1..*.*(..))")
private void db1Aspect() {
}
@Pointcut("execution(* com.polycis.main.service.db2..*.*(..))")
private void db2Aspect() {
}
@Pointcut("execution(* com.polycis.main.service.db3..*.*(..))")
private void db3Aspect() {
}
@Before( "db1Aspect()" )
public void db1(JoinPoint joinPoint) {
log.info("DataSourceSwitchAspect切換到db1 數據源...");
setDataSource(joinPoint,DBTypeEnum.db1);
}
@Before("db2Aspect()" )
public void db2 (JoinPoint joinPoint) {
log.info("DataSourceSwitchAspect切換到db2 數據源...");
setDataSource(joinPoint,DBTypeEnum.db2);
}
@Before("db3Aspect()" )
public void db3 (JoinPoint joinPoint) {
log.info("DataSourceSwitchAspect切換到db3 數據源...");
setDataSource(joinPoint,DBTypeEnum.db3);
}
@After( "db1Aspect()" )
public void db11(JoinPoint joinPoint) {
log.info("清除數據源db1");
clearDbType();
}
@After("db2Aspect()" )
public void db22 (JoinPoint joinPoint) {
log.info("清除數據源db2");
clearDbType();
}
@After("db3Aspect()" )
public void db33 (JoinPoint joinPoint) {
log.info("清除數據源db3");
clearDbType();
}
/**
* 添加註解方式,如果有註解優先註解,沒有則按傳過來的數據源配置
* @param joinPoint
* @param dbTypeEnum
*/
private void setDataSource(JoinPoint joinPoint, DBTypeEnum dbTypeEnum) {
MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
DataSourceSwitch dataSourceSwitch = methodSignature.getMethod().getAnnotation(DataSourceSwitch.class);
if (Objects.isNull(dataSourceSwitch) || Objects.isNull(dataSourceSwitch.value())) {
DbContextHolder.setDbType(dbTypeEnum);
}else{
log.info("根據註解來切換數據源,註解值爲:"+dataSourceSwitch.value());
switch (dataSourceSwitch.value().getValue()) {
case "db1":
DbContextHolder.setDbType(DBTypeEnum.db1);
break;
case "db2":
DbContextHolder.setDbType(DBTypeEnum.db2);
break;
case "db3":
DbContextHolder.setDbType(DBTypeEnum.db3);
break;
default:
DbContextHolder.setDbType(dbTypeEnum);
}
}
}
}
public enum DBTypeEnum {
db1("db1"), db2("db2"),db3("db3");;
private String value;
DBTypeEnum(String value) {
this.value = value;
}
public String getValue() {
return value;
}
}
4. mybatis配置類
在sqlSessionFactory裏,將動態數據源map設置進去.
然後關鍵的來了,
sqlSessionFactory.setTransactionFactory(new MultiDataSourceTransactionFactory());
這裏要設置MultiDataSourceTransactionFactory.
@Configuration
@MapperScan({"com.polycis.main.mapper*"})
public class MybatisPlusConfig {
/**
* mybatis-plus分頁插件<br>
* 文檔:http://mp.baomidou.com<br>
*/
@Bean
public PaginationInterceptor paginationInterceptor() {
PaginationInterceptor paginationInterceptor = new PaginationInterceptor();
//paginationInterceptor.setLocalPage(true);// 開啓 PageHelper 的支持
return paginationInterceptor;
}
/**
* mybatis-plus SQL執行效率插件【生產環境可以關閉】
*//*
@Bean
public PerformanceInterceptor performanceInterceptor() {
return new PerformanceInterceptor();
}
*/
@Bean(name = "db1")
@ConfigurationProperties(prefix = "spring.datasource.druid.db1" )
public DataSource db1 () {
return DruidDataSourceBuilder.create().build();
}
@Bean(name = "db2")
@ConfigurationProperties(prefix = "spring.datasource.druid.db2" )
public DataSource db2 () {
return DruidDataSourceBuilder.create().build();
}
@Bean(name = "db3")
@ConfigurationProperties(prefix = "spring.datasource.druid.db3" )
public DataSource db3 () {
return DruidDataSourceBuilder.create().build();
}
/**
* 動態數據源配置
* @return
*/
@Bean
@Primary
public DataSource multipleDataSource (@Qualifier("db1") DataSource db1,
@Qualifier("db2") DataSource db2,
@Qualifier("db3") DataSource db3) {
DynamicDataSource dynamicDataSource = new DynamicDataSource();
Map< Object, Object > targetDataSources = new HashMap<>();
targetDataSources.put(DBTypeEnum.db1.getValue(), db1 );
targetDataSources.put(DBTypeEnum.db2.getValue(), db2);
targetDataSources.put(DBTypeEnum.db3.getValue(), db3);
dynamicDataSource.setTargetDataSources(targetDataSources);
dynamicDataSource.setDefaultTargetDataSource(db1);
return dynamicDataSource;
}
@Bean("sqlSessionFactory")
public SqlSessionFactory sqlSessionFactory() throws Exception {
MybatisSqlSessionFactoryBean sqlSessionFactory = new MybatisSqlSessionFactoryBean();
sqlSessionFactory.setDataSource(multipleDataSource(db1(),db2(),db3()));
sqlSessionFactory.setTransactionFactory(new MultiDataSourceTransactionFactory());
//sqlSessionFactory.setMapperLocations(new PathMatchingResourcePatternResolver().getResources("classpath:/mapper/*/*Mapper.xml"));
MybatisConfiguration configuration = new MybatisConfiguration();
//configuration.setDefaultScriptingLanguage(MybatisXMLLanguageDriver.class);
configuration.setJdbcTypeForNull(JdbcType.NULL);
configuration.setMapUnderscoreToCamelCase(true);
configuration.setCacheEnabled(false);
sqlSessionFactory.setConfiguration(configuration);
sqlSessionFactory.setPlugins(new Interceptor[]{ //PerformanceInterceptor(),OptimisticLockerInterceptor()
paginationInterceptor()
});
sqlSessionFactory.setGlobalConfig(globalConfiguration());
return sqlSessionFactory.getObject();
}
@Bean
public GlobalConfiguration globalConfiguration() {
GlobalConfiguration conf = new GlobalConfiguration(new LogicSqlInjector());
conf.setLogicDeleteValue("-1");
conf.setLogicNotDeleteValue("1");
conf.setIdType(0);
conf.setMetaObjectHandler(new MyMetaObjectHandler());
conf.setDbColumnUnderline(true);
conf.setRefresh(true);
return conf;
}
}
5. 事務工廠類
public class MultiDataSourceTransactionFactory extends SpringManagedTransactionFactory {
@Override
public Transaction newTransaction(DataSource dataSource, TransactionIsolationLevel level, boolean autoCommit) {
return new MultiDataSourceTransaction(dataSource);
}
}
6. 多數據源事務接口,重寫了getConnection,commit等接口的邏輯
public class MultiDataSourceTransaction implements Transaction{
private static final Log LOGGER = LogFactory.getLog(MultiDataSourceTransaction.class);
private final DataSource dataSource;
private Connection mainConnection;
private String mainDatabaseIdentification;
private ConcurrentMap<String, Connection> otherConnectionMap;
private boolean isConnectionTransactional;
private boolean autoCommit;
public MultiDataSourceTransaction(DataSource dataSource) {
notNull(dataSource, "No DataSource specified");
this.dataSource = dataSource;
otherConnectionMap = new ConcurrentHashMap<>();
mainDatabaseIdentification= DbContextHolder.getDbType();
}
/**
* {@inheritDoc}
*/
@Override
public Connection getConnection() throws SQLException {
String databaseIdentification = DbContextHolder.getDbType();
if (databaseIdentification.equals(mainDatabaseIdentification)) {
if (mainConnection != null){
return mainConnection;
} else {
openMainConnection();
mainDatabaseIdentification =databaseIdentification;
return mainConnection;
}
} else {
if (!otherConnectionMap.containsKey(databaseIdentification)) {
try {
Connection conn = dataSource.getConnection();
otherConnectionMap.put(databaseIdentification, conn);
} catch (SQLException ex) {
throw new CannotGetJdbcConnectionException("Could not get JDBC Connection", ex);
}
}
return otherConnectionMap.get(databaseIdentification);
}
}
private void openMainConnection() throws SQLException {
this.mainConnection = DataSourceUtils.getConnection(this.dataSource);
this.autoCommit = this.mainConnection.getAutoCommit();
this.isConnectionTransactional = DataSourceUtils.isConnectionTransactional(this.mainConnection, this.dataSource);
if (LOGGER.isDebugEnabled()) {
LOGGER.debug(
"JDBC Connection ["
+ this.mainConnection
+ "] will"
+ (this.isConnectionTransactional ? " " : " not ")
+ "be managed by Spring");
}
}
/**
* {@inheritDoc}
*/
@Override
public void commit() throws SQLException {
if (this.mainConnection != null && !this.isConnectionTransactional && !this.autoCommit) {
if (LOGGER.isDebugEnabled()) {
LOGGER.debug("Committing JDBC Connection [" + this.mainConnection + "]");
}
this.mainConnection.commit();
for (Connection connection : otherConnectionMap.values()) {
connection.commit();
}
}
}
/**
* {@inheritDoc}
*/
@Override
public void rollback() throws SQLException {
if (this.mainConnection != null && !this.isConnectionTransactional && !this.autoCommit) {
if (LOGGER.isDebugEnabled()) {
LOGGER.debug("Rolling back JDBC Connection [" + this.mainConnection + "]");
}
this.mainConnection.rollback();
for (Connection connection : otherConnectionMap.values()) {
connection.rollback();
}
}
}
/**
* {@inheritDoc}
*/
@Override
public void close() throws SQLException {
DataSourceUtils.releaseConnection(this.mainConnection, this.dataSource);
for (Connection connection : otherConnectionMap.values()) {
DataSourceUtils.releaseConnection(connection, this.dataSource);
}
}
@Override
public Integer getTimeout() throws SQLException {
return null;
}
}