**多数据源动态切换 *分库分表***
参考 [AbstractRoutingDataSource动态数据源切换,AOP实现动态数据源切换](https://blog.csdn.net/u012881904/article/details/77449710)
案例原理:
主要是DynamicDataSource继承AbstractRoutingDataSource重写determineCurrentLookupKey进行了lookupkey的set,
利用AOP在业务里实现spring-jdbc的AbstractRoutingDataSource。
/**
* 数据源动态切换类
*/
public class DynamicDataSource extends AbstractRoutingDataSource {
@Override
protected Object determineCurrentLookupKey() {
return DbContextHolder.getDbKey(); //获取当前数据源
}
}
dataSource每次getConnection之前都要通过lookupkey获取指定的DataSource(这里的get(lookupkey)由别名获取到数据源,是因为在application-db.xml里注册了数据源bean)
/**
* 以下为org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource.class源码
*
* Retrieve the current target DataSource. Determines the
* {@link #determineCurrentLookupKey() current lookup key}, performs
* a lookup in the {@link #setTargetDataSources targetDataSources} map,
* falls back to the specified
* {@link #setDefaultTargetDataSource default target DataSource} if necessary.
* @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;
}
数据源配置:
${mysql.jdbc.url}
${common.mysql.jdbc.user}
${common.mysql.jdbc.password}
msd1
msd6
sld1
sld6
xxxxx
xxxxx_express
AOP通过用户id计算key值匹配配置文件里定义好的Datasource集合 得到具体数据源。 从而继续执行MyBatis后续分表和Sql操作。
在service层利用@Router的切面织入业务: 计算分库分表key,去setLookupkey的代码,每次检查到@Router就去切换数据源,执行数据操作。
service层加@Router注解
/**
*
* @param userId
* @param orderIds
* @return
*/
@Router
public Map getOrder(String userId, String[] orderIds) {
//do something
return null;
}
切面前置增强:
/**
* 切换到分库
*
* @param jp
* @return
* @throws NoSuchMethodException
* @throws RouterException
* @throws Throwable
*/
@Before("aopPoint()")
public Object doRoute(JoinPoint jp) throws NoSuchMethodException, RouterException {
Method method = getMethod(jp);
Router router = method.getAnnotation(Router.class);
String routeField = router.routerField();
Object[] args = jp.getArgs();
Signature signature = jp.getSignature();
MethodSignature methodSignature = (MethodSignature) signature;
String[] argsName = methodSignature.getParameterNames();
//根据缓存的userId和请求提供的进行比较,计算routeFieldValue。“计算逻辑”见下文
String routeFieldValue = getRouteFieldValue(args, argsName, routeField);
if (StringUtils.isNotEmpty(routeFieldValue)) {
dBRouter.doRoute(routeFieldValue, router);
} else {
log.error("分库分表字段为空,未切换数据源");
}
return true;
}
分库分表原理:
spring检查到aop组件进入切面处理代码DBRouterInterceptor
前置增强进行数据源切换:
根据当前用户id进行 doRoute,若参数有userId,以入参为准,
计算逻辑:
a.id转换为utf-8下Base64位的string,获取此string的哈希值的绝对值
b.再将此哈希值除以10000去余数得到整数值
c.配合数据源配置的路由数组例如mode=6*6, b步骤得到的整数值除以mode取余除以路由的库数 为计算出的dbIndex
d.b步骤得到的整数值除以路由的单库的表数取余为tbIndex
将dbIndex tbIndex全放入路由对象RouterInfo,格式化表路由tableIndex之后存入DbContexHolder(此处格式化是因为表名前缀一致,后缀_00递增)
再将库路由dbIndex存入DbContexHolder
*至此*路由全部计算出存入DbContexHolder。需要理解DbContexHolder装载的线程常量ThreadLocal特性
DbContexHolder的工作原理参见 “原理” 因为是继承了dataSource的AbstractRoutingDataSource才可以进行lookupkey的set操作。
完成库的路由计算,切换了数据源,然后就是MyBatis的分表。
ShardingInterceptor拦截器拦截Executor实际的sql执行操作,路由到DbContexHolder里指定的表。具体原理再看
一个多库多表的请求从路由到指定库、指定表即以上步骤。目前设定的是:执行完之后AOP后置增强会切回主数据源,异常也会切回主数据源。
小结:项目是水平分表,将一个大表分了几个字段一模一样的表。数据表里的seq实际是以 库数字后缀_表数字后缀_数字串 拼接进行唯一标识。数据这样分布式否均匀,还是取决于用户id的划分是否合理(计算逻辑是否合理)。
如果有查全量的数据,这样分好合数据吗?一个用户的数据会分到指定库指定表,如果有跨库业务怎么办。目前,日统计和周期统计都是单独task服务去做的。
垂直分表目前还没理解。待续
持续更新
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------