在项目中在某些时候可能会需要同时连接使用多个不同的数据库,这就是我们今天要说的多数据源问题。可以是主从的场景,主库执行增删改的业务逻辑,从库进行大量复杂查询、报表之类的,从而不影响主要业务。也可以是业务逻辑设计到多个主数据库的问题,我这里是不太推荐的,如果可以的话最好拆分成微服务进行调用。多个数据源中处理起事务也是非常麻烦的,而且也没有必要的,相比之下使用一些补偿机制达到最终用一致性也许是更好的选择。这个也没有绝对性的,因需求而异。
这个实现方式网上早已有许多成熟的例子,我也是借鉴了一些,记录一下,让自己熟悉熟悉。如果涉及到一些版权问题。请及时联系,非常抱歉。
主要实现方式是注解+AOP
下面总结下步骤:
- 创建一个动态数据源的对象
/**
* Created by baixiangzhu on 2017/4/19.
*/
public class DynamicDataSource extends AbstractRoutingDataSource {
@Override
protected Object determineCurrentLookupKey() {
return DynamicDataSourceContextHolder.getDataSourceType();
}
}
- 注册数据源。通过spring的Environment对象,读取配置文件中配置的多个数据源配置。创建动态数据源对象,注册到spring容器中。
/**
* Created by baixiangzhu on 2017/4/19.
*/
@Slf4j
public class DynamicDataSourceRegister implements ImportBeanDefinitionRegistrar,EnvironmentAware{
private ConversionService conversionService = new DefaultConversionService();
private PropertyValues dataSourcePropertyValues;
// 如配置文件中未指定数据源类型,使用该默认值
private static final Object DATASOURCE_TYPE_DEFAULT = "org.apache.tomcat.jdbc.pool.DataSource";
// 数据源
private DataSource defaultDataSource;
private Map<String, DataSource> customDataSources = Maps.newHashMap();
@Override
public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {
Map<Object, Object> targetDataSources = Maps.newHashMap();
// 将主数据源添加到更多数据源中
targetDataSources.put("dataSource", defaultDataSource);
DynamicDataSourceContextHolder.dataSourceIds.add("dataSource");
// 添加更多数据源
targetDataSources.putAll(customDataSources);
//记录注册的数据源key
customDataSources.keySet().stream().forEach( e -> DynamicDataSourceContextHolder.dataSourceIds.add(e));
// 创建DynamicDataSource
GenericBeanDefinition beanDefinition = new GenericBeanDefinition();
beanDefinition.setBeanClass(DynamicDataSource.class);
beanDefinition.setSynthetic(true);
MutablePropertyValues mpv = beanDefinition.getPropertyValues();
mpv.addPropertyValue("defaultTargetDataSource", defaultDataSource);
mpv.addPropertyValue("targetDataSources", targetDataSources);
registry.registerBeanDefinition("dataSource", beanDefinition);
log.info("Dynamic DataSource Registry");
}
@SuppressWarnings("unchecked")
public DataSource buildDataSource(Map<String, Object> dsMap) {
try {
Object type = dsMap.get("type");
if (type == null)
type = DATASOURCE_TYPE_DEFAULT;// 默认DataSource
Class<? extends DataSource> dataSourceType;
dataSourceType = (Class<? extends DataSource>) Class.forName((String) type);
String driverClassName = dsMap.get("driver-class-name").toString();
String url = dsMap.get("url").toString();
String username = dsMap.get("username").toString();
String password = dsMap.get("password").toString();
DataSourceBuilder factory = DataSourceBuilder.create().driverClassName(driverClassName).url(url)
.username(username).password(password).type(dataSourceType);
return factory.build();
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
return null;
}
/**
* 加载多数据源配置
*/
@Override
public void setEnvironment(Environment env) {
initDataSource(env);
}
/**
* 初始化主数据源
*/
private void initDataSource(Environment env) {
// 读取主数据源
RelaxedPropertyResolver propertyResolver = new RelaxedPropertyResolver(env, "datasource.");
String dsPrefixs = propertyResolver.getProperty("names");
//获取所有数据源名称
String[] dataSourceNames = dsPrefixs.split(",");
//绑定主数据源
Arrays.stream(dataSourceNames).filter( e -> isMaster(e)).forEach( e-> bindMasterDataSource(env));
//绑定从数据源
Arrays.stream(dataSourceNames).filter( e -> !isMaster(e)).forEach(e-> bindSlaveDataSource(e,env));
}
/**
* 绑定数据源
* @param dsPrefix
* @param env
*/
private void bindSlaveDataSource(String dsPrefix, Environment env) {
RelaxedPropertyResolver otherPropertyResolver = new RelaxedPropertyResolver(env, dsPrefix + ".datasource.");
Map<String, Object> dsMap=this.convertData(otherPropertyResolver);
DataSource ds = buildDataSource(dsMap);
dataBinder(ds, env);
customDataSources.put(dsPrefix, ds);
}
/**
* 绑定默认数据源
*/
private void bindMasterDataSource(Environment env) {
//绑定默认数据源
RelaxedPropertyResolver defaulPropertyResolver = new RelaxedPropertyResolver(env, "spring.datasource.");
Map<String, Object> dsMap=this.convertData(defaulPropertyResolver);
defaultDataSource = buildDataSource(dsMap);
dataBinder(defaultDataSource, env);
}
private boolean isMaster(String dataSourceName) {
return "master".equals(dataSourceName);
}
private Map<String,Object> convertData(RelaxedPropertyResolver pr){
Map<String, Object> dsMap = Maps.newHashMap();
dsMap.put("type", pr.getProperty("type"));
dsMap.put("driver-class-name", pr.getProperty("driver-class-name"));
dsMap.put("url", pr.getProperty("url"));
dsMap.put("username", pr.getProperty("username"));
dsMap.put("password", pr.getProperty("password"));
return dsMap;
}
private void dataBinder(DataSource dataSource, Environment env){
RelaxedDataBinder dataBinder = new RelaxedDataBinder(dataSource);
dataBinder.setConversionService(conversionService);
dataBinder.setIgnoreNestedProperties(false);//false
dataBinder.setIgnoreInvalidFields(false);//false
dataBinder.setIgnoreUnknownFields(true);//true
if(dataSourcePropertyValues == null){
Map<String, Object> rpr = new RelaxedPropertyResolver(env, "offlinetrade_master.datasource").getSubProperties(".");
Map<String, Object> values =Maps.newHashMap(rpr);
// 排除已经设置的属性
values.remove("type");
values.remove("driver-class-name");
values.remove("url");
values.remove("username");
values.remove("password");
dataSourcePropertyValues = new MutablePropertyValues(values);
}
dataBinder.bind(dataSourcePropertyValues);
}
}
注:我这里的数据库配置,主数据源(即默认数据源)在config配置中心的application.properties文件中,从数据源在本地的bootstrap.yml 文件中,从数据源可以有多个,我这里只配置了一个。
数据源配置如下:
-
主数据源:
从数据源
3.创建一个注解,主要作用的标记dao使用的数据源。可以用在方法上和类上,但是不能用在mapper的接口上。
/**
* Created by baixiangzhu on 2017/4/19.
*/
@Target({ ElementType.METHOD, ElementType.TYPE })
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface UseDataSource {
String value() default "";
}
4.创建一个维护数据源的容器,主要是记录数据源的名字。就是注解中需要配置的名称.
/**
* Created by baixiangzhu on 2017/4/19.
*/
public class DynamicDataSourceContextHolder {
private static final ThreadLocal<String> contextHolder = new ThreadLocal<String>();
public static List<String> dataSourceIds = Lists.newArrayList();
public static void setDataSourceType(String dataSourceType) {
contextHolder.set(dataSourceType);
}
public static String getDataSourceType() {
return contextHolder.get();
}
public static void clearDataSourceType() {
contextHolder.remove();
}
public static boolean containsDataSource(String dataSourceId){
return dataSourceIds.contains(dataSourceId);
}
}
5.写一个AOP类,去拦截标有数据源的方法或者类,根据注解的数据源动态替换
/**
* Created by baixiangzhu on 2017/4/19.
*/
@Slf4j
@Aspect
@Order(-1)// 保证该AOP在@Transactional之前执行
@Component
public class DynamicDataSourceAspect {
@Before("@annotation(ds)")
public void changeDataSource(JoinPoint point, UseDataSource ds) throws Throwable {
String dsId = ds.value();
if (!DynamicDataSourceContextHolder.containsDataSource(dsId)) {
log.error("数据源[{}]不存在,使用默认数据源 > {}", ds.value(), point.getSignature());
} else {
log.debug("Use DataSource : {} > {}", ds.value(), point.getSignature());
DynamicDataSourceContextHolder.setDataSourceType(ds.value());
}
}
@After("@annotation(ds)")
public void restoreDataSource(JoinPoint point, UseDataSource ds) {
log.debug("Revert DataSource : {} > {}", ds.value(), point.getSignature());
DynamicDataSourceContextHolder.clearDataSourceType();
}
}
至此,重要的实现已经完成。现在来看看如何使用:
- 需要在项目的启动类中引入动态注入数据源的类
- 在需要使用数据的方法或者类,添加数据源注解
这下小伙伴们就可以愉快的使用了哈。如果有任何疑问,欢迎留言