Java 技术探秘

人如果没有了梦想,跟闲鱼有什么区别?


  • 首页

  • 源码分析

  • 日常记录

  • 学习笔记

  • 解决方案

  • 关于

  • Search
close

springboot + druid 实现数据源下动态切换

Published at: 2020-06-03   |   Categories: 解决方案     |   Reading: 843 words ~4min

springboot + druid 实现数据源下动态切换

1. 项目背景

最近接到一个需求:为多个业务系统开发统一的管理后台,这几个业务系统的相关业务惊人地相似,用的是同一份代码,只是地区、受众不同,因此服务器、数据库都单独分开了。

项目很明显要用到多数据源,经过在google多方打探,终于了解到基于AbstractRoutingDataSource的多数据源解决方案了。废话不多说,上代码介绍我的实现方式。

2. 代码实现

2.1 引入多数据源

在 application.yml 引入多数据源配置,这里使用的是阿里的 Druid 连接池:

# 数据源配置
spring:
  datasource:
    type: com.alibaba.druid.pool.DruidDataSource
    driverClassName: com.mysql.cj.jdbc.Driver
    druid:
      # 主库数据源
      main:
        url: jdbc:mysql://127.0.0.1:3306/main?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8
        username: root
        password: 123
      # 应用数据源
      app:
        # db1
        db1:
          url: jdbc:mysql://127.0.0.1:3306/db1?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8
          username: root
          password: 123
        # db2
        db2:
          url: jdbc:mysql://127.0.0.1:3306/db2?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8
          username: root
          password: 123
        # ds3
        db3:
          url: jdbc:mysql://127.0.0.1:3306/db3?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8
          username: root
          password: 123

这里一共引入了4个数据源:main、db1、db2、db3

  • main: 管理后台的数据源,存放管理后台相关数据,如用户登录数据、系统菜单等
  • db1: 业务系统1的数据源
  • db2: 业务系统2的数据源
  • db3: 业务系统3的数据源

2.2 实例化多数据源

在java中,使用DataSourceConfig处理多数据源实例化:

@Slf4j
@Configuration
public class DataSourceConfig {
    @Bean
    @ConfigurationProperties("spring.datasource.druid.main")
    public DataSource mainDataSource() {
        return DruidDataSourceBuilder.create().build();
    }

    /**
     * 处理多数据源
     * @param appDataSourceProperties
     * @return
     */
    @Bean
    public Map<String, DataSource> appDataSourceMap(
            AppDataSourceProperties appDataSourceProperties) {
        LinkedHashMap<String, DataSource> appDataSourceMap = new LinkedHashMap<>();
        List<String> appDataSourceList = appDataSourceProperties.getDataSourceList();
        for(String dataSourceName : appDataSourceList) {
            log.info("创建app数据源:{}", dataSourceName);
            DruidDataSource dataSource = new DruidDataSource();
            dataSource.setUrl(appDataSourceProperties.getUrl(dataSourceName));
            dataSource.setUsername(appDataSourceProperties.getUsername(dataSourceName));
            dataSource.setPassword(appDataSourceProperties.getPassword(dataSourceName));
            appDataSourceMap.put(dataSourceName, dataSource);
        }
        return appDataSourceMap;
    }

    @Bean(name = "dynamicDataSource")
    @Primary
    public DynamicDataSource dataSource(DataSource mainDataSource, @Qualifier("appDataSourceMap") Map<String, DataSource> appDataSourceMap) {
        Map<Object, Object> targetDataSources = new HashMap<>(appDataSourceMap.size() + 1);
        targetDataSources.put(DataSourceType.main.name(), mainDataSource);
        targetDataSources.putAll(appDataSourceMap);
        return new DynamicDataSource(mainDataSource, targetDataSources);
    }

}

2.3 AbstractRoutingDataSource:spring多数据源支持

AbstractRoutingDataSource 是 spring 提供的一个抽象类,可以实现该类处理数据源切换:

public class DynamicDataSource extends AbstractRoutingDataSource {
    public DynamicDataSource(DataSource defaultTargetDataSource, Map<Object, Object> targetDataSources) {
        super.setDefaultTargetDataSource(defaultTargetDataSource);
        super.setTargetDataSources(targetDataSources);
        super.afterPropertiesSet();
    }
    
    /**
     * 在实际进行数据库查询时,spring会调用该方法,从而拿到当前的数据源
     */
    @Override
    protected Object determineCurrentLookupKey() {
        return DynamicDataSourceContextHolder.getDataSourceType();
    }
}

DynamicDataSourceContextHolder 代码如下:

@Slf4j
public class DynamicDataSourceContextHolder {

    /**
     * 使用ThreadLocal维护变量,ThreadLocal为每个使用该变量的线程提供独立的变量副本,
     * 所以每一个线程都可以独立地改变自己的副本,而不会影响其它线程所对应的副本。
     */
    private static final ThreadLocal<String> CONTEXT_HOLDER = new ThreadLocal<>();

    /**
     * 设置数据源的变量
     */
    public static void setDataSourceType(String dsType) {
        if(StringUtils.isNotBlank(dsType)) {
            log.info("切换到{}数据源", dsType);
            CONTEXT_HOLDER.set(dsType);
        }
    }

    /**
     * 获得数据源的变量
     */
    public static String getDataSourceType() {
        return CONTEXT_HOLDER.get();
    }

    /**
     * 清空数据源变量
     */
    public static void clearDataSourceType() {
        CONTEXT_HOLDER.remove();
    }

    /**
     * 获取主数据源
     * @return
     */
    public static String getMainDateSource() {
        return DataSourceType.main.name();
    }
}

2.4 使用 spring 切面声明需要动态切换数据源的类

这里主要是使用注解 DataSourceDeclareAspect 来声明该类需要多数据源支持:

/**
 * 数据源声明,有此标记,表示需要动态切换数据源
 *
 * @author funcy
 * @date 2019-09-06 10:30
 */
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
public @interface DataSourceDeclare {
    
    //多自由添加其他属性进行数据源的一些其他声明

}

切面按如下方式处理:

@Slf4j
@Aspect
@Order(1)
@Component
public class DataSourceDeclareAspect {

    @Pointcut("@annotation(com.gitee.funcy.multiple.datasource.aspectj.annotation.DataSourceDeclare)"
            + "|| @within(com.gitee.funcy.multiple.datasource.aspectj.annotation.DataSourceDeclare)"
            + "|| execution(* com.gitee.funcy.multiple.datasource.mapper..*.*(..))"
    )
    public void dsPointCut() {

    }

    @Around("dsPointCut()")
    public Object around(ProceedingJoinPoint point) throws Throwable {
        DataSourceDeclare dataSource = getDataSource(point);
        if(null != dataSource) {
            DynamicDataSourceContextHolder.setDataSourceType(AppStorageContextHolder.getApp());
        }

        try {
            return point.proceed();
        } finally {
            // 销毁数据源 在执行方法之后
            if(null != dataSource) {
                DynamicDataSourceContextHolder.clearDataSourceType();
            }
        }
    }

    /**
     * 获取需要切换的数据源
     */
    public DataSourceDeclare getDataSource(ProceedingJoinPoint point) {
        MethodSignature signature = (MethodSignature) point.getSignature();

        //1.获取方法上的DataSource
        Method method = signature.getMethod();
        DataSourceDeclare dataSource = method.getAnnotation(DataSourceDeclare.class);
        if(null != dataSource) {
            return dataSource;
        }

        //2.获取声明类的DataSource
        dataSource = method.getDeclaringClass().getAnnotation(DataSourceDeclare.class);
        if(null != dataSource) {
            return dataSource;
        }

        //3.获取目标类上的DataSourceDeclare
        Class<? extends Object> targetClass = point.getTarget().getClass();
        dataSource = targetClass.getAnnotation(DataSourceDeclare.class);
        if (null != dataSource) {
            return dataSource;
        }

        //反射获取
        /*
         * point.getTarget()(为com.baomidou.mybatisplus.core.override.MybatisMapperProxy的实例)的
         * 结构如下:
         *
         * point.getTarget()
         * |-h
         *   |-sqlSession
         *   |-mapperInterface
         *   |-methodCache
         *
         */
        Object h = ReflectUtils.getFieldValue(point.getTarget(), "h");
        if(null != h) {
            Class<? extends Object> clazz = ReflectUtils.getFieldValue(h, "mapperInterface");
            if (null != clazz) {
                dataSource = clazz.getAnnotation(DataSourceDeclare.class);
                if (null != dataSource) {
                    return dataSource;
                }
            }
        }

        log.warn("未找到对应的DataSource,方法:{},声明类:{},目标类:{}", method.getName(),
                method.getDeclaringClass().getName(), targetClass.getName());
        return null;
    }
}

可以看到,定义切点时这里不但使用了 @annotation、@within,还使用了 execution,这是因为在继承的情况,前两个注解并不切到目标方法的执行,这里这块的说明,请见(spring aop 如何准备切 mapper)[http://www.baidu.com]

2.5 创建业务类

所谓的业务类,是指 po、mapper、service 与 controller 那些与业务相关的类,其他的类与多数据源并无联系,这里主要说明下mapper类:

/**
 * UserMapper 的示例
 */
@DataSourceDeclare
public interface UserMapper extends BaseMapper<User> {

}

UserMapper 对应业务数据库中 user表的操作。使用注解 @DataSourceDeclare来声明该类需要进行动态数据源切换,关于切面为何选在 mapper 层而不是 service 层,主要是考虑到在一个 service 中可能需要查询 main 数据源中的数据,如果切service层就不太合适了。

对于下AppMapper:

/**
 * AppMapper 的示例
 */
public interface AppMapper extends BaseMapper<App> {
}

AppMapper 对应 main 数据源中 app 表的操作,主要是用来定义接入的业务数据源。

2.6 拦截启用多数据源的路径

请求业务数据库时,页面传入的 appId 参数并非与业务相关,并不需要传到 controller 层,这里使用拦截器将其拦截:

@Slf4j
@Component
public class AppDataSourceInterceptor implements HandlerInterceptor {

	private static final Logger logger = LoggerFactory.getLogger(AppDataSourceInterceptor.class);

	private final String PARAM_APP_ID = "appId";

	@Autowired
	private IAppService appService;

	@Override
	public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
		String appId = request.getParameter(PARAM_APP_ID);
		if(StringUtils.isNotBlank(appId)) {
			App app = appService.selectById(NumberUtils.toLong(appId));
			if(null != app) {
				AppStorageContextHolder.setApp(app.getDataSource());
			}
		}
		return true;
	}

	@Override
	public void afterCompletion(HttpServletRequest request, HttpServletResponse response, 
            Object handler, Exception ex) throws Exception {
		AppStorageContextHolder.remove();
	}

}

注册到spring中:

@Configuration
public class ResourcesConfig implements WebMvcConfigurer {

    @Autowired
    private AppDataSourceInterceptor appDataSourceInterceptor;

    /**
     * 自定义拦截规则
     */
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        // 指定拦截的路径为 /api/xxx
        registry.addInterceptor(appDataSourceInterceptor).addPathPatterns("/api/**");
    }
}

拦截后,将其保存到 AppStorageContextHolder 中,当需要使用时,再从 AppStorageContextHolder 获取:

@Slf4j
public class AppStorageContextHolder {

    private static final ThreadLocal<String> STORAGE = new ThreadLocal<>();

    /**
     * 设置app
     */
    public static void setApp(String dsType) {
        STORAGE.set(dsType);
    }

    /**
     * 获得app
     */
    public static String getApp() {
        return STORAGE.get();
    }

    /**
     * 移除app
     */
    public static void remove() {
        STORAGE.remove();
    }
}

AppStorageContextHolder 相当于一个中间存储器,暂时保存业务的数据源,以便后期使用。

那为何不直接保存在 DynamicDataSourceContextHolder 中呢?因为在请求过程中,可能会访问下 main 数据源再访问业务数据源,直接保存就不行了。而且 DynamicDataSourceContextHolder 用完后就清理了,如果一次请求中多次访问业务数据源也不行。

2.7 运行

运行前,请先执行project.sql中脚本,这里创建了三个库,每个库上数据如下:

  • main.admin 表:

    id app_name data_source
    1 应用1 db1
    2 应用2 db2
    3 应用3 db3
  • db1.user 表:

    uid nick
    1 db1_张三
    2 db1_李四
    3 db1_王五
  • db2.user 表:

    uid nick
    1 db2_张三
    2 db2_李四
    3 db2_王五
  • db3.user 表:

    uid nick
    1 db3_张三
    2 db3_李四
    3 db3_王五

启动后,请求接口/api/user/get,结果如下:

  • 数据源1
http://localhost:8084/api/user/get?appId=1
[
    {
        "uid": 1,
        "nick": "db1_张三"
    },
    {
        "uid": 2,
        "nick": "db1_李四"
    },
    {
        "uid": 3,
        "nick": "db1_王五"
    }
]
  • 数据源2
http://localhost:8084/api/user/get?appId=2
[
    {
        "uid": 1,
        "nick": "db2_张三"
    },
    {
        "uid": 2,
        "nick": "db2_李四"
    },
    {
        "uid": 3,
        "nick": "db2_王五"
    }
]
  • 数据源2
http://localhost:8084/api/user/get?appId=3
[
    {
        "uid": 1,
        "nick": "db3_张三"
    },
    {
        "uid": 2,
        "nick": "db3_李四"
    },
    {
        "uid": 3,
        "nick": "db3_王五"
    }
]

可以看到数据已经正确请求了。

以上代码见gitee仓库https://gitee.com/funcy/springboot-demomultiple-datasource模块.

相关文章:

  • springboot + druid 实现数据源下动态切换
  • springboot 多数据源下,开启事务后,数据源切换失败的处理
#springboot# #druid#
spring+redis自主实现分布式session
  • Table of Content
  • Site Information

Funcy

9 Blogs
4 Categories
24 Tags
Gitee 掘金
    • 1. 项目背景
    • 2. 代码实现
    • 2.2 实例化多数据源
    • 2.3 AbstractRoutingDataSource:spring多数据源支持
© 2009 - 2024 Java 技术探秘
Powered by - Hugo v0.127.0
粤ICP备2024268097号-1
0%