美文网首页web后端
Spring boot mybatis 动态多数据源实现

Spring boot mybatis 动态多数据源实现

作者: 乱世小民 | 来源:发表于2017-04-19 16:02 被阅读235次

    在项目中在某些时候可能会需要同时连接使用多个不同的数据库,这就是我们今天要说的多数据源问题。可以是主从的场景,主库执行增删改的业务逻辑,从库进行大量复杂查询、报表之类的,从而不影响主要业务。也可以是业务逻辑设计到多个主数据库的问题,我这里是不太推荐的,如果可以的话最好拆分成微服务进行调用。多个数据源中处理起事务也是非常麻烦的,而且也没有必要的,相比之下使用一些补偿机制达到最终用一致性也许是更好的选择。这个也没有绝对性的,因需求而异。

    这个实现方式网上早已有许多成熟的例子,我也是借鉴了一些,记录一下,让自己熟悉熟悉。如果涉及到一些版权问题。请及时联系,非常抱歉。

    主要实现方式是注解+AOP

    下面总结下步骤:

    1. 创建一个动态数据源的对象
     /**
     * Created by baixiangzhu on 2017/4/19.
     */
    public class DynamicDataSource extends AbstractRoutingDataSource {
    
        @Override
        protected Object determineCurrentLookupKey() {
    
            return DynamicDataSourceContextHolder.getDataSourceType();
          }
    }
    
    1. 注册数据源。通过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 文件中,从数据源可以有多个,我这里只配置了一个。

    数据源配置如下:

    • 主数据源:


      Paste_Image.png
    • 从数据源

    Paste_Image.png

    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();
        }
    
    }
    

    至此,重要的实现已经完成。现在来看看如何使用:

    • 需要在项目的启动类中引入动态注入数据源的类
    Paste_Image.png
    • 在需要使用数据的方法或者类,添加数据源注解
    Paste_Image.png

    这下小伙伴们就可以愉快的使用了哈。如果有任何疑问,欢迎留言

    相关文章

      网友评论

        本文标题:Spring boot mybatis 动态多数据源实现

        本文链接:https://www.haomeiwen.com/subject/ncqhzttx.html