前言
先了解一波什么是数据源,这里引用百度文库上的一句
数据源是指数据库应用程序所使用的数据库或者数据库服务器。
数据源(Data Source)顾名思义,数据的来源,是提供某种所需要数据的器件或原始媒体。在数据源中存储了所有建立数据库连接的信息。就像通过指定文件名称可以在文件系统中找到文件一样,通过提供正确的数据源名称,你可以找到相应的数据库连接。
关键信息:
数据源存储了数据库的连接信息,比如以下:
java
spring:
datasource:
type: com.alibaba.druid.pool.DruidDataSource
druid:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://127.0.0.1:3306/school?useUnicode=true&characterEncoding=UTF-8&autoReconnect=true&useSSL=false&zeroDateTimeBehavior=convertToNull&serverTimezone=Asia/Shanghai&allowMultiQueries=true&useInformationSchema=true
username: root
password: root
。。。。。
这是在springboot中的配置,我们这里是配置了一条数据源,如果我们配置了多条这样的配置信息,是不是就意味着我们配置了多数据源,以下将介绍一下多数据源实现原理
环境准备
准备两个库
库1
sql
CREATE DATABASE `db_datasource1` CHARACTER SET 'utf8mb4' COLLATE 'utf8mb4_general_ci';
SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;
-- ----------------------------
-- Table structure for sys_user
-- ----------------------------
DROP TABLE IF EXISTS `sys_user`;
CREATE TABLE `sys_user` (
`user_id` varchar(35) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT '用户ID',
`real_name` varchar(50) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '用户真实姓名',
`dept_id` varchar(35) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '部门ID',
`email` varchar(50) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '用户邮箱',
`phone` varchar(11) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '手机号码',
`sex` char(1) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT '0' COMMENT '用户性别(0男 1女 2未知)',
`status` char(1) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT '0' COMMENT '帐号状态(0正常 1停用)',
`del_flag` char(1) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT '0' COMMENT '删除标志(0代表存在1代表删除)',
`create_user` varchar(64) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '创建者',
`create_time` datetime NULL DEFAULT NULL COMMENT '创建时间',
`update_user` varchar(64) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '更新者',
`update_time` datetime NULL DEFAULT NULL COMMENT '更新时间',
`remark` varchar(500) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '备注',
PRIMARY KEY (`user_id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci COMMENT = '系统用户' ROW_FORMAT = DYNAMIC;
SET FOREIGN_KEY_CHECKS = 1;
INSERT INTO `sys_user` VALUES ('1', 'Shen Jiehong', 'HImSjOldFD', 'shenj@yahoo.com', '74-290-4854', 'M', 'U', 'N', 'bAGDP4vz24', '2009-12-01 03:20:27', '2013-11-20', '2007-03-12 21:15:15', 'HjcojsEOef');
INSERT INTO `sys_user` VALUES ('10', 'Nomura Miu', 'FWZBmk4gLG', 'nomi@yahoo.com', '5642 551482', 'F', 'W', 'Q', 'JdQBb9AGyI', '2019-09-30 23:32:32', '2022-04-11', '2019-03-27 12:10:22', 'uAdHDe9ebf');
INSERT INTO `sys_user` VALUES ('2', 'Tiffany Foster', 'ZVZIq3mPf4', 'fostertiffany720@mail.com', '66-540-0361', 'F', 'Z', 'a', 'G6spXmocKm', '2005-12-11 10:04:44', '2003-04-19', '2018-08-18 07:03:37', 'YhXitiLmCt');
INSERT INTO `sys_user` VALUES ('3', 'Kimura Hazuki', 'n6o04Cdb5x', 'kimurah@hotmail.com', '90-8458-7963', 'F', 'i', 'L', 'R84R75eyiQ', '2020-03-21 06:56:00', '2016-12-17', '2007-11-19 13:17:45', 'qYfpPhJ4RE');
INSERT INTO `sys_user` VALUES ('4', 'Yue Cho Yee', 'AMHgUb0Yw5', 'yuchoye43@icloud.com', '(1223) 65 1256', 'F', '2', '4', 'yxzrxQKz7D', '2005-06-04 12:41:34', '2000-04-07', '2008-11-06 17:24:53', 'NbR0NvXgMD');
INSERT INTO `sys_user` VALUES ('5', 'Amy Fernandez', 'vKctPEe7GV', 'amyfernandez1963@gmail.com', '196-6038-4330', 'F', 'f', 't', 'C9nTVCS4ca', '2016-03-09 03:59:15', '2010-08-22', '2002-10-26 02:00:18', 'Bs0pNTize3');
INSERT INTO `sys_user` VALUES ('6', 'Lam Tak Wah', 'eZUOFVEO3a', 'lam5@outlook.com', '718-717-3358', 'M', 'm', '3', 'GtebIinCra', '2015-05-09 07:47:31', '2004-07-27', '2012-02-14 23:31:22', 'BPYtx8BUE3');
INSERT INTO `sys_user` VALUES ('7', 'Xiao Yuning', 'lJ3xQTGA8Q', 'yuning5@icloud.com', '330-570-1809', 'M', 'k', 'T', 'EVN6XyuSkz', '2010-08-03 12:03:36', '2008-02-15', '2016-04-17 04:20:27', '9tugzv8rJz');
INSERT INTO `sys_user` VALUES ('8', 'Xu Jialun', 'WzRAV3kWYU', 'xujialun915@hotmail.com', '(116) 809 8039', 'M', 'V', 't', 'NMqT5Us1Ey', '2003-07-15 07:23:11', '2006-09-09', '2014-06-07 10:18:13', '1crvm0kfMk');
INSERT INTO `sys_user` VALUES ('9', 'Kojima Momoe', 'I3XXHNZFS5', 'momoeko@icloud.com', '838-674-0508', 'F', '6', 'd', 'FuYiJgqojl', '2005-01-24 09:13:25', '2007-05-27', '2014-07-02 18:57:38', 'id96Rd7LN6');
库2
sql
CREATE DATABASE `db_datasource2` CHARACTER SET 'utf8mb4' COLLATE 'utf8mb4_general_ci';
SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;
-- ----------------------------
-- Table structure for sys_dept
-- ----------------------------
DROP TABLE IF EXISTS `sys_dept`;
CREATE TABLE `sys_dept` (
`dept_id` varchar(35) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT '部门id',
`parent_id` varchar(35) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT '0' COMMENT '父部门id',
`ancestors` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '祖级列表',
`dept_name` varchar(30) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '部门名称',
`sort` int NULL DEFAULT 0 COMMENT '显示顺序',
`leader` varchar(20) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '负责人',
`phone` varchar(11) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '联系电话',
`email` varchar(50) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '邮箱',
`status` char(1) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT '0' COMMENT '部门状态(0正常 1停用)',
`del_flag` char(1) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT '0' COMMENT '删除标志(0代表存在 1代表删除)',
`create_user` varchar(64) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '创建者',
`create_time` datetime NULL DEFAULT NULL COMMENT '创建时间',
`update_user` varchar(64) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '更新者',
`update_time` datetime NULL DEFAULT NULL COMMENT '更新时间',
`remark` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
PRIMARY KEY (`dept_id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci COMMENT = '部门' ROW_FORMAT = DYNAMIC;
SET FOREIGN_KEY_CHECKS = 1;
INSERT INTO `sys_dept` VALUES ('1', 'HUgJ0Q5LFk', 'Nj2g1qDopB', 'Lillian Scott', 949, 'kJd3YtR37A', '330-960-0289', 'lilsc@mail.com', 'H', 'a', 'nW7JbRL1lW', '2017-01-31 11:27:19', '2020-04-25', '2021-08-05 21:01:30', 'CtnWDnTMLb');
INSERT INTO `sys_dept` VALUES ('10', 'LiyW45NXM6', 'lRC9gwAR9x', 'Zou Xiuying', 27, 'mrAsaP3Hp9', '182-6382-7016', 'zou1997@gmail.com', '7', 'D', 'K4JIkvIrZG', '2009-10-30 19:08:55', '2017-03-30', '2021-10-17 03:48:53', 'wqkUJDbSMb');
INSERT INTO `sys_dept` VALUES ('2', 'mZs4KoUT3C', 'j4tO6YplDj', 'Yamaguchi Seiko', 119, 'EojAdin4MZ', '90-0704-3278', 'yseik@gmail.com', 'G', 'F', 'vLtNL0lx2y', '2002-03-31 09:55:24', '2012-05-21', '2021-08-15 15:10:50', 'MnXylte5qG');
INSERT INTO `sys_dept` VALUES ('3', 'jr9CY8BS5f', 'pgbuQ30O73', 'Monica Jones', 680, 'TatwoIxlvF', '(116) 601 4171', 'jonesmo@hotmail.com', 'I', '1', 'uZXxd7exsd', '2006-07-04 07:52:47', '2006-06-18', '2009-07-08 12:37:22', 'B9Ki28jjWs');
INSERT INTO `sys_dept` VALUES ('4', 'EkH6RjIUV0', 'RclPd0DxWx', 'Kwan Ling Ling', 295, 'A5R6LTZDuM', '614-058-4648', 'klingling@icloud.com', 'o', 'B', 'c3UcvVrDbO', '2017-12-17 18:14:00', '2004-09-29', '2003-01-28 21:03:24', 'Y8K4Dkm7UW');
INSERT INTO `sys_dept` VALUES ('5', 'iP9wtBe2fH', 'dtO7pM8Iow', 'Lei Shihan', 6, 'VEKw58FFri', '183-4374-4355', 'lesh@outlook.com', '7', 'X', '73vEosRJhr', '2012-10-29 00:41:19', '2005-12-31', '2023-12-25 18:00:29', 'fqIt2FJB2e');
INSERT INTO `sys_dept` VALUES ('6', 'efebc5Kc9m', 'BcdTSBuYaU', 'Fujiwara Misaki', 57, 'EL2Qcaus76', '5155 518752', 'mfujiwara@gmail.com', '1', 't', 'IBxSGb0kDa', '2013-10-13 11:46:15', '2010-12-17', '2005-11-15 03:48:31', 'k8CbC1FeH8');
INSERT INTO `sys_dept` VALUES ('7', '7nu5LmFK6Y', 'PXDFTdfXYj', 'Li Rui', 847, 'CiYQstpf7b', '(1223) 06 7489', 'ruili@outlook.com', '3', 'Z', 'cgADm8ZxpO', '2001-06-20 20:31:16', '2021-12-21', '2010-06-15 18:21:14', 'R1iUg5Qurx');
INSERT INTO `sys_dept` VALUES ('8', 'oVXgc9WjJD', 'Vh7XjZiMqp', 'Benjamin Salazar', 479, 'k3XWxcqbA3', '146-2118-0700', 'besalazar@mail.com', 'X', 'u', 'jE2AhPjII4', '2004-01-27 05:48:09', '2007-03-07', '2012-04-18 17:34:09', 'v5TDvPBmTB');
INSERT INTO `sys_dept` VALUES ('9', 'IS4SFMoET8', 'gLaql9lHJE', 'Ying Kar Yan', 472, 'UWrxMmxJ33', '90-6411-1494', 'ykaryan@hotmail.com', '2', '2', 'k6wiSiJpm9', '2012-09-19 17:27:00', '2009-07-10', '2007-01-28 16:27:44', 'EPdTrljVr1');
最后效果如下
数据源配置
sql
spring:
dynamic:
datasource:
datasource1:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://ip:3306/db_datasource1?useUnicode=true&characterEncoding=UTF-8&autoReconnect=true&useSSL=false&zeroDateTimeBehavior=convertToNull&serverTimezone=Asia/Shanghai&allowMultiQueries=true&useInformationSchema=true
username: root
password: root
initial-size: 10
max-active: 100
min-idle: 10
max-wait: 60000
pool-prepared-statements: true
max-pool-prepared-statement-per-connection-size: 20
time-between-eviction-runs-millis: 60000
min-evictable-idle-time-millis: 300000
test-while-idle: true
test-on-borrow: false
test-on-return: false
aop-patterns: "com.dfec.module.*.service.*"
datasource2:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://ip:3306/db_datasource2?useUnicode=true&characterEncoding=UTF-8&autoReconnect=true&useSSL=false&zeroDateTimeBehavior=convertToNull&serverTimezone=Asia/Shanghai&allowMultiQueries=true&useInformationSchema=true
username: root
password: root
initial-size: 10
max-active: 100
min-idle: 10
max-wait: 60000
pool-prepared-statements: true
max-pool-prepared-statement-per-connection-size: 20
time-between-eviction-runs-millis: 60000
min-evictable-idle-time-millis: 300000
test-while-idle: true
test-on-borrow: false
test-on-return: false
aop-patterns: "com.dfec.module.*.service.*"
测试
自动切换
这里通过注解的形式实现自动切换
java
@RestController
@RequestMapping("test")
@RequiredArgsConstructor
public class TestController {
private final SysDeptService sysDepetService;
private final SysUserService sysUserService;
@DataSource(value = "datasource1")
@GetMapping("/test1")
public AjaxResult test(){
return AjaxResult.ok().put(sysUserService.list());
}
@DataSource(value = "datasource2")
@GetMapping("/test2")
public AjaxResult test2(){
return AjaxResult.ok( ).put(sysDepetService.list());
}
}
我们使用apifox进行调用测试
测试接口1
数据与数据库一致,即测试通过
测试接口2
数据与数据库一致,即测试通过
手动切换
对代码进行改造,我们借助JdbcDataSourceRouter类进行手动切换
java
@GetMapping("/test1")
public AjaxResult test(){
JdbcDataSourceRouter.setDataSourceKey("datasource1");
List<SysUserEntity> list = sysUserService.list();
JdbcDataSourceRouter.remove();
return AjaxResult.ok().put(list);
}
@GetMapping("/test2")
public AjaxResult test2(){
JdbcDataSourceRouter.setDataSourceKey("datasource2");
List<SysDeptEntity> list = sysDepetService.list();
JdbcDataSourceRouter.remove();
return AjaxResult.ok( ).put(list);
}
测试接口1
测试通过
测试接口2
实现原理
AbstractRoutingDataSource类由springboot提供,它根据用户定义的规则选择当前的数据源,这样我们可以在执行查询之前,设置使用的数据源。实现可动态路由的数据源,在每次数据库查询操作前执行。它的抽象方法 determineCurrentLookupKey() 决定使用哪个数据源。
简单理解就是SQL在实际执行之前才设置数据源,决定由哪个数据源去执行
实现流程
定义AbstractRoutingDataSource类继承抽象类AbstractRoutingDataSource,并实现了determineCurrentLookupKey()方法。
- determineCurrentLookupKey()方法每次获取当前数据源key的时候都是从当前threadLoad本地线程中去获取
- 然后determineTargetDataSource()方法通过从本地线程中获取到的当前数据源id去切换对应的数据源
java
/**
* 切换数据源
*
* @return
*/
@Override
protected DataSource determineTargetDataSource() {
//获取到当前数据源id
Object dataSourceKey = this.determineCurrentLookupKey();
// 默认系统数据源
if (dataSourceKey == null||dataSourceKey.equals(BaseConstant.DEFAULT_DATA_SOURCE_ID)) {
return super.getResolvedDefaultDataSource();
}
String dataSourceId = getDataSourceId(dataSourceKey.toString());
//从 DataSourceManager.DATA_SOURCE_POOL_JDBC中获取到当前
DataSource dataSource = DataSourceManager.DATA_SOURCE_POOL_JDBC.get(dataSourceId);
if (dataSource == null) {
throw new ServiceException("无效的数据源:" + dataSourceId, 44003);
}
return dataSource;
}
那么下面我们只要追溯一下三个问题即可
第一个、DataSourceManager.DATA_SOURCE_POOL_JDBC中的数据是怎么来的,因为上面是从这个里面去获取配置的数据源的
第二个、本地线程中的数据源id是怎么塞进去的
这里我们是不是只要手动塞进去数据源id,那么是不是就可以走多数据源配置了?答案是肯定,我们接着往下看
第三个、系统数据源是怎么定义的
只要解决 这两个问题,那么多数据源流程我们就搞清楚了
以下我将先介绍几个核心类
核心类:AbstractRoutingDataSource
那我们就先揭开这个类的神秘面纱
java
/**
* 抽象类,用于根据当前的上下文信息动态地选择要使用的DataSource。
* 需要由子类实现determineCurrentLookupKey()方法,以定义如何根据当前上下文确定要使用的DataSource。
*/
public abstract class AbstractRoutingDataSource extends AbstractDataSource implements InitializingBean {
@Nullable
private Map<Object, Object> targetDataSources; // 目标DataSource的映射,键为查找键,值为DataSource实例或名称字符串。
@Nullable
private Object defaultTargetDataSource; // 默认的目标DataSource,当没有匹配的DataSource时使用。
private boolean lenientFallback = true; // 是否允许宽容的回退到默认的DataSource。
private DataSourceLookup dataSourceLookup = new JndiDataSourceLookup(); // 用于解析DataSource名称的查找器。
@Nullable
private Map<Object, DataSource> resolvedDataSources; // 解析后的DataSource映射。
@Nullable
private DataSource resolvedDefaultDataSource; // 解析后的默认DataSource.
/**
* 设置目标DataSource的映射,键为查找键,值可以是对应的{@link javax.sql.DataSource}实例或DataSource名称字符串(通过{@link #setDataSourceLookup DataSourceLookup}解析)。
* 查找键可以是任意类型,具体处理由{@link #resolveSpecifiedLookupKey(Object)}和{@link #determineCurrentLookupKey()}完成。
*/
public void setTargetDataSources(Map<Object, Object> targetDataSources) {
this.targetDataSources = targetDataSources;
}
/**
* 设置默认的目标DataSource,如果没有匹配的键,则使用此DataSource。
* 值可以是对应的{@link javax.sql.DataSource}实例或DataSource名称字符串(通过{@link #setDataSourceLookup DataSourceLookup}解析)。
*/
public void setDefaultTargetDataSource(Object defaultTargetDataSource) {
this.defaultTargetDataSource = defaultTargetDataSource;
}
/**
* 设置当找不到匹配的DataSource时,是否宽容地回退到默认的DataSource。
* 默认为"true",允许查找键没有对应条目的情况,此时会回退到默认的DataSource。
* 如果将此标志设置为"false",则只有在查找键为{@code null}时才会回退到默认的DataSource,没有对应条目的查找键会导致IllegalStateException异常。
* @see #setTargetDataSources
* @see #setDefaultTargetDataSource
* @see #determineCurrentLookupKey()
*/
public void setLenientFallback(boolean lenientFallback) {
this.lenientFallback = lenientFallback;
}
/**
* 设置用于解析目标DataSource映射中DataSource名称字符串的DataSourceLookup实现。
* 默认为{@link JndiDataSourceLookup},允许直接指定应用服务器DataSources的JNDI名称。
*/
public void setDataSourceLookup(@Nullable DataSourceLookup dataSourceLookup) {
this.dataSourceLookup = (dataSourceLookup != null ? dataSourceLookup : new JndiDataSourceLookup());
}
@Override
public void afterPropertiesSet() {
if (this.targetDataSources == null) {
throw new IllegalArgumentException("Property 'targetDataSources' is required");
}
this.resolvedDataSources = CollectionUtils.newHashMap(this.targetDataSources.size());
this.targetDataSources.forEach((key, value) -> {
Object lookupKey = resolveSpecifiedLookupKey(key);
DataSource dataSource = resolveSpecifiedDataSource(value);
this.resolvedDataSources.put(lookupKey, dataSource);
});
if (this.defaultTargetDataSource != null) {
this.resolvedDefaultDataSource = resolveSpecifiedDataSource(this.defaultTargetDataSource);
}
}
/**
* 初始化时调用,用于解析指定的查找键对象,转化为实际匹配的查找键。
* 默认实现直接返回给定的查找键。
* @param lookupKey 用户指定的查找键对象
* @return 实际用于匹配的查找键
*/
protected Object resolveSpecifiedLookupKey(Object lookupKey) {
return lookupKey;
}
/**
* 初始化时调用,用于解析指定的DataSource对象或名称,转化为DataSource实例。
* 默认实现支持DataSource实例和DataSource名称字符串(通过{@link DataSourceLookup}解析)。
* @param dataSource 目标DataSource的值对象,可以是实例或名称
* @return 解析后的DataSource实例
* @throws IllegalArgumentException 如果给定的值类型不支持
*/
protected DataSource resolveSpecifiedDataSource(Object dataSource) throws IllegalArgumentException {
if (dataSource instanceof DataSource) {
return (DataSource) dataSource;
}
else if (dataSource instanceof String) {
return this.dataSourceLookup.getDataSource((String) dataSource);
}
else {
throw new IllegalArgumentException(
"Illegal data source value - only [javax.sql.DataSource] and String supported: " + dataSource);
}
}
/**
* 返回此路由器管理的已解析目标DataSources。
* @return 一个不可修改的映射,包含解析后的查找键和DataSources
* @throws IllegalStateException 如果目标DataSources尚未解析
* @since 5.2.9
* @see #setTargetDataSources
*/
public Map<Object, DataSource> getResolvedDataSources() {
Assert.state(this.resolvedDataSources != null, "DataSources not resolved yet - call afterPropertiesSet");
return Collections.unmodifiableMap(this.resolvedDataSources);
}
/**
* 返回已解析的默认目标DataSource,如果未指定或尚未解析,则返回null。
* @return 默认的DataSource,或者null(如果未指定或尚未解析)
* @since 5.2.9
* @see #setDefaultTargetDataSource
*/
@Nullable
public DataSource getResolvedDefaultDataSource() {
return this.resolvedDefaultDataSource;
}
@Override
public Connection getConnection() throws SQLException {
return determineTargetDataSource().getConnection();
}
@Override
public Connection getConnection(String username, String password) throws SQLException {
return determineTargetDataSource().getConnection(username, password);
}
@Override
@SuppressWarnings("unchecked")
public <T> T unwrap(Class<T> iface) throws SQLException {
if (iface.isInstance(this)) {
return (T) this;
}
return determineTargetDataSource().unwrap(iface);
}
@Override
public boolean isWrapperFor(Class<?> iface) throws SQLException {
return (iface.isInstance(this) || determineTargetDataSource().isWrapperFor(iface));
}
/**
* 确定当前要使用的DataSource。此方法会根据{@link #determineCurrentLookupKey() 当前查找键}在{@link #setTargetDataSources 目标DataSources映射}中查找,
* 如果没有找到匹配的DataSource且{@link #setLenientFallback 宽容回退}为true或查找键为null,则会回退到指定的{@link #setDefaultTargetDataSource 默认目标DataSource}。
* @return 当前应使用的DataSource
* @see #determineCurrentLookupKey()
*/
protected DataSource determineTargetDataSource() {
Assert.notNull(this.resolvedDataSources, "DataSource router not initialized");
Object lookupKey = determineCurrentLookupKey();
DataSource dataSource = this.resolvedDataSources.get(lookupKey);
if (dataSource == null && (this.lenientFallback || lookupKey == null)) {
dataSource = this.resolvedDefaultDataSource;
}
if (dataSource == null) {
throw new IllegalStateException("Cannot determine target DataSource for lookup key [" + lookupKey + "]");
}
return dataSource;
}
/**
* 确定当前的查找键。通常,此方法会检查绑定到线程的事务上下文。
* 允许返回任意类型的键,返回的键需要与通过{@link #resolveSpecifiedLookupKey}方法解析的存储查找键类型匹配。
*/
@Nullable
protected abstract Object determineCurrentLookupKey();
}
此类为抽象类,要求我们去实现此类,同时去实现里面的determineCurrentLookupKey(),来决定我们到底要使用哪个数据源
实现类JdbcDataSourceRouter
当前类主要是AbstractRoutingDataSource的实现类,并重写了determineCurrentLookupKey()、determineTargetDataSource()方法
java
package com.dfec.framework.datasourse.core.config;
import com.alibaba.druid.pool.DruidDataSource;
import com.dfec.common.exception.ServiceException;
import com.dfec.framework.datasourse.core.constant.BaseConstant;
import com.dfec.framework.datasourse.core.driver.DataSourceManager;
import com.zaxxer.hikari.HikariDataSource;
import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;
import javax.sql.DataSource;
import java.sql.Connection;
import java.sql.SQLException;
/**
* JDBC数据源 动态路由
*
*
*/
public class JdbcDataSourceRouter extends AbstractRoutingDataSource {
/**
* 当前线程数据源KEY
*/
private static final ThreadLocal<String> DATA_SOURCE_KEY = new ThreadLocal<>();
/**
* 获取数据源key
*
* @return
*/
public static String getDataSourceKey() {
return DATA_SOURCE_KEY.get();
}
/**
* 设置数据源key
*
* @param key
*/
public static void setDataSourceKey(String key) {
DATA_SOURCE_KEY.set(key);
}
/**
* 移除数据源
*/
public static void remove() {
DATA_SOURCE_KEY.remove();
}
/**
* 判断数据源是否存在
*/
public static boolean exist(String dataSourceId) {
DataSource dataSource = DataSourceManager.DATA_SOURCE_POOL_JDBC.get(getDataSourceId(dataSourceId));
if (dataSource != null) {
return true;
}
return false;
}
/**
* 获取数据源ID
*
* @param dataSourceId
* @return
*/
private static String getDataSourceId(String dataSourceId) {
return dataSourceId.split(":")[0];
}
/**
* 销毁
*
* @param dataSourceId
* @return
*/
public static void destroy(String dataSourceId) {
DataSource dataSource = DataSourceManager.DATA_SOURCE_POOL_JDBC.get(getDataSourceId(dataSourceId));
if (dataSource instanceof DruidDataSource) {
DruidDataSource druidDataSource = (DruidDataSource) dataSource;
druidDataSource.close();
} else if (dataSource instanceof HikariDataSource) {
HikariDataSource hikariDataSource = (HikariDataSource) dataSource;
hikariDataSource.close();
}
DataSourceManager.DATA_SOURCE_POOL_JDBC.remove(dataSourceId);
}
/**
* 获取数据源
*
* @param dataSourceId
* @return
*/
public static DataSource getDataSource(String dataSourceId) {
DataSource dataSource = DataSourceManager.DATA_SOURCE_POOL_JDBC.get(getDataSourceId(dataSourceId));
if (dataSource == null) {
throw new ServiceException("无效的数据源", 44003);
}
return dataSource;
}
/**
* 获取数据源
*
* @return
*/
public static DataSource getDataSource() {
String dataSourceKey = getDataSourceKey();
DataSource dataSource = DataSourceManager.DATA_SOURCE_POOL_JDBC.get(getDataSourceId(dataSourceKey));
if (dataSource == null) {
throw new ServiceException("无效的数据源", 44003);
}
return dataSource;
}
/**
* 添加数据源
*
* @param dataSourceId
**/
public static void setDataSource(String dataSourceId, DataSource dataSource) {
DataSourceManager.DATA_SOURCE_POOL_JDBC.put(dataSourceId, dataSource);
}
/**
* 切换数据源
*
* @return
*/
@Override
protected DataSource determineTargetDataSource() {
Object dataSourceKey = this.determineCurrentLookupKey();
// 默认系统数据源
if (dataSourceKey == null||dataSourceKey.equals(BaseConstant.DEFAULT_DATA_SOURCE_ID)) {
return super.getResolvedDefaultDataSource();
}
String dataSourceId = getDataSourceId(dataSourceKey.toString());
DataSource dataSource = DataSourceManager.DATA_SOURCE_POOL_JDBC.get(dataSourceId);
if (dataSource == null) {
throw new ServiceException("无效的数据源:" + dataSourceId, 44003);
}
return dataSource;
}
/**
* 获取连接
*
* @return
* @throws SQLException
*/
@Override
public Connection getConnection() throws SQLException {
Connection connection = this.determineTargetDataSource().getConnection();
return connection;
}
@Override
protected Object determineCurrentLookupKey() {
return getDataSourceKey();
}
}
数据源管理类DataSourceManager
java
package com.dfec.framework.datasourse.core.driver;
import com.dfec.common.exception.ServiceException;
import com.dfec.framework.datasource.spi.DataSourceDriver;
import com.dfec.framework.datasource.spi.MetaDataMapper;
import com.dfec.framework.datasource.spi.StatementMapper;
import com.dfec.framework.datasource.spi.bean.BaseDataSource;
import com.zaxxer.hikari.HikariDataSource;
import javax.sql.DataSource;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.ServiceLoader;
import java.util.concurrent.ConcurrentHashMap;
/**
* 数据源驱动管理
*
*/
public class DataSourceManager {
/**
* 数据源插件类型
*/
private final Map<String, DataSourceDriver> PLUGIN_TYPE = new ConcurrentHashMap<>();
/**
* 数据源插驱动接池
*/
private final Map<String, DataSourceDriver> DATA_SOURCE_POOL_PLUGIN = new ConcurrentHashMap<>();
/**
* JDBC数据源连接池
*/
public static final Map<String, DataSource> DATA_SOURCE_POOL_JDBC = new ConcurrentHashMap<>();
/**
* 支持的关系型数据源类型
*/
private static final List<String> JDBC_DATA_SOURCE_TYPE = Arrays.asList("mysql", "mariadb", "oracle", "sqlserver", "postgresql", "db2", "sqlite", "tidb", "opengauss", "oceanbase", "polardb", "tdsql", "dm", "gbase", "hive2","xugu","clickhouse");
/**
* 默认数据源驱动实现
*/
private DataSourceDriver<?> defaultDriver;
public DataSourceManager(DataSourceDriver<?> dataSourceDriver) {
// 默认JDBC驱动
this.defaultDriver = dataSourceDriver;
// 初始化加载驱动插件
init();
}
/**
* 初始化加载插件驱动
*/
public void init() {
ServiceLoader<DataSourceDriver> loader = ServiceLoader.load(DataSourceDriver.class);
for (DataSourceDriver driver : loader) {
String driverName = driver.getName();
if (driverName == null || "".equals(driverName)) {
throw new ServiceException( "插件名称不能为空",51003);
}
if (PLUGIN_TYPE.containsKey(driverName)) {
throw new ServiceException("该插件名称" + driverName + "已存在",51004);
}
PLUGIN_TYPE.putIfAbsent(driverName.toLowerCase(), driver);
}
}
/**
* 插件创建数据源
*
* @param dataSource
*/
public void createDataSource(BaseDataSource dataSource) {
String datasourceType = dataSource.getDatasourceType();
DataSourceDriver<?> dataSourceDriver = PLUGIN_TYPE.get(datasourceType);
if (dataSourceDriver != null) {
// 初始化
dataSourceDriver.init(dataSource);
DATA_SOURCE_POOL_PLUGIN.putIfAbsent(dataSource.getDatasourceId(), dataSourceDriver);
} else {
dataSourceDriver = this.defaultDriver;
dataSourceDriver.init(dataSource);
}
}
/**
* 测试数据源
*
* @param dataSource
* @return
*/
public String test(BaseDataSource dataSource) {
String datasourceType = dataSource.getDatasourceType();
DataSourceDriver<?> dataSourceDriver = PLUGIN_TYPE.get(datasourceType);
if (dataSourceDriver != null) {
return dataSourceDriver.test(dataSource);
} else if (JDBC_DATA_SOURCE_TYPE.contains(datasourceType)) {
return defaultDriver.test(dataSource);
} else {
throw new ServiceException("暂不支持" + datasourceType + "数据源类型!",51001);
}
}
/**
* 获取数据源驱动
*
* @param datasourceId
* @return
*/
public DataSourceDriver<?> getDataSource(String datasourceId) {
DataSourceDriver<?> dataSourceDriver = null;
DataSource dataSource = DATA_SOURCE_POOL_JDBC.get(datasourceId);
if (dataSource != null) {
dataSourceDriver = this.defaultDriver;
} else {
dataSourceDriver = DATA_SOURCE_POOL_PLUGIN.get(datasourceId);
}
if (dataSourceDriver == null) {
throw new ServiceException("数据源不存在!",51001);
}
return dataSourceDriver;
}
/**
* 元数据对象
* @param datasourceId
* @return
*/
public MetaDataMapper<?> getMetaData(String datasourceId){
DataSourceDriver<?> dataSource = this.getDataSource(datasourceId);
return dataSource.getMetaData();
}
/**
* 数据处理对象
* @param datasourceId
* @return
*/
public StatementMapper<?> getStatementMapper(String datasourceId){
DataSourceDriver<?> dataSource = this.getDataSource(datasourceId);
return dataSource.getStatement();
}
/**
* 删除数据源驱动
*
* @param datasourceId
*/
public void remove(String datasourceId) {
DataSourceDriver<?> dataSourceDriver = DATA_SOURCE_POOL_PLUGIN.get(datasourceId);
if (dataSourceDriver != null) {
dataSourceDriver.destroy(datasourceId);
DATA_SOURCE_POOL_PLUGIN.remove(datasourceId);
}
DataSource dataSource = DATA_SOURCE_POOL_JDBC.get(datasourceId);
if (dataSource != null && dataSource instanceof HikariDataSource) {
HikariDataSource hikariDataSource = (HikariDataSource) dataSource;
hikariDataSource.close();
}
DATA_SOURCE_POOL_JDBC.remove(datasourceId);
}
}
配置类DynamicDataSourceConfig
java
package com.dfec.framework.datasourse.core.config;
import com.alibaba.druid.pool.DruidDataSource;
import com.dfec.framework.datasource.spi.DataSourceDriver;
import com.dfec.framework.datasourse.core.constant.BaseConstant;
import com.dfec.framework.datasourse.core.driver.DataSourceManager;
import com.dfec.framework.datasourse.core.driver.jdbc.JdbcDataSourceDriver;
import com.dfec.framework.datasourse.core.factory.DataSourceFactory;
import com.dfec.framework.datasourse.core.mapper.BaseDataHandleMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import javax.annotation.Resource;
import java.util.HashMap;
import java.util.Map;
/**
* 数据源路由配置
*
*/
@Configuration
public class DynamicDataSourceConfig {
@Resource
private DynamicDataSourceProperties properties;
/**
* 数据源配置
*
* @return
*/
@Bean
@ConfigurationProperties(prefix = "spring.datasource.druid")
public DataSourceProperties dataSourceProperties() {
return new DataSourceProperties();
}
/**
* 默认系统数据源
*
* @return
*/
@Bean
public JdbcDataSourceRouter defaultDataSource() {
DruidDataSource dataSource = DataSourceFactory.buildDruidDataSource(dataSourceProperties());
JdbcDataSourceRouter dynamic = new JdbcDataSourceRouter();
dynamic.setTargetDataSources(getDynamicDataSource());
// 设置默认数据源
dynamic.setDefaultTargetDataSource(dataSource);
JdbcDataSourceRouter.setDataSourceKey(BaseConstant.DEFAULT_DATA_SOURCE_ID);
JdbcDataSourceRouter.setDataSource(BaseConstant.DEFAULT_DATA_SOURCE_ID, dataSource);
return dynamic;
}
private Map<Object, Object> getDynamicDataSource() {
Map<String, DataSourceProperties> dataSourcePropertiesMap = this.properties.getDatasource();
Map<Object, Object> targetDataSources = new HashMap(dataSourcePropertiesMap.size());
dataSourcePropertiesMap.forEach((k, v) -> {
DruidDataSource druidDataSource = DataSourceFactory.buildDruidDataSource(v);
targetDataSources.put(k, druidDataSource);
JdbcDataSourceRouter.setDataSource(k, druidDataSource);
});
return targetDataSources;
}
/**
* JDBC 数据源驱动
*
* @param baseDataHandleMapper
* @return
*/
@Bean
public DataSourceDriver dataSourceDriver(BaseDataHandleMapper baseDataHandleMapper) {
return new JdbcDataSourceDriver(baseDataHandleMapper);
}
/**
* 数据源插件驱动
*
* @param jdbcDataSourceDriver
* @return
*/
@Bean
public DataSourceManager dataSourceDriverManager(DataSourceDriver jdbcDataSourceDriver) {
DataSourceManager driverManager = new DataSourceManager(jdbcDataSourceDriver);
return driverManager;
}
}
注意:这里回答我们上面的问题
再回顾一下上面三个问题
第一个、DataSourceManager.DATA_SOURCE_POOL_JDBC中的数据是怎么来的
回答:在配置文件中通过 JdbcDataSourceRouter.setDataSource(k, druidDataSource);进行设置的
java
public static void setDataSource(String dataSourceId, DataSource dataSource) {
DataSourceManager.DATA_SOURCE_POOL_JDBC.put(dataSourceId, dataSource);
}
而这对应的k和druidDataSource信息是从yml配置文件中读取进来的
第二个、本地线程中的数据源id是怎么塞进去的
回答:这里我们在测试代码的时候其实就已经看到了,通过手动去切换的,就这段代码,记得吧
java
@GetMapping("/test1")
public AjaxResult test(){
JdbcDataSourceRouter.setDataSourceKey("datasource1");
List<SysUserEntity> list = sysUserService.list();
JdbcDataSourceRouter.remove();
return AjaxResult.ok().put(list);
}
第三个、系统数据源是怎么定义的
回答:构建默认的数据源 DruidDataSource dataSource = DataSourceFactory.buildDruidDataSource(dataSourceProperties());
java
public static DruidDataSource buildDruidDataSource(DataSourceProperties properties) {
DruidDataSource druidDataSource = new DruidDataSource();
druidDataSource.setDriverClassName(properties.getDriverClassName());
druidDataSource.setUrl(properties.getUrl());
druidDataSource.setUsername(properties.getUsername());
druidDataSource.setPassword(properties.getPassword());
druidDataSource.setInitialSize(properties.getInitialSize());
druidDataSource.setMaxActive(properties.getMaxActive());
druidDataSource.setMinIdle(properties.getMinIdle());
druidDataSource.setMaxWait(properties.getMaxWait());
druidDataSource.setTimeBetweenEvictionRunsMillis(properties.getTimeBetweenEvictionRunsMillis());
druidDataSource.setMinEvictableIdleTimeMillis(properties.getMinEvictableIdleTimeMillis());
druidDataSource.setMaxEvictableIdleTimeMillis(properties.getMaxEvictableIdleTimeMillis());
druidDataSource.setValidationQuery(properties.getValidationQuery());
druidDataSource.setValidationQueryTimeout(properties.getValidationQueryTimeout());
druidDataSource.setTestOnBorrow(properties.isTestOnBorrow());
druidDataSource.setTestOnReturn(properties.isTestOnReturn());
druidDataSource.setPoolPreparedStatements(properties.isPoolPreparedStatements());
druidDataSource.setMaxOpenPreparedStatements(properties.getMaxOpenPreparedStatements());
druidDataSource.setSharePreparedStatements(properties.isSharePreparedStatements());
try {
druidDataSource.setFilters(properties.getFilters());
druidDataSource.init();
} catch (SQLException e) {
e.printStackTrace();
}
return druidDataSource;
}
查看这个方法defaultDataSource()
java
@Bean
public JdbcDataSourceRouter defaultDataSource() {
DruidDataSource dataSource = DataSourceFactory.buildDruidDataSource(dataSourceProperties());
JdbcDataSourceRouter dynamic = new JdbcDataSourceRouter();
//设置目标数据源 ,即动态数据源
dynamic.setTargetDataSources(getDynamicDataSource());
// 设置默认数据源
dynamic.setDefaultTargetDataSource(dataSource);
JdbcDataSourceRouter.setDataSourceKey(BaseConstant.DEFAULT_DATA_SOURCE_ID);
JdbcDataSourceRouter.setDataSource(BaseConstant.DEFAULT_DATA_SOURCE_ID, dataSource);
return dynamic;
}
private Map<Object, Object> getDynamicDataSource() {
//入口1,注意这里的入口是我们通过yml进行配置多数据源的
Map<String, DataSourceProperties> dataSourcePropertiesMap = this.properties.getDatasource();
Map<Object, Object> targetDataSources = new HashMap(dataSourcePropertiesMap.size());
dataSourcePropertiesMap.forEach((k, v) -> {
DruidDataSource druidDataSource = DataSourceFactory.buildDruidDataSource(v);
targetDataSources.put(k, druidDataSource);
JdbcDataSourceRouter.setDataSource(k, druidDataSource);
});
return targetDataSources;
}
下面我再附上通过注解动态切换数据源
通过注解动态切换数据源
java
package com.dfec.framework.datasourse.core.annotation;
import java.lang.annotation.Documented;
import java.lang.annotation.Inherited;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* 多数据源注解
*
* @date 2023-12-01 16:12:51
*/
@Target({java.lang.annotation.ElementType.METHOD, java.lang.annotation.ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
public @interface DataSource
{
String value() default "系统数据源";
}
java
package com.dfec.framework.datasourse.core.aspect;
import com.dfec.common.utils.str.StringUtils;
import com.dfec.framework.datasourse.core.annotation.DataSource;
import com.dfec.framework.datasourse.core.config.JdbcDataSourceRouter;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
import java.util.Objects;
/**
* 多数据源切面
*/
@Aspect
@Component
@Order(-2147483648)
public class DataSourceAspect {
protected Logger logger = LoggerFactory.getLogger(getClass());
@Pointcut("@annotation(com.dfec.framework.datasourse.core.annotation.DataSource) || @within(com.dfec.framework.datasourse.core.annotation.DataSource)")
public void dataSourcePointCut() {}
@Around("dataSourcePointCut()")
public Object around(ProceedingJoinPoint point) throws Throwable {
DataSource dataSource = getDataSource(point);
if (StringUtils.isNotNull(dataSource)) {
JdbcDataSourceRouter.setDataSourceKey(dataSource.value());
}
try {
return point.proceed();
} finally {
JdbcDataSourceRouter.remove();
this.logger.debug("clean datasource");
}
}
/**
* 获取需要切换的数据源
*/
public DataSource getDataSource(ProceedingJoinPoint point) {
MethodSignature signature = (MethodSignature) point.getSignature();
DataSource dataSource = AnnotationUtils.findAnnotation(signature.getMethod(), DataSource.class);
if (Objects.nonNull(dataSource)) {
return dataSource;
}
return AnnotationUtils.findAnnotation(signature.getDeclaringType(), DataSource.class);
}
}
总结
- 定义AbstractRoutingDataSource类继承抽象类AbstractRoutingDataSource,并实现了以下两个方法
determineCurrentLookupKey()方法每次获取当前数据源key的时候都是从当前threadLoad本地线程中去获取
determineTargetDataSource()方法通过从本地线程中获取到数据源id去切换对应的数据源
- 通过手动去设置数据源id,可以实现数据源的切换,我们这里只需要去开发一个注解简化切换
- 数据源信息主要存储在DataSourceManager.DATA_SOURCE_POOL_JDBC集合中,它是在项目启动的时候通过配置DynamicDataSourceConfig#defaultDataSource()的方法进行设置了动态数据源和默认的数据源信息
扩展
以上配置方式给我提供了入口,那么我们可以尝试将数据源结合表进行管理,这样可以实现动态的管理数据源了,篇幅有限,后面单独写吧