在项目中,有这么一些场景,需要去考虑数据源路由的事情,比如说

你看到了,以上不管是分库、分表、还是主从实现读写分离,都表现出了一个应用,对应多个数据库实例的场景。

那么这个时候,我们就需要去考虑多数据源路由的事情了。在具体实现方案上,有两种比较常见的选择方案

这两种方案,具体该如何去选择呢?如果我们是中小型的团队,应用规模也不是很大,那么适合选择在应用层面实现数据源路由的方案,主要理由

基于以上,反过来我们就可以考虑采用proxy的代理方案了。方便你理解,还是上一个图吧,典型的主从集群,读写分离架构

1.准备环境

通过以上描述,我们知道在实现多数据路由上,有两种可选的方案,本篇文章我们分享方案一的实现springboot多数据源配置,即扩展spring提供的AbstractRoutingDataSource。

首先来准备环境,我在本地准备了两个数据库实例

这两个库虽然本身没有直接的关系,我们假设这是一个报表类应用(聚合多个数据源),且我们的重点是实现在数据源之间的路由,因此不影响。

在本案例的实现中

导入依赖

<dependencies>        <dependency>        <groupId>org.springframework.boot</groupId>        <artifactId>spring-boot-starter-web</artifactId>    </dependency>        <dependency>        <groupId>org.springframework.boot</groupId>        <artifactId>spring-boot-starter-aop</artifactId>    </dependency>        <dependency>        <groupId>org.springframework.boot</groupId>        <artifactId>spring-boot-starter-jdbc</artifactId>    </dependency>        <dependency>        <groupId>org.mybatis.spring.boot</groupId>        <artifactId>mybatis-spring-boot-starter</artifactId>        <version>1.3.2</version>    </dependency>        <dependency>        <groupId>com.alibaba</groupId>        <artifactId>druid</artifactId>        <version>1.0.9</version>    </dependency>        <dependency>        <groupId>mysql</groupId>        <artifactId>mysql-connector-java</artifactId>        <scope>runtime</scope>    </dependency>        <dependency>        <groupId>org.projectlombok</groupId>        <artifactId>lombok</artifactId>    </dependency></dependencies>

2.核心配置

本案例持久层,选择的是mybatis框架,相关核心配置如下

2.1.application.yml

server:  port: 8080spring:  application:    name: follow-me-springboot-multidatasource
#数据源配置mysql: datasource: type-aliases-package: cn.edu.anan.entity mapper-locations: classpath:mybatis/mapper/*Mapper.xml config-location: classpath:mybatis/sqlMapConfig.xml write: url: jdbc:mysql://127.0.0.1:3320/training?characterEncoding=utf-8&useSSL=false&serverTimezone=UTC username: root password: admin driver-class-name: com.mysql.cj.jdbc.Driver read: url: jdbc:mysql://127.0.0.1:3310/user-center?characterEncoding=utf-8&useSSL=false&serverTimezone=UTC username: root password: admin driver-class-name: com.mysql.cj.jdbc.Driver

2.2.sqlMapConfig.xml

<configuration>    <settings>                <setting name="cacheEnabled" value="true" />                <setting name="lazyLoadingEnabled" value="true" />                <setting name="aggressiveLazyLoading" value="true"/>                <setting name="multipleResultSetsEnabled" value="true" />                <setting name="useColumnLabel" value="true" />                <setting name="useGeneratedKeys" value="false" />                <setting name="autoMappingBehavior" value="PARTIAL" />                <setting name="defaultExecutorType" value="SIMPLE" />                <setting name="defaultStatementTimeout" value="25" />                <setting name="defaultFetchSize" value="100" />                <setting name="safeRowBoundsEnabled" value="false" />                <setting name="mapUnderscoreToCamelCase" value="true" />                <setting name="localCacheScope" value="SESSION" />                <setting name="jdbcTypeForNull" value="NULL" />                <setting name="lazyLoadTriggerMethods" value="equals,clone,hashCode,toString" />    </settings>
</configuration>

2.3.项目代码结构

3.核心代码

扩展AbstractRoutingDataSource实现多数据源之间路由,核心是

3.1.数据源配置DataSourceConfig

/** * 数据源配置 * * @author ThinkPad * @version 1.0 * @date 2021/9/21 15:55 */@Configuration@MapperScan(basePackages = "cn.edu.anan.dao", sqlSessionFactoryRef = "sqlSessionFactory")public class DataSourceConfig {
/** * 包扫描别名 */ @Value("${mysql.datasource.type-aliases-package}") private String typeAliasesPackage;
/** *mapper映射文件位置 */ @Value("${mysql.datasource.mapper-locations}") private String mapperLocation;
/** *mybatis配置文件位置 */ @Value("${mysql.datasource.config-location}") private String configLocation;
/** * 写数据源 * @return */ @Primary @Bean @ConfigurationProperties(prefix = "mysql.datasource.write") public DataSource writeDataSource() { return new DruidDataSource(); }
/** * 读数据源 * @return */ @Bean @ConfigurationProperties(prefix = "mysql.datasource.read") public DataSource readDataSource() { return new DruidDataSource(); }
/** * 配置sqlSessionFactory * @return * @throws Exception */ @Bean public SqlSessionFactory sqlSessionFactory() throws Exception { SqlSessionFactoryBean bean = new SqlSessionFactoryBean();
bean.setDataSource(routingDataSource()); bean.setTypeAliasesPackage(typeAliasesPackage);
ResourcePatternResolver resolver = new PathMatchingResourcePatternResolver(); bean.setMapperLocations(resolver.getResources(mapperLocation)); bean.setConfigLocation(resolver.getResource(configLocation));
return bean.getObject(); }
/** * 设置数据源路由表 * @return */ @Bean public AbstractRoutingDataSource routingDataSource() { MyAbstractRoutingDataSource proxy = new MyAbstractRoutingDataSource(); Map targetDataSources = new HashMap(2);
targetDataSources.put(DbContextHolder.WRITE, writeDataSource()); targetDataSources.put(DbContextHolder.READ, readDataSource());
proxy.setDefaultTargetDataSource(writeDataSource()); proxy.setTargetDataSources(targetDataSources);
return proxy; }
/** * 配置事务管理器 * @return */ @Bean public DataSourceTransactionManager dataSourceTransactionManager() { return new DataSourceTransactionManager(routingDataSource()); }
}

3.2.数据源线程上下文DbContextHolder

/** * 数据源上下文环境 * * @author ThinkPad * @version 1.0 * @date 2021/9/21 16:02 */@Slf4jpublic class DbContextHolder {
/** * 写数据源标识 */ public static final String WRITE = "write"; /** * 读数据源标识 */ public static final String READ = "read";
/** * 本地线程绑定 */ private static ThreadLocal contextHolder= new ThreadLocal();
/** * 设置数据源类型 * @param dbType */ public static void setDbType(String dbType) { if (dbType == null) { log.error("dbType为空"); throw new NullPointerException(); } log.info("设置dbType为:{}",dbType); contextHolder.set(dbType); }
/** * 获取数据源类型 * @return */ public static String getDbType() { return contextHolder.get() == null ? WRITE : contextHolder.get(); }
/** * 清除ThreadLocal */ public static void clearDbType() { contextHolder.remove(); }
}

3.3.扩展数据源路由MyAbstractRoutingDataSource

/** * 数据源路由,扩展AbstractRoutingDataSource * * @author ThinkPad * @version 1.0 * @date 2021/9/21 16:05 */@Slf4jpublic class MyAbstractRoutingDataSource extends AbstractRoutingDataSource{
/** * 返回数据源路由key * @return */ @Override protected Object determineCurrentLookupKey() { String dbKey = DbContextHolder.getDbType(); if (dbKey == DbContextHolder.WRITE) { log.info("当前更新动作,走主库"); return dbKey; }
log.info("当前读取操作,走从库"); return DbContextHolder.READ; }}

3.4.注解DataSourceSwitcher

@Retention(RetentionPolicy.RUNTIME)@Target(ElementType.METHOD)@Documentedpublic @interface DataSourceSwitcher {    /**     * 默认数据源     * @return     */    String value() default "write";
/** * 清除 * @return */ boolean clear() default true;
}

3.5.切面ReadOnlyAspect

/** * 读数据源切面 * * @author ThinkPad * @version 1.0 * @date 2021/9/21 17:07 */@Aspect@Component@Slf4jpublic class ReadOnlyAspect implements Ordered{
/** * 线程上下文设置读数据源 * @param pjp * @param read * @return * @throws Throwable */ @Around("@annotation(read)") public Object setRead(ProceedingJoinPoint pjp, DataSourceSwitcher read) throws Throwable{ try{ DbContextHolder.setDbType(DbContextHolder.READ); return pjp.proceed(); }finally { DbContextHolder.clearDbType(); log.info("清除threadLocal"); } }
/** * 顺序 * @return */ @Override public int getOrder() { return 0; }}

4.使用案例

在数据源路由配置中,设置了默认的数据源是:写数据源

如果是写入操作,默认应用代码不需要特殊处理;如果读操作,应用代码方法上,需要加上@DataSourceSwitcher(value=”read”)注解,比如

那么在运行时,通过切面绑定线程上下文数据源信息

准备两个测试案例

/** * controller * * @author ThinkPad * @version 1.0 * @date 2021/9/21 15:46 */@RestController@RequestMapping("route")@Slf4jpublic class MultiDataSourceController {
@Autowired private UserService userService;
@Autowired private OrderService orderService;
/** * 写数据源测试:写入一个订单 * @param order * @return */ @RequestMapping("write") public Order write(@RequestBody Order order){ orderService.insertOne(order); return order;
}
/** * 读数据源测试:查询全部用户列表数据 * @return */ @RequestMapping("read") public List read(@RequestBody User user){ log.info("查询条件:{}", user); return userService.selectAll(user); }

启动应用,分别访问端点

观察控制台输出

案例输出读操作springboot多数据源配置,走从库;写操作,走主库。我们看到已经实现多数据源路由,最后本文源码,请参考:,子模块:follow-me-springboot-multidatasource