一、Springboot+mybatis+多数据源配置
第一步:添加依赖包。这里就不做说明了
第二步:添加数据库连接配置,在application.properties中添加数据源配置信息
### mysql config ###
env.spring.datasource.jdbc-url=jdbc:mysql://xxx?autoReconnect=true&useSSL=false&useUnicode=true&characterEncoding=utf8&allowMultiQueries=true
env.spring.datasource.master.username=root
env.spring.datasource.master.password=root
env.spring.datasource.master.driver-class-name: com.mysql.jdbc.Driver
# 系统数据库配置
owner.env.spring.datasource.jdbc-url=jdbc:mysql://yyy?autoReconnect=true&useSSL=false&useUnicode=true&characterEncoding=utf8&allowMultiQueries=true
env.spring.datasource.owner.username=root
env.spring.datasource.owner.password=root
env.spring.datasource.owner.driver-class-name: com.mysql.jdbc.Driver
第三步:Java Bean的方式注册两个数据源。注意选择主数据源要加上@Primary,这样基于Type方式的注入(如@Autowired)就可以使用它作为默认的注入对象了
主数据源bean配置
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import javax.sql.DataSource;
import org.apache.ibatis.plugin.Interceptor;
import org.apache.ibatis.session.SqlSessionFactory;
import org.mybatis.spring.SqlSessionFactoryBean;
import org.mybatis.spring.SqlSessionTemplate;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.core.io.Resource;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
import org.springframework.core.io.support.ResourcePatternResolver;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
import com.alibaba.druid.pool.DruidDataSource;
import jp.co.warmlight.system.interceptor.CommonDbPageInterceptor;
import jp.co.warmlight.system.interceptor.CommonDbUpdateInterceptor;
/**
*
* @author 听琴
*
*/
@Configuration
@MapperScan(basePackages = DataSourceConfig.PACKAGE, sqlSessionFactoryRef = "masterSqlSessionFactory")
public class DataSourceConfig {
/**
* 设置扫描包的路径
*/
public static final String PACKAGE = "com.algmis.*.dao";
@Value("${env.spring.datasource.master.url}")
private String url;
@Value("${env.spring.datasource.master.username}")
private String user;
@Value("${env.spring.datasource.master.password}")
private String password;
@Autowired
CommonDbPageInterceptor inteceptor;
@Autowired
CommonDbUpdateInterceptor updateInteceptor;
/**
*
* 将这个对象放入Spring容器中
*
* @Primary 表示这个数据源是默认数据源
* 读取application.properties中的配置参数映射成为一个对象
* prefix表示参数的前缀
* @return DataSource
*/
@Bean(name = "masterDataSource")
@Primary
public DataSource getDateSource1() {
DruidDataSource dataSource = new DruidDataSource();
dataSource.setUrl(url);
dataSource.setUsername(user);
dataSource.setPassword(password);
return dataSource;
}
/**
*
* @Qualifier表示查找Spring容器中名字为masterDataSource的对象
* @Primary表示这个数据源是默认数据源
*
* @param datasource datasource
* @return SqlSessionFactory
* @throws Exception ex
*/
@Bean(name = "masterSqlSessionFactory")
@Primary
public SqlSessionFactory masterSqlSessionFactory(@Qualifier("masterDataSource") DataSource datasource) throws Exception {
SqlSessionFactoryBean bean = new SqlSessionFactoryBean();
bean.setDataSource(datasource);
bean.setMapperLocations(resolveMapperLocations());
bean.setPlugins(new Interceptor[]{inteceptor,updateInteceptor});
return bean.getObject();
}
/**
*
* @return Resource[]
*/
public Resource[] resolveMapperLocations() {
ResourcePatternResolver resourceResolver = new PathMatchingResourcePatternResolver();
List<String> mapperLocations = new ArrayList<>();
// mybatis自动生成的xml文件
mapperLocations.add("classpath*:/mybatis/mapper/*Dao.xml");
// 用户编写的xml文件
mapperLocations.add("classpath*:/com/algmis/*/dao/**.xml");
List<Resource> resources = new ArrayList<Resource>();
if (mapperLocations != null) {
for (String mapperLocation : mapperLocations) {
try {
Resource[] mappers = resourceResolver.getResources(mapperLocation);
resources.addAll(Arrays.asList(mappers));
} catch (IOException e) {
// ignore
}
}
}
return resources.toArray(new Resource[resources.size()]);
}
/**
* 配置事务管理
* @param dataSource dataSource
* @return DataSourceTransactionManager
*/
@Bean(name = "masterTransactionManager")
public DataSourceTransactionManager testTransactionManager(@Qualifier("masterDataSource") DataSource dataSource) {
return new DataSourceTransactionManager(dataSource);
}
/**
*
* @param sessionfactory sessionfactory
* @return SqlSessionTemplate
*/
@Bean("masterSqlSessionTemplate")
@Primary
public SqlSessionTemplate masterSqlSessionTemplate(@Qualifier("masterSqlSessionFactory") SqlSessionFactory sessionfactory) {
return new SqlSessionTemplate(sessionfactory);
}
}
从数据源和主数据源配置基本一致,但是bean的名字不能一样的,导致服务启动不了,这里就省略了。
第四步:使用事务时指定对应的事务管理器
根据使用不同的数据源,选择不同的数据事务管理器
@Transactional(transactionManager = "masterTransactionManager")
public void saveUserInfo(User user) {
}
因为@Transactional只能指定一个事务管理器,并且注解不允许重复,所以就只能使用一个数据源的事务管理器了。那么对于一个方法涉及到多个数据源操作需要保证事务一致性的怎么办呢?请继续往下看。
二、实现跨数据源事务
先说一下两阶段提交:首先多个数据源的事务分别都开起来,然后各事务分别去执行对应的sql(此所谓第一阶段提交),最后如果都成功就把事务全部提交,只要有一个失败就把事务都回滚——此所谓第二阶段提交。
Transactional注解只能指定一个数据源的事务管理器。我们重新定义一个,让它支持指定多个数据源的事务管理器,然后我们在使用了这个注解的方法前后进行所谓的两阶段协议。
/**
* 多数据源事务注解
*
*/
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
public @interface MultiDataSourceTransactional {
/**
* 事务管理器数组
*/
String[] transactionManagers();
}
说到方法前后,相信很多人想到了AOP。那么就用Spring的Aspect来完成我们的想法吧。
先来回顾一下它的切入点
@Before: 标识一个前置增强方法,相当于BeforeAdvice的功能。
@After: 后置增强,不管是抛出异常或者正常退出都会执行。
@AfterReturning: 后置增强,似于AfterReturningAdvice, 方法正常退出时执行。
@AfterThrowing: 异常抛出增强,相当于ThrowsAdvice。
@Around: 环绕增强,相当于MethodInterceptor。
咋一看,@Around是可以的:ProceedingJoinPoint的proceed方法是执行目标方法,在它前面声明事务,try...catch...一下如果有异常就回滚没异常就提交。不过,最开始用这个的时候,好像发现有点问题,具体记不住了,大家可以试一下。因为当时工期紧没仔细研究,就采用了下面这种
@Before + @AfterReturning + @AfterThrowing组合,看名字和功能简直是完美契合啊!但是有一个问题,不同方法怎么共享那个事务呢?成员变量?对,没错。但是又有线程安全问题咋办?ThreadLocal帮你解决(*^_^*)。
直接上代码
import java.util.HashMap;
import java.util.Map;
import java.util.Stack;
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.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(MultiTransactional注解路径)")
public void pointcut() {
}
/**
* 声明事务
*
* @param transactional 注解
*/
@Before("pointcut() && @annotation(transactional)")
public void before(MultiDataSourceTransactional transactional) {
// 根据设置的事务名称按顺序声明,并放到ThreadLocal里
String[] transactionManagerNames = transactional.transactionManagers();
Stack<Map<DataSourceTransactionManager, TransactionStatus>> pairStack = new Stack<>();
for (String transactionManagerName : transactionManagerNames) {
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((key,value)->key.commit(value));
}
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((key,value)->key.rollback(value));
}
THREAD_LOCAL.remove();
}
}
需要注意的点:
1)声明事务和提交事务或者回滚事务的顺序应该相反的,就是先进后出,所以采用了栈来存储。
2)线程执行结束后记得清空本地变量。
3)可以参照@Transactional的那些属性升级功能,比如隔离级别回滚异常等。
在事务的方法上添加前面我们的多数据源事务注解
/**
* 测试多数据源事务
*/
@MultiDataSourceTransactional(transactionManagers={"masterTransactionManager","ownerTransactionManager"})
public void tetTransaction() {
}