前言
公司项目有连接多个不同数据库的需求,特研究了一下,根据网上的资料,造了一个基于 AOP 方式的数据源切换轮子,但继续探索,突然发现有开源的多数据源管理启动器。不过,本篇两种方式都会介绍。
基于 dynamic-datasource 实现多数据源
dynamic-datasource 介绍
dynamic-datasource-spring-boot-starter 是一个基于 springboot 的快速集成多数据源的启动器。
其支持 Jdk 1.7+, SpringBoot 1.4.x 1.5.x 2.x.x
dynamic-datasource 特性
支持 数据源分组 ,适用于多种场景 纯粹多库 读写分离 一主多从 混合模式。 支持数据库敏感配置信息 加密 ENC()。 支持每个数据库独立初始化表结构 schema 和数据库 database。 支持无数据源启动,支持懒加载数据源(需要的时候再创建连接)。 支持 自定义注解 ,需继承 DS (3.2.0+)。 提供并简化对 Druid,HikariCp,BeeCp,Dbcp2 的快速集成。 提供对 Mybatis-Plus,Quartz,ShardingJdbc,P6sy,Jndi 等组件的集成方案。 提供 自定义数据源来源 方案(如全从数据库加载)。 提供项目启动后 动态增加移除数据源 方案。 提供 Mybatis 环境下的 纯读写分离 方案。 提供使用 spel 动态参数 解析数据源方案。内置 spel,session,header,支持自定义。 支持 多层数据源嵌套切换 。(ServiceA >>> ServiceB >>> ServiceC)。 提供 基于 seata 的分布式事务方案。 提供 本地多数据源事务方案。 附:不能和原生 spring 事务混用。
我们目前只探讨使用 dynamic-datasource 进行数据源切换,其他请自行搜索
dynamic-datasource 的相关约定
dynamic-datasource 只做 切换数据源 这件核心的事情,并不限制你的具体操作,切换了数据源可以做任何 CRUD。 配置文件所有以下划线 _
分割的数据源 首部 即为组的名称,相同组名称的数据源会放在一个组下。切换数据源可以是组名,也可以是具体数据源名称。组名则切换时采用负载均衡算法切换。 默认的数据源名称为 master ,你可以通过 spring.datasource.dynamic.primary
修改。方法上的注解优先于类上注解。 DS 支持继承抽象类上的 DS,暂不支持继承接口上的 DS。
引入 dynamic-datasource 依赖
<dependency><groupId>com.baomidou</groupId><artifactId>dynamic-datasource-spring-boot-starter</artifactId><version>${version}</version></dependency>
配置数据源
spring:datasource:dynamic:primary: mysql #设置默认的数据源或者数据源组,默认值即为masterstrict: false #严格匹配数据源,默认false. true未匹配到指定数据源时抛异常,false使用默认数据源datasource:mysql:url: jdbc:mysql://xx.xx.xx.xx:3306/dynamicusername: rootpassword: 123456driver-class-name: com.mysql.jdbc.Driver # 3.2.0开始支持SPI可省略此配置pgsql:url: ENC(xxxxx) # 内置加密username: ENC(xxxxx)password: ENC(xxxxx)driver-class-name: org.postgresql.Driver
使用 @DS 切换数据源
@DS 可以注解在方法上或类上,同时存在就近原则 方法上注解 优先于 类上注解。
| 注解 | 结果 |
|---|---|
| 不使用 @DS 注解 | 默认数据源,即 primary: mysql |
| @DS("dsName") | dsName 可以为组名也可以为具体某个库的名称 |
@DS 使用实例
@Service@DS("mysql")public class UserServiceImpl implements UserService {@Autowiredprivate JdbcTemplate jdbcTemplate;// 不使用@DS注解则代表使用默认数据源// 如果类上存在,则使用类上标注的数据源public List selectAll() {return jdbcTemplate.queryForList("select * from user");}@Override@DS("pgsql")// 方法上注解 优先于 类上注解,即使类上标注也优先采用方法上的标注public List selectByCondition() {return jdbcTemplate.queryForList("select * from user where age >10");}}
基于 AOP 手动实现多数据源
本次代码参考 https://github.com/mianshenglee/my-example/tree/master/multi-datasource/dynamic-datasource ,因源码不满足的需求,因此我在此基础做了修改。
项目工程结构

项目依赖
<?xml version="1.0" encoding="UTF-8"?><project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"><modelVersion>4.0.0</modelVersion><parent><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-parent</artifactId><version>2.2.2.RELEASE</version><relativePath/> <!-- lookup parent from repository --></parent><groupId>me.mason.demo</groupId><artifactId>dynamic-datasource</artifactId><version>0.0.1-SNAPSHOT</version><name>dynamic-datasource</name><description>Demo project for dynamic datasource</description><properties><java.version>1.8</java.version></properties><dependencies><!--spring boot--><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-configuration-processor</artifactId></dependency><dependency><groupId>com.alibaba</groupId><artifactId>druid-spring-boot-starter</artifactId><version>1.1.9</version></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-jdbc</artifactId></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-aop</artifactId></dependency><!--mysql 驱动--><dependency><groupId>mysql</groupId><artifactId>mysql-connector-java</artifactId><scope>runtime</scope></dependency><!--mybatis-plus--><dependency><groupId>com.baomidou</groupId><artifactId>mybatis-plus-boot-starter</artifactId><version>3.3.0</version></dependency><dependency><groupId>org.projectlombok</groupId><artifactId>lombok</artifactId><optional>true</optional></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-test</artifactId><scope>test</scope><exclusions><exclusion><groupId>org.junit.vintage</groupId><artifactId>junit-vintage-engine</artifactId></exclusion></exclusions></dependency></dependencies><build><plugins><plugin><groupId>org.springframework.boot</groupId><artifactId>spring-boot-maven-plugin</artifactId></plugin></plugins></build></project>
配置文件
server.port=8080server.servlet.context-path=/ddlogging.level.root=INFOlogging.level.me.mason.demo.dynamicdatasource.mapper=DEBUG# mybatis-plusmybatis-plus.type-aliases-package=me.mason.demo.dynamicdatasource.entity# 默认位置,可不配置#mybatis-plus.mapper-locations=classpath*:/mapper/*.xmlmybatis.mapper-locations=classpath*:/mapper/*.xml# 使用数据库自增IDmybatis-plus.global-config.db-config.id-type=auto# masterspring.datasource.master.driver-class-name=com.mysql.cj.jdbc.Driverspring.datasource.master.url=jdbc:mysql://10.0.1.243:3306/scheduling?useSSL=false&serverTimezone=GMT%2B8&characterEncoding=UTF-8spring.datasource.master.username=rootspring.datasource.master.password=123456# slavespring.datasource.slave.driver-class-name=com.mysql.cj.jdbc.Driverspring.datasource.slave.url=jdbc:mysql://10.0.1.243:3306/scheduling1?useSSL=false&serverTimezone=GMT%2B8&characterEncoding=UTF-8spring.datasource.slave.username=rootspring.datasource.slave.password=123456
自定义注解
// 标记注解可使用在方法与类上@Target({ElementType.METHOD,ElementType.TYPE})@Retention(RetentionPolicy.RUNTIME)public @interface DS {// 默认值为MASTERString value() default DataSourceConstants.DS_KEY_MASTER;}
编写 DataSourceConstants
/*** 数据源常量**/public class DataSourceConstants {/*** master数据源*/public static final String DS_KEY_MASTER = "master";/*** slave数据源*/public static final String DS_KEY_SLAVE = "slave";}
动态数据源名称上下文处理
/*** 动态数据源名称上下文处理**/public class DynamicDataSourceContextHolder {/*** 动态数据源名称上下文*/private static final ThreadLocalDATASOURCE_CONTEXT_KEY_HOLDER = new ThreadLocal<>(); /*** 设置数据源* @param key*/public static void setContextKey(String key){DATASOURCE_CONTEXT_KEY_HOLDER.set(key);}/*** 获取数据源名称* @return*/public static String getContextKey(){String key = DATASOURCE_CONTEXT_KEY_HOLDER.get();return key == null?DataSourceConstants.DS_KEY_MASTER:key;}/*** 删除当前数据源名称*/public static void removeContextKey(){DATASOURCE_CONTEXT_KEY_HOLDER.remove();}}
获取当前动态数据源方法
/*** 动态数据源**/public class DynamicDataSource extends AbstractRoutingDataSource {@Overrideprotected Object determineCurrentLookupKey() {return DynamicDataSourceContextHolder.getContextKey();}}
动态数据源配置
/*** 动态数据源配置**/@EnableAutoConfiguration(exclude = { DataSourceAutoConfiguration.class })@Configuration// 此处我们//@PropertySource("classpath:config/jdbc.properties")@MapperScan(basePackages = "me.mason.demo.dynamicdatasource.mapper")public class DynamicDataSourceConfig {@Bean(DataSourceConstants.DS_KEY_MASTER)// 需要与配置文件中对应@ConfigurationProperties(prefix = "spring.datasource.master")public DruidDataSource masterDataSource() {return DruidDataSourceBuilder.create().build();}@Bean(DataSourceConstants.DS_KEY_SLAVE)@ConfigurationProperties(prefix = "spring.datasource.slave")public DruidDataSource slaveDataSource() {return DruidDataSourceBuilder.create().build();}@Bean@Primarypublic DynamicDataSource dynamicDataSource() {MapdataSourceMap.put(DataSourceConstants.DS_KEY_MASTER, masterDataSource());dataSourceMap.put(DataSourceConstants.DS_KEY_SLAVE, slaveDataSource());//设置动态数据源DynamicDataSource dynamicDataSource = new DynamicDataSource();dynamicDataSource.setTargetDataSources(dataSourceMap);dynamicDataSource.setDefaultTargetDataSource(masterDataSource());return dynamicDataSource;}}
AOP 切面
/*** 切面*/@Aspect@Component//@Order(-10)public class DynamicDataSourceAspect {// 以在类上使用了@Service作为切入点@Pointcut("@within(org.springframework.stereotype.Service)")public void dataSourcePointCut() {}@Around("dataSourcePointCut()")public Object around(ProceedingJoinPoint joinPoint) throws Throwable {MethodSignature signature = (MethodSignature) joinPoint.getSignature();Class aClass = Class.forName(signature.getDeclaringType().getName());// 方法优先,如果方法上存在注解,则优先使用方法上的注解if (signature.getMethod().isAnnotationPresent(DS.class)) { DynamicDataSourceContextHolder.setContextKey(signature.getMethod().getAnnotation(DS.class).value());// 其次类优先,如果类上存在注解,则使用类上的注解}else if (aClass.isAnnotationPresent(DS.class)) { DynamicDataSourceContextHolder.setContextKey(aClass.getAnnotation(DS.class).value());// 如果都不存在,则使用默认} else {DynamicDataSourceContextHolder.setContextKey(DataSourceConstants.DS_KEY_MASTER);}try {return joinPoint.proceed();} finally {DynamicDataSourceContextHolder.removeContextKey();}}}
编写 TestUser 实体
@Data@TableName("test_user")public class TestUser implements Serializable {private static final long serialVersionUID = 1L;/** id */private Long id;/** 姓名 */private String name;/** 手机号 */private String phone;/** 职称职别 */private String title;/** 邮箱 */private String email;/** 性别 */private String gender;/** 出生时间 */private Date dateOfBirth;/** 1:已删除,0:未删除 */private Integer deleted;/** 创建时间 */private Date sysCreateTime;/** 创建人 */private String sysCreateUser;/** 更新时间 */private Date sysUpdateTime;/** 更新人 */private String sysUpdateUser;/** 版本号 */private Long recordVersion;public TestUser() {}}
TestUserMapper
@Repositorypublic interface TestUserMapper extends BaseMapper<TestUser> {/*** 自定义查询* @param wrapper 条件构造器* @return*/ListselectAll(@Param(Constants.WRAPPER) Wrapper<TestUser> wrapper); }
TestUserService
@Service//@DS(DataSourceConstants.DS_KEY_SLAVE)public class TestUserService {@Autowiredprivate TestUserMapper testUserMapper;/*** 查询master库User* @return*/// @DS(DataSourceConstants.DS_KEY_MASTER)public ListgetMasterUser(){ QueryWrapperqueryWrapper = new QueryWrapper<>(); return testUserMapper.selectAll(queryWrapper.isNotNull("name"));}/*** 查询slave库User* @return*/// @DS(DataSourceConstants.DS_KEY_MASTER)public ListgetSlaveUser(){ return testUserMapper.selectList(null);}}
TestUserController
@RestController@RequestMapping("/user")public class TestUserController {@Autowiredprivate TestUserService testUserService;/*** 查询全部*/@GetMapping("/listall")public Object listAll() {int initSize = 2;Mapresult = new HashMap<>(initSize); ListmasterUser = testUserService.getMasterUser(); result.put("masterUser", masterUser);ListslaveUser = testUserService.getSlaveUser(); result.put("getSlaveUser", slaveUser);return ResponseResult.success(result);}}
MapperXml
<?xml version="1.0" encoding="UTF-8" ?><!DOCTYPE mapperPUBLIC "-//mybatis.org//DTD Mapper 3.0//EN""http://mybatis.org/dtd/mybatis-3-mapper.dtd"><mapper namespace="me.mason.demo.dynamicdatasource.mapper.TestUserMapper"><select id="selectAll" resultType="me.mason.demo.dynamicdatasource.entity.TestUser">select * from test_user<if test="ew!=null">${ew.customSqlSegment}</if></select></mapper>
启动测试
不使用注解
@Service//@DS(DataSourceConstants.DS_KEY_SLAVE)public class TestUserService {@Autowiredprivate TestUserMapper testUserMapper;/*** 查询master库User* @return*/// @DS(DataSourceConstants.DS_KEY_MASTER)public ListgetMasterUser(){ QueryWrapperqueryWrapper = new QueryWrapper<>(); return testUserMapper.selectAll(queryWrapper.isNotNull("name"));}/*** 查询slave库User* @return*/// @DS(DataSourceConstants.DS_KEY_MASTER)public ListgetSlaveUser(){ return testUserMapper.selectList(null);}}
效果
该代码优先级与使用框架效果一致,即不使用注解将默认使用 MASTER 数据库,方法上存在注解优先使用方法上标注的注解。
已知 MASTER 6 条数据, SLAVE4 条数据
访问 http://127.0.0.1:8080/dd/user/listall 查看效果

类上使用注解
@Service@DS(DataSourceConstants.DS_KEY_SLAVE)public class TestUserService {@Autowiredprivate TestUserMapper testUserMapper;/*** 查询master库User* @return*/// @DS(DataSourceConstants.DS_KEY_MASTER)public ListgetMasterUser(){ QueryWrapperqueryWrapper = new QueryWrapper<>(); return testUserMapper.selectAll(queryWrapper.isNotNull("name"));}/*** 查询slave库User* @return*/// @DS(DataSourceConstants.DS_KEY_MASTER)public ListgetSlaveUser(){ return testUserMapper.selectList(null);}}
效果

方法上使用注解
@Service@DS(DataSourceConstants.DS_KEY_SLAVE)public class TestUserService {@Autowiredprivate TestUserMapper testUserMapper;/*** 查询master库User* @return*/@DS(DataSourceConstants.DS_KEY_SLAVE)public ListgetMasterUser(){ QueryWrapperqueryWrapper = new QueryWrapper<>(); return testUserMapper.selectAll(queryWrapper.isNotNull("name"));}/*** 查询slave库User* @return*/@DS(DataSourceConstants.DS_KEY_MASTER)public ListgetSlaveUser(){ return testUserMapper.selectList(null);}}
效果

文章转载自风雪留客,如果涉嫌侵权,请发送邮件至:contact@modb.pro进行举报,并提供相关证据,一经查实,墨天轮将立刻删除相关内容。




