Java项目中多数据源动态切换的实现
前言
从事餐饮软件项目,作为阿里本地生活的一份子,从19年下半年开始阿里口碑一批P6 P7开发人员陆续加入开发,此处的话题是一个餐饮pos机项目上的问题。每个餐饮门店都有一台或多台pos机,主要架构上是C++语言作为前端展示,Java开发的jar包程序作为其后端进行业务处理,pos可以有多台而Java后台只有一个,考虑到轻量,选用了sqlite数据库
sqlite每个数据库都是以单个文件的形式存在,每个文件都是一个数据库,文件不宜过大,我们根据业务含义将数据库划分成了4个业务文件,请注意这里不是分库分表,每个文件中表结构都不同,那项目在运行过程中读取不同数据库就需要做到无缝切换,不仅如此,这些数据库文件是有版本的,是可以不段生成的,当新版本的数据库文件生成时我们就需要连接新版本的数据库文件,即动态增加数据源的连接
前提
springBoot框架环境
项目里的多数据源含义
一个项目里,可以同时访问多个不同的数据库
实现
数据源切换的一般实现(推荐)
- 可以直接用原生的jdbc DriverManager. getConnection直接连接,但这种方式没有连接池且对于我们来说需要使用mybatis框架
- 将存放多个数据源的map(key是自定义的数据源id,value是java DataSource的实体)给到spring提供的AbstractRoutingDataSource并指定默认数据源(可以在determineCurrentLookupKey获取不到数据源时使用默认的数据源)
- 继承spring的AbstractRoutingDataSource接口并重写determineCurrentLookupKey方法,通过threadLocal获取当前线程的数据源id
- 项目启动时SqlSessionFactory的dataSource指定为重写的AbstractRoutingDataSource业务代码中手动指定当前线程threadLocal的数据源id
两个问题
- 不同数据源的事务不能同时回滚
- 无法动态的新增数据源
解决方案
如何自动切换
- 每个数据源都配置一套SqlSessionFactory,每个SqlSessionFactory都指定自己的mapper
- 不用手动指定数据源,但是SqlSessionFactory有多个
每个数据源的配置可以参考下面示例
@Configuration
@PropertySource("classpath:/dataSourceConfig.properties")
@MapperScan(basePackages = "com.alsc.basic.offline.sdk.dao.mapper.record",
sqlSessionTemplateRef = "recordSqlSessionTemplate")
@Slf4j
public class MyBatisConfigRecord {
@Autowired
DataSourcePathService dataSourcePathService;
/**
* 加载数据源配置
*/
@Bean(name = "recordDataSourceProperties")
@ConfigurationProperties("spring.datasource.record")
public DataSourceProperties recordDataSourceProperties() {
return new DataSourceProperties();
}
/**
* 加载数据源
*/
@Bean(name = "recordDataSource")
HikariDataSource recordDataSource(
@Qualifier("recordDataSourceProperties") DataSourceProperties dataSourceProperties) {
String dataSourceAbsolutePath = ""; //这里你要自己定义路径
String url = "jdbc:sqlite:" + dataSourceAbsolutePath;
dataSourceProperties.setUrl(url);
return dataSourceProperties.initializeDataSourceBuilder()
.type(HikariDataSource.class)
.build();
}
@Bean(name = "recordSessionFactory")
public SqlSessionFactory recordSessionFactory(@Qualifier("recordDataSource") DataSource dataSource)
throws Exception {
return getSqlSessionFactory(dataSource, new String[] {"classpath*:mapper/record/*.xml"});
}
@Bean(name = "recordSqlSessionTemplate")
public SqlSessionTemplate recordSqlSessionTemplate(
@Qualifier("recordSessionFactory") SqlSessionFactory recordSessionFactory)
throws Exception {
return new SqlSessionTemplate(recordSessionFactory);
}
private SqlSessionFactory getSqlSessionFactory(DataSource dataSource, String[] mapperLocations) throws Exception {
MybatisProperties properties = new MybatisProperties();
properties.setMapperLocations(mapperLocations);
SqlSessionFactoryBean bean = new SqlSessionFactoryBean();
bean.setDataSource(dataSource);
bean.setMapperLocations(properties.resolveMapperLocations());
org.apache.ibatis.session.Configuration configuration = new org.apache.ibatis.session.Configuration();
configuration.setMapUnderscoreToCamelCase(true);
bean.setConfiguration(configuration);
return bean.getObject();
}
}
如何动态替换
问题:当前数据库存在版本问题,数据库名称相同但存放在不同的版本目录下,需将当前版本的数据源切换url指向另一个版本
解决:重新生成一个DataSource对象和Environment对象指定给SqlSessionFactory的Configuration
示例代码如下
private void changeDataSource(String version, SqlSessionFactory sessionFactory, DataSourceIdEnum dataSourceIdEnum,
DataSourceProperties properties) {
String dataSourceAbsolutePath = dataSourcePathService.getDataSourceAbsolutePath(dataSourceIdEnum);
String url = "jdbc:sqlite:" + dataSourceAbsolutePath;
properties.setUrl(url);
HikariDataSource hikariDataSource =
properties.initializeDataSourceBuilder().type(HikariDataSource.class).build();
DataSource oldDataSource = sessionFactory.getConfiguration().getEnvironment().getDataSource();
/**
* 此处environment属性id,目前了解的主要作用在mybatis的一级缓存
* 生成CacheKey会用到,即使所有的id都设置相同,生成的CacheKey可能相同
* 由于不同数据源走不同sqlSession,不会有影响
*/
Environment environment =
new Environment(dataSourceIdEnum.getCode() + version, new JdbcTransactionFactory(), hikariDataSource);
sessionFactory.getConfiguration().setEnvironment(environment);
closeDataSource(oldDataSource);
}
SqlSessionFactory的Configuration就是mybatis的配置信息,我们常用的配置如:下划线驼峰命名转换,新增记录返回自动生成的主键等都在这里
此处没有使用事务
事务同步回滚
基本思路
我们可以获取事务方法中所有需要回滚的DataSourceTransactionManager,依次手动回滚
Mybatis手动事务回滚的一般代码实现
DefaultTransactionDefinition def = new DefaultTransactionDefinition();
def.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRED);
TransactionStatus transactionStatus = transactionManager.getTransaction(def);
transactionManager.rollback(transactionStatus);
问题
真正在实现这个demo的时候发现,在第二个数据源的事务回滚时会报错:Transaction synchronization is not active() (事务同步没有被激活)
分析
- 事务同步的使用一般是用TransactionSynchronizationManager.registerSynchronization向事务管理器中注册一个实现了TransactionSynchronization接口的类,该接口里提供了围绕事务执行的前置和后置增强方法
- Spring中一个线程中的多个事务应该是按照嵌套事务来设计的
- 上面的transactionManager.getTransaction执行时,Spring源码中会执行一个startTransaction方法,里面就开始一个事务,会判断ThreadLocal<Set> synchronizations是否为空,不为空就会生成一个挂起资源,第一个事务的挂起资源为空
- 在startTransaction中会给synchronizations赋值,即激活事务同步,也就是每个事务开始后都会初始化synchronizations,但下一个事务会读取当前线程的synchronizations并挂起,当做嵌套的事务
因此当我们执行完第一个事务的回滚后,由于第一个事务没有事务同步的挂起资源,代码会将该事务当做内嵌事务的最外层来处理,会清空synchronizations即关闭事务同步,再执行第二个事务的回滚,由于当前线程的synchronizations已被清空,所以会抛出事务同步没有激活的异常
解决
- 获取jdbc的connection回滚
@Aspect
@Component
@Slf4j
public class MultiDataSourceTransactionAspectConnection {
/**
* 多数据源组合事务
* */
@Pointcut("@annotation(com.alsc.basic.offline.sdk.dao.annotation.TransactionalGroup)")
public void transactionalGroupAspect() {};
@Around(value = "transactionalGroupAspect()")
public Object transactionalGroupAspectAround(ProceedingJoinPoint pjp) throws Throwable {
log.info("[多数据源组合事务切面]: 进入多数据源组合事务管理切面");
Object result = null;
List<Connection> transactionConnectionList = new ArrayList<>();
MethodSignature methodSignature = (MethodSignature) pjp.getSignature();
Method targetMethod = methodSignature.getMethod();
TransactionalGroup transactionalGroup = targetMethod.getAnnotation(TransactionalGroup.class);
AssertUtil.isNotNull(transactionalGroup,"[多数据源组合事务切面]: TransactionalGroup注解不能为空");
DataSourceIdEnum[] dataSourceIdEnums = transactionalGroup.value();
for(DataSourceIdEnum dataSourceIdEnum : dataSourceIdEnums){
DataSourceTransactionManager transactionManager = (DataSourceTransactionManager) SpringContextUtil.getBean(TransactionManagerEnum
.getByDataSourceId(dataSourceIdEnum).getTransactionManagerBeanName());
AssertUtil.isNotNull(transactionManager, CommonErrorCode.NULL_PARAMETER,
"[多数据源组合事务切面]: 从spring获取transactionManager为空");
DefaultTransactionDefinition def = new DefaultTransactionDefinition();
def.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRED);
Connection connection = transactionManager.getDataSource().getConnection();
connection.setAutoCommit(false);
transactionConnectionList.add(connection);
}
try {
result = pjp.proceed();
}catch (Throwable e){
if(!CollectionUtils.isEmpty(transactionConnectionList)){
Iterator<Connection> iterator = transactionConnectionList.iterator();
while (iterator.hasNext()){
Connection connection = iterator.next();
connection.rollback();
}
}
}
log.info("[多数据源组合事务切面]: 多数据源组合事务管理切面结束");
return result;
}
}
- 由事务开始的顺序倒叙执行事务回滚
Spring事务源码解读:每个事务开始的时候都会初始化事务同步,下一个新事务会生成一个挂起资源类,前一个事务的同步(TransactionSynchronization)就被存到资源中并清空threadLocal,当然后面又会重新初始化threadLocal,在每个事务回滚或提交结束后会判断当前事务有无挂起资源,若有就重新恢复挂起的事务同步,这样下一个事务执行回滚或提交就会执行这些事务同步,就不会出现事务同步被清空的情况
这种方式其实就是按照嵌套事务回滚的思路解决
@Aspect
@Component
@Slf4j
public class MultiDataSourceTransactionAspectDemo1 {
/**
* 多数据源组合事务
* */
@Pointcut("@annotation(com.alsc.basic.offline.sdk.dao.annotation.TransactionalGroup)")
public void transactionalGroupAspect() {};
@Around(value = "transactionalGroupAspect()")
public Object transactionalGroupAspectAround(ProceedingJoinPoint pjp) throws Throwable {
log.info("[多数据源组合事务切面]: 进入多数据源组合事务管理切面");
Object result = null;
List<TransactionManageDTO> transactionManagerDTOList = new ArrayList<>();
MethodSignature methodSignature = (MethodSignature) pjp.getSignature();
Method targetMethod = methodSignature.getMethod();
TransactionalGroup transactionalGroup = targetMethod.getAnnotation(TransactionalGroup.class);
DataSourceIdEnum[] dataSourceIdEnums = transactionalGroup.value();
for(DataSourceIdEnum dataSourceIdEnum : dataSourceIdEnums){
DataSourceTransactionManager transactionManager =
(DataSourceTransactionManager) SpringContextUtil.getBean(TransactionManagerEnum
.getByDataSourceId(dataSourceIdEnum).getTransactionManagerBeanName());
DefaultTransactionDefinition def = new DefaultTransactionDefinition();
def.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRED);
TransactionStatus transactionStatus = transactionManager.getTransaction(def);
addtransactionManagerList(transactionManagerDTOList, dataSourceIdEnum, transactionManager, transactionStatus);
}
try {
result = pjp.proceed();
}catch (Throwable e){
Collections.reverse(transactionManagerDTOList);
for(TransactionManageDTO transactionManageDTO : transactionManagerDTOList){
DataSourceTransactionManager dataSourceTransactionManager = transactionManageDTO.getDataSourceTransactionManager();
TransactionStatus transactionStatus = transactionManageDTO.getTransactionStatus();
dataSourceTransactionManager.rollback(transactionStatus);
}
}
log.info("[多数据源组合事务切面]: 多数据源组合事务管理切面结束");
return result;
}
private void addtransactionManagerList(List<TransactionManageDTO> transactionManagerDTOList, DataSourceIdEnum dataSourceIdEnum, DataSourceTransactionManager transactionManager, TransactionStatus transactionStatus) {
TransactionManageDTO transactionManageDTO = new TransactionManageDTO();
transactionManageDTO.setDataSourceId(dataSourceIdEnum);
transactionManageDTO.setDataSourceTransactionManager(transactionManager);
transactionManageDTO.setTransactionStatus(transactionStatus);
transactionManagerDTOList.add(transactionManageDTO);
}
}
- 多个事务执行 transactionManager.getTransaction后(即开始事务后)都执行一遍TransactionSynchronizationManager.clear() 这样下一个事务再开始时就不会有挂起的资源,都相当于最外层事务,我们理解为完全独立的事务,回滚可以按原顺序执行但每个事务回滚后都要再执行一遍TransactionSynchronizationManager.initSynchronization(); 重新初始化一遍,但是这样就不能使用自定义的事务同步
这种方式虽然不能使用事务同步,是一种退而求其次的方式,但若没有对事务同步有强要求的也可以采取
@Aspect
@Component
@Slf4j
public class MultiDataSourceTransactionAspectDemo2 {
/**
* 多数据源组合事务
* */
@Pointcut("@annotation(com.alsc.basic.offline.sdk.dao.annotation.TransactionalGroup)")
public void transactionalGroupAspect() {};
@Around(value = "transactionalGroupAspect()")
public Object transactionalGroupAspectAround(ProceedingJoinPoint pjp) throws Throwable {
log.info("[多数据源组合事务切面]: 进入多数据源组合事务管理切面");
Object result = null;
List<TransactionManageDTO> transactionManagerDTOList = new ArrayList<>();
MethodSignature methodSignature = (MethodSignature) pjp.getSignature();
Method targetMethod = methodSignature.getMethod();
TransactionalGroup transactionalGroup = targetMethod.getAnnotation(TransactionalGroup.class);
DataSourceIdEnum[] dataSourceIdEnums = transactionalGroup.value();
for(DataSourceIdEnum dataSourceIdEnum : dataSourceIdEnums){
DataSourceTransactionManager transactionManager =
(DataSourceTransactionManager) SpringContextUtil.getBean(TransactionManagerEnum
.getByDataSourceId(dataSourceIdEnum).getTransactionManagerBeanName());
DefaultTransactionDefinition def = new DefaultTransactionDefinition();
def.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRED);
TransactionStatus transactionStatus = transactionManager.getTransaction(def);
addtransactionManagerList(transactionManagerDTOList, dataSourceIdEnum, transactionManager, transactionStatus);
TransactionSynchronizationManager.clear();
}
try {
result = pjp.proceed();
}catch (Throwable e){
for(TransactionManageDTO transactionManageDTO : transactionManagerDTOList){
TransactionSynchronizationManager.initSynchronization();
DataSourceTransactionManager dataSourceTransactionManager = transactionManageDTO.getDataSourceTransactionManager();
TransactionStatus transactionStatus = transactionManageDTO.getTransactionStatus();
dataSourceTransactionManager.rollback(transactionStatus);
}
}
log.info("[多数据源组合事务切面]: 多数据源组合事务管理切面结束");
return result;
}
private void addtransactionManagerList(List<TransactionManageDTO> transactionManagerDTOList, DataSourceIdEnum dataSourceIdEnum, DataSourceTransactionManager transactionManager, TransactionStatus transactionStatus) {
TransactionManageDTO transactionManageDTO = new TransactionManageDTO();
transactionManageDTO.setDataSourceId(dataSourceIdEnum);
transactionManageDTO.setDataSourceTransactionManager(transactionManager);
transactionManageDTO.setTransactionStatus(transactionStatus);
transactionManagerDTOList.add(transactionManageDTO);
}
}
demo用到的辅助类和注解
@Data
@ToString
public class TransactionManageDTO {
private DataSourceIdEnum dataSourceId;
private DataSourceTransactionManager dataSourceTransactionManager;
private TransactionStatus transactionStatus;
}
@Target({ ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface TransactionalGroup {
/**
*
* 配置的数据源id
* */
DataSourceIdEnum[] value();
/******* 以下参数,参见 @Transactional ********/
//事务传播行为
Propagation propagation() default Propagation.REQUIRED;
//事务隔离级别
Isolation isolation() default Isolation.DEFAULT;
//哪些异常回滚
Class<? extends Throwable>[] rollbackFor() default {};
// 哪些异常不回滚事务
Class<? extends Throwable>[] noRollbackFor() default {};
}