很多情况,我们确实需要在一个服务中访问多个数据源。虽然它让整体设计变的不那么优雅,但真实的世界确实需要它。比如,你的业务为两个比较大的客户服务,但你希望他们能够共用一套代码。
也就是说,你的代码刚开始没有考虑设计多租户这种功能,但后面又有这种蛋疼的需求。但还好不是爆炸式的租户增长。
除了引入一些分库分表组件, Spring 自身提供了AbstractRoutingDataSource的方式,让多数数据源的管理成为可能。其实分库分表组件使用上限制很多,你不得不首先梳理这座屎山,接下来还要忍受中间件对你的 SQL 的苛刻要求;反而是一些野路子,能够让代码的改动量尽量的减少。
心动不如行动。接下来,就让我们来看一下它的具体实现吧。
1.基本原理
多数据源能进行动态切换的核心就是spring底层提供了AbstractRoutingDataSource类进行数据源路由。AbstractRouting dataSource 实现了DataSource接口,所以我们可以将其直接注入到 DataSource 的属性上。
我们主要继承这个类,实现里面的方法determineCurrentLookupKey(),而此方法只需要返回一个数据库的名称即可。
比如,Controller通过拿到前端业务传递的数值,进行业务逻辑分发。它就可以手动设置当前请求的数据库标识,然后路由到正确的库表里面。
public class ARDTestController { | |
"test") | (|
public void chifeng(){ | |
//db-a 应该是上层传递下来的属性,我们可以把它放在 ThreadLocal 里 | |
DataSourceContextHolder.setDbKey("db-a"); | |
} | |
} |
那么当 sql语句 执行的时候,它如何知道自己需要切换到哪个数据源呢?是不是需要把db-a这个属性一直透传下去呢?
在 Java 中,可以使用ThreadLocal绑定这个 透传 的属性。像Spring的嵌套事务等实现的原理,也是基于ThreadLocal去运行的。所以,DataSourceContextHolder.本质上是一个操作ThreadLocal的类。
public class DataSourceContextHolder { | |
private static InheritableThreadLocal<String> dbKey = new InheritableThreadLocal<>(); | |
public static void setDbKey(String key){ | |
dbKey.set(key); | |
} | |
public static String getDbKey(){ | |
return dbKey.get(); | |
} | |
} |
2.配置代码
首先,我们自定义了配置文件的格式。如下面的代码,就配置了db-a和db-b两个数据库。
multi: | |
Dbs : | |
db-a: | |
driver-class-name: org.h.Driver | |
url: jdbc :h2:mem:dba;MODE=MYSQL; DATABASE _TO_UPPER=false; | |
db-b: | |
driver-class-name: org.h.Driver | |
url: jdbc:h:mem:dbb;MODE=MYSQL;DATABASE_TO_UPPER=false; |
然后,我们将它解析称properties。
"multi") | (prefix =|
public class DbsProperties { | |
private Map<String, Map<String, String>> dbs = new HashMap<>(); | |
public Map<String, Map<String, String>> getDbs() { | |
return dbs ; | |
} | |
public void setDbs(Map<String, Map<String, String>> dbs) { | |
this.dbs = dbs; | |
} | |
} |
接下来一步,需要配置整个应用所默认的数据源。如你所见,它的主要逻辑,就是在运行的时候,从ThreadLocal里取出提前设置的这个值。
public class DynamicDataSource extends AbstractRoutingDataSource { | |
protected Object determineCurrentLookupKey() { | |
return DataSourceContextHolder.getDbKey(); | |
} | |
} |
最后一步,设置整个项目中默认的DataSource。注意,我们生成DynamicDataSource之后,还需要提供targetDataSource和defaultTargetDataSource两个属性的值,才能够正常运行。
public class DynamicDataSourceConfiguration { | |
DbsProperties properties; | |
public DataSource dataSource(){ | |
DynamicDataSource dataSource = new DynamicDataSource(); | |
final Map<Object,Object> targetDataSource = getTargetDataSource(); | |
dataSource.setTargetDataSources(targetDataSource); | |
//TODO 默认数据库需要设置 | |
dataSource.setDefaultTargetDataSource(targetDataSource.values(). iterator ().next()); | |
return dataSource; | |
} | |
private Map<Object,Object> getTargetDataSource(){ | |
Map<Object,Object> dataSources = new HashMap<>(); | |
this.properties.getDbs().entrySet().stream() | |
. forEach (e->{ | |
DriverManagerDataSource dmd = new DriverManager DataSource(); | |
dmd.setUrl(e.getValue().get("url")); | |
dmd.setDriverClassName(e.getValue().get("driver-class-name")); | |
dataSources.put(e.getKey(),dmd); | |
}); | |
return dataSources; | |
} | |
} |
3.问题
通过以上简单的代码,就可以实现Spring简单的多数据源管理。但明显的,它还存在很多问题。
- 需要产品设计选择模式,进行业务切换。
- 前端可以采用放在localStroage的方式,保存属性,可使用拦截器方式将变量每次都传递。
- 后端每次请求,都需要带上目标db,可以采用放在ThreadLocal里的方式。但ThreadLocal有线程透传的问题,如果任务里开启了子线程,则变量不能共享。
- 由于表是动态选择的,所以 JPA 自动创建和update等模式,将不可用。不方便测试和单元测试,在测试接口的时候,也需要每次强制指定指向的库。
- 由于是修改数据源的模式,每次增加库,都需要重新启动上线才可以。如果要做到动态性,数据源销毁是个问题。
End
对于一个微服务来说,有很多默认的限制策略,比如,不同域之间的服务是不能共享一个数据库的。这些基本原则,把微服务整的清清爽爽,是一些基本的原则。
同理的,如果我们在设计开始,就给每一张表加上租户的字段ID,那么写代码的时候就顺畅的多。但是世界上没有这么多如果。
原则为何而存在?当然是为了让人去打破的。
编程只是工具,反正代码在自己手里,怎么玩,看需要,也看心情。条条大路通罗马,曲径通幽处,风光无限好。