来源
- 业务数据存在数据源A,统计数据存在数据源B(表不同)
- 应用拆分,灰度切换数据源,部分数据使用数据源A,部分数据使用数据源C(表相同)
实现
场景1:
不同的表结构存在不同的库:数据源A和数据源B不同的表
使用MapperScan
,不同的DAO绑定不同的数据源
数据源A 配置
@Configuration
@ConfigurationProperties(prefix = "spring.datasourceA")
@EnableTransactionManagement
@MapperScan(basePackages = {"com.xxx.xxx.xxx","com.xxx.xxx.yyy"})
public class DruidConfig implements TransactionManagementConfigurer {
...
...
数据源B 配置
@Configuration
@ConfigurationProperties(prefix = "spring.datasourceB")
@EnableTransactionManagement
@MapperScan(basePackages = {"com.xxx.xxx.zzz"})
public class DruidConfig implements TransactionManagementConfigurer {
...
...
场景2:
同样的表结构存在不同的库:数据源A 和 数据源C,表相同,根据启动环境或者业务判断选用不同的数据源
使用AbstractRoutingDataSource
的determineCurrentLookupKey
决定当前线程使用哪个数据源
But一开始的想法(被抛弃和走的弯路):
- 使用场景1的方式,配置两个不同的数据源,拷贝俩份DAO和mapper.xml,代码判断使用哪个数据源的DAO
- 使用场景1的方式,配置两个不同的数据源,拷贝俩份DAO和mapper.xml,通过注解和切面,切换DAO
- 自定义注解AliasSource
@AliasSource("xxxcccDAO") // 另一个DAO的bean name
@Repository("xxxDAO")
public interface XxxDAO {
...
...
- 自定义切面
@Aspect
@Component
public class AliasSourceAspect {
@Pointcut("@within(com.xxx.annotation.AliasSource)")
public void pointcut() {
}
@Around("pointcut()")
public Object around(ProceedingJoinPoint pjp) throws Throwable {
// 通过判断环境或者线程变量 判断是否使用注解指定的bean,反射调用之
Method method = ((MethodSignature) pjp.getSignature()).getMethod();
Class<?> clazz = method.getDeclaringClass();
AliasSource annotation = clazz.getAnnotation(AliasSource.class);
String name = annotation.value();
Object bean = SpringBeanUtil.getBean(name);
Method realMethod = bean.getClass().getDeclaredMethod(method.getName(), method.getParameterTypes());
return realMethod.invoke(bean, pjp.getArgs());
// 不然直接反射调用原来的方法
return pjp.proceed();
}
}
- 使用了
AbstractRoutingDataSource
的determineCurrentLookupKey
决定当前线程使用哪个数据源,但是切面切的不对,切DAO的时候为时过晚:
@Aspect
@Component
public class DataSourceAspect {
@Pointcut("(execution(public * com.xxx.xxx.xxx.dal..*(..))
|| execution(public * com.xxx.xxx.yyy.dal..*(..)))
&& !execution(public * com.xxx.xxx.zzz.dal..*(..))")
public void pointcut() {
}
@Around("pointcut()")
public Object around(ProceedingJoinPoint pjp) throws Throwable {
// 通过判断环境或者业务类型,来指定线程变量
if (环境A/C) {
// 下面代码有解释这个环境变量,指定数据源用的,
// 这里其实已经晚了,走到这个切面的时候,
// 已经调用过`AbstractRoutingDataSource`的`determineCurrentLookupKey`先选好数据源了,得到connection了
线程变量.set("dbA/dbC")`;
}
// 调用dao方法
return pjp.proceed();
}
}
原因1:在service层使用了事务(@Transactional
ORtransactionTemplate.execute(new TransactionCallbackWithoutResult() {})
),这中场景肯定会先获取connection,开启事务,然后才走到我们自己的切面,晚了
原因2:由于切面本身也是切的DAO,但是spirng mybatis自己的切面在前,先获取了connection,然后才走到我们自己的切面,晚了
SO~ 去掉切面,改成在webapp的filter(自定义DataSourceFilter
)就先设置好环境变量(线程变量.set("dbA/dbC")
)选择好当前线程使用的数据源;这样就能正确的拿到指定数据源的connection啦
数据源A 和 数据源C 配置
@Configuration
@ConfigurationProperties(prefix = "spring.datasource")// 注入两份配置
@EnableTransactionManagement
@MapperScan(basePackages = {"com.xxx.xxx.xxx","com.xxx.xxx.yyy"})
public class DruidConfig implements TransactionManagementConfigurer {
...
...
@Bean
@Primary
@Override
public DataSourceTransactionManager annotationDrivenTransactionManager() {
DruidDataSource dataSourceA = createDruidDataSource(urlA, usernameA, passwordA);
DruidDataSource dataSourceC = createDruidDataSource(urlC, usernameC, passwordC);
AbstractRoutingDataSource dynamicDataSource = new AbstractRoutingDataSource() {
@Override
protected Object determineCurrentLookupKey() {
// 1. 可以通过启动环境判断使用哪个数据源
if (环境A/C) {
return "dbA/dbC";
}
// 2. 可以通过自己设置的线程变量的值来指定使用哪个数据源
String db = 线程变量.get();
return db;
}
};
Map<Object, Object> dsMap = Maps.newHashMap();
dsMap.put("dbA", dataSourceA);
dsMap.put("dbC", dataSourceC);
dynamicDataSource.setDefaultTargetDataSource(salaryDruidDataSource);
dynamicDataSource.setTargetDataSources(dsMap);
dynamicDataSource.afterPropertiesSet();
return new DataSourceTransactionManager(dynamicDataSource);
}
private DruidDataSource createDruidDataSource(String url, String username, String password) {
DruidDataSource dataSource = new DruidDataSource();
dataSource.setUrl(url);
dataSource.setUsername(username);
dataSource.setPassword(password);
dataSource.setDriverClassName(driverClassName);
...
...
return dataSource;
}
...
...