
定义路由注解
定义:
@Documented@Retention(RetentionPolicy.RUNTIME)@Target({ElementType.TYPE, ElementType.METHOD})public @interface DBRouter {String key() default "";}
使用:
@Mapperpublic interface IUserDao {@DBRouter(key = "userId")User queryUserInfoByUserId(User req);@DBRouter(key = "userId")void insertUser(User req);}
- 首先我们需要自定义一个注解,用于放置在需要被数据库路由的方法上。
它的使用方式是通过方法配置注解,就可以被我们指定的 AOP 切面进行拦截,拦截后进行相应的数据库路由计算和判断,并切换到相应的操作数据源上。
配置多数据源
获取多数据源配置——> 放入 dataSourceMap中
配置多数据源
数据源切换
在结合 SpringBoot 开发的 Starter 中,需要提供一个 DataSource 的实例化对象,那么这个对象我们就放在 DataSourceAutoConfig 来实现,并且这里提供的数据源是可以动态变换的,也就是支持动态切换数据源。
这里是一个简化的创建案例,把基于从配置信息中读取到的数据源信息,进行实例化创建。
- 数据源创建完成后存放到 DynamicDataSource 中,它是一个继承了 AbstractRoutingDataSource 的实现类,这个类里可以存放和读取相应的具体调用的数据源信息。
切面拦截
在 AOP 的切面拦截中需要完成;数据库路由计算、扰动函数加强散列、计算库表索引、设置到 ThreadLocal 传递数据源,整体案例代码如下:
@Around("aopPoint() && @annotation(dbRouter)")public Object doRouter(ProceedingJoinPoint jp, DBRouter dbRouter) throws Throwable {String dbKey = dbRouter.key();if (StringUtils.isBlank(dbKey)) throw new RuntimeException("annotation DBRouter key is null!");// 计算路由String dbKeyAttr = getAttrValue(dbKey, jp.getArgs());int size = dbRouterConfig.getDbCount() * dbRouterConfig.getTbCount();// 扰动函数int idx = (size - 1) & (dbKeyAttr.hashCode() ^ (dbKeyAttr.hashCode() >>> 16));// 库表索引int dbIdx = idx / dbRouterConfig.getTbCount() + 1;int tbIdx = idx - dbRouterConfig.getTbCount() * (dbIdx - 1);// 设置到 ThreadLocalDBContextHolder.setDBKey(String.format("%02d", dbIdx));DBContextHolder.setTBKey(String.format("%02d", tbIdx));logger.info("数据库路由 method:{} dbIdx:{} tbIdx:{}", getMethod(jp).getName(), dbIdx, tbIdx);// 返回结果try {return jp.proceed();} finally {DBContextHolder.clearDBKey();DBContextHolder.clearTBKey();}}
- 简化的核心逻辑实现代码如上,首先我们提取了库表乘积的数量,把它当成 HashMap 一样的长度进行使用。
- 接下来使用和 HashMap 一样的扰动函数逻辑,让数据分散的更加散列。
- 当计算完总长度上的一个索引位置后,还需要把这个位置折算到库表中,看看总体长度的索引因为落到哪个库哪个表。
- 最后是把这个计算的索引信息存放到 ThreadLocal 中,用于传递在方法调用过程中可以提取到索引信息。 ```java package cn.bugstack.middleware.db.router;
import cn.bugstack.middleware.db.router.annotation.DBRouter; import cn.bugstack.middleware.db.router.strategy.IDBRouterStrategy; import org.apache.commons.beanutils.BeanUtils; import org.apache.commons.lang.StringUtils; import org.aspectj.lang.JoinPoint; import org.aspectj.lang.ProceedingJoinPoint; import org.aspectj.lang.Signature; 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 java.lang.reflect.Method;
/**
- @description: 数据路由切面,通过自定义注解的方式,拦截被切面的方法,进行数据库路由
- @author: 小傅哥,微信:fustack
- @date: 2021/9/22
- @github: https://github.com/fuzhengwei
@Copyright: 公众号:bugstack虫洞栈 | 博客:https://bugstack.cn - 沉淀、分享、成长,让自己和他人都能有所收获! */ @Aspect public class DBRouterJoinPoint {
private Logger logger = LoggerFactory.getLogger(DBRouterJoinPoint.class);
private DBRouterConfig dbRouterConfig;
private IDBRouterStrategy dbRouterStrategy;
public DBRouterJoinPoint(DBRouterConfig dbRouterConfig, IDBRouterStrategy dbRouterStrategy) {
this.dbRouterConfig = dbRouterConfig;this.dbRouterStrategy = dbRouterStrategy;
} //注解的路径 @Pointcut(“@annotation(cn.bugstack.middleware.db.router.annotation.DBRouter)”) public void aopPoint() { }
/**
- 所有需要分库分表的操作,都需要使用自定义注解进行拦截,拦截后读取方法中的入参字段,根据字段进行路由操作。
- dbRouter.key() 确定根据哪个字段进行路由
- getAttrValue 根据数据库路由字段,从入参中读取出对应的值。比如路由 key 是 uId,那么就从入参对象 Obj 中获取到 uId 的值。
- dbRouterStrategy.doRouter(dbKeyAttr) 路由策略根据具体的路由值进行处理
- 路由处理完成比,就是放行。 jp.proceed();
- 最后 dbRouterStrategy 需要执行 clear 因为这里用到了 ThreadLocal 需要手动清空。关于 ThreadLocal 内存泄漏介绍 https://t.zsxq.com/027QF2fae */ @Around(“aopPoint() && @annotation(dbRouter)”) public Object doRouter(ProceedingJoinPoint jp, DBRouter dbRouter) throws Throwable { String dbKey = dbRouter.key(); if (StringUtils.isBlank(dbKey) && StringUtils.isBlank(dbRouterConfig.getRouterKey())) { throw new RuntimeException(“annotation DBRouter key is null!”); } dbKey = StringUtils.isNotBlank(dbKey) ? dbKey : dbRouterConfig.getRouterKey(); // 路由属性 String dbKeyAttr = getAttrValue(dbKey, jp.getArgs()); // 路由策略 dbRouterStrategy.doRouter(dbKeyAttr); // 返回结果 try { return jp.proceed(); } finally { dbRouterStrategy.clear(); } }
private Method getMethod(JoinPoint jp) throws NoSuchMethodException { Signature sig = jp.getSignature(); MethodSignature methodSignature = (MethodSignature) sig; return jp.getTarget().getClass().getMethod(methodSignature.getName(), methodSignature.getParameterTypes()); }
public String getAttrValue(String attr, Object[] args) { if (1 == args.length) {
Object arg = args[0];if (arg instanceof String) {return arg.toString();}
}
String filedValue = null; for (Object arg : args) {
try {if (StringUtils.isNotBlank(filedValue)) {break;}filedValue = BeanUtils.getProperty(arg, attr);} catch (Exception e) {logger.error("获取路由属性值失败 attr:{}", attr, e);}
} return filedValue; }
}
<a name="HLmwa"></a>### Mybatis 拦截器处理分表- 最开始考虑直接在Mybatis对应的表 INSERT INTO user_strategy_export**_${tbIdx}** 添加字段的方式处理分表。但这样看上去并不优雅,不过也并不排除这种使用方式,仍然是可以使用的。- 那么我们可以基于 Mybatis 拦截器进行处理,通过拦截 SQL 语句动态修改添加分表信息,再设置回 Mybatis 执行 SQL 中。- 此外再完善一些分库分表路由的操作,比如配置默认的分库分表字段以及单字段入参时默认取此字段作为路由字段。cn.bugstack.middleware.db.router.dynamic.DynamicMybatisPlugin```java@Intercepts({@Signature(type = StatementHandler.class, method = "prepare", args = {Connection.class, Integer.class})})public class DynamicMybatisPlugin implements Interceptor {private Pattern pattern = Pattern.compile("(from|into|update)[\\s]{1,}(\\w{1,})", Pattern.CASE_INSENSITIVE);@Overridepublic Object intercept(Invocation invocation) throws Throwable {// 获取StatementHandlerStatementHandler statementHandler = (StatementHandler) invocation.getTarget();MetaObject metaObject = MetaObject.forObject(statementHandler, SystemMetaObject.DEFAULT_OBJECT_FACTORY, SystemMetaObject.DEFAULT_OBJECT_WRAPPER_FACTORY, new DefaultReflectorFactory());MappedStatement mappedStatement = (MappedStatement) metaObject.getValue("delegate.mappedStatement");// 获取自定义注解判断是否进行分表操作String id = mappedStatement.getId();String className = id.substring(0, id.lastIndexOf("."));Class<?> clazz = Class.forName(className);DBRouterStrategy dbRouterStrategy = clazz.getAnnotation(DBRouterStrategy.class);if (null == dbRouterStrategy || !dbRouterStrategy.splitTable()){return invocation.proceed();}// 获取SQLBoundSql boundSql = statementHandler.getBoundSql();String sql = boundSql.getSql();// 替换SQL表名 USER 为 USER_03Matcher matcher = pattern.matcher(sql);String tableName = null;if (matcher.find()) {tableName = matcher.group().trim();}assert null != tableName;String replaceSql = matcher.replaceAll(tableName + "_" + DBContextHolder.getTBKey());// 通过反射修改SQL语句Field field = boundSql.getClass().getDeclaredField("sql");field.setAccessible(true);field.set(boundSql, replaceSql);return invocation.proceed();}}
yml 中自动配置config位置
package cn.bugstack.middleware.db.router.config;import cn.bugstack.middleware.db.router.DBRouterConfig;import cn.bugstack.middleware.db.router.DBRouterJoinPoint;import cn.bugstack.middleware.db.router.dynamic.DynamicDataSource;import cn.bugstack.middleware.db.router.dynamic.DynamicMybatisPlugin;import cn.bugstack.middleware.db.router.strategy.IDBRouterStrategy;import cn.bugstack.middleware.db.router.strategy.impl.DBRouterStrategyHashCode;import cn.bugstack.middleware.db.router.util.PropertyUtil;import org.apache.ibatis.plugin.Interceptor;import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;import org.springframework.boot.context.properties.EnableConfigurationProperties;import org.springframework.boot.jdbc.DataSourceBuilder;import org.springframework.context.EnvironmentAware;import org.springframework.context.annotation.Bean;import org.springframework.context.annotation.Configuration;import org.springframework.core.env.Environment;import org.springframework.jdbc.datasource.DataSourceTransactionManager;import org.springframework.jdbc.datasource.DriverManagerDataSource;import org.springframework.transaction.PlatformTransactionManager;import org.springframework.transaction.support.TransactionTemplate;import javax.sql.DataSource;import java.sql.DriverManager;import java.sql.SQLException;import java.util.HashMap;import java.util.Map;/*** @description: 数据源配置解析* @author: 小傅哥,微信:fustack* @date: 2021/9/22* @github: https://github.com/fuzhengwei* @Copyright: 公众号:bugstack虫洞栈 | 博客:https://bugstack.cn - 沉淀、分享、成长,让自己和他人都能有所收获!*/@Configurationpublic class DataSourceAutoConfig implements EnvironmentAware {/*** 数据源配置组*/private Map<String, Map<String, Object>> dataSourceMap = new HashMap<>();/*** 默认数据源配置*/private Map<String, Object> defaultDataSourceConfig;/*** 分库数量*/private int dbCount;/*** 分表数量*/private int tbCount;/*** 路由字段*/private String routerKey;@Bean(name = "db-router-point")@ConditionalOnMissingBeanpublic DBRouterJoinPoint point(DBRouterConfig dbRouterConfig, IDBRouterStrategy dbRouterStrategy) {return new DBRouterJoinPoint(dbRouterConfig, dbRouterStrategy);}@Beanpublic DBRouterConfig dbRouterConfig() {return new DBRouterConfig(dbCount, tbCount, routerKey);}@Beanpublic Interceptor plugin() {return new DynamicMybatisPlugin();}@Beanpublic DataSource dataSource() {// 创建数据源Map<Object, Object> targetDataSources = new HashMap<>();for (String dbInfo : dataSourceMap.keySet()) {Map<String, Object> objMap = dataSourceMap.get(dbInfo);targetDataSources.put(dbInfo, new DriverManagerDataSource(objMap.get("url").toString(), objMap.get("username").toString(), objMap.get("password").toString()));}// 设置数据源DynamicDataSource dynamicDataSource = new DynamicDataSource();dynamicDataSource.setTargetDataSources(targetDataSources);dynamicDataSource.setDefaultTargetDataSource(new DriverManagerDataSource(defaultDataSourceConfig.get("url").toString(), defaultDataSourceConfig.get("username").toString(), defaultDataSourceConfig.get("password").toString()));return dynamicDataSource;}@Beanpublic IDBRouterStrategy dbRouterStrategy(DBRouterConfig dbRouterConfig) {return new DBRouterStrategyHashCode(dbRouterConfig);}@Beanpublic TransactionTemplate transactionTemplate(DataSource dataSource) {DataSourceTransactionManager dataSourceTransactionManager = new DataSourceTransactionManager();dataSourceTransactionManager.setDataSource(dataSource);TransactionTemplate transactionTemplate = new TransactionTemplate();transactionTemplate.setTransactionManager(dataSourceTransactionManager);transactionTemplate.setPropagationBehaviorName("PROPAGATION_REQUIRED");return transactionTemplate;}@Overridepublic void setEnvironment(Environment environment) {String prefix = "mini-db-router.jdbc.datasource.";dbCount = Integer.valueOf(environment.getProperty(prefix + "dbCount"));tbCount = Integer.valueOf(environment.getProperty(prefix + "tbCount"));routerKey = environment.getProperty(prefix + "routerKey");// 分库分表数据源String dataSources = environment.getProperty(prefix + "list");assert dataSources != null;for (String dbInfo : dataSources.split(",")) {Map<String, Object> dataSourceProps = PropertyUtil.handle(environment, prefix + dbInfo, Map.class);dataSourceMap.put(dbInfo, dataSourceProps);}// 默认数据源String defaultData = environment.getProperty(prefix + "default");defaultDataSourceConfig = PropertyUtil.handle(environment, prefix + defaultData, Map.class);}}
如何进行数据源动态切换
/*** @description: 动态数据源获取,每当切换数据源,都要从这个里面进行获取* @author: 小傅哥,微信:fustack* @date: 2021/9/22* @github: https://github.com/fuzhengwei* @Copyright: 公众号:bugstack虫洞栈 | 博客:https://bugstack.cn - 沉淀、分享、成长,让自己和他人都能有所收获!*/public class DynamicDataSource extends AbstractRoutingDataSource {@Overrideprotected Object determineCurrentLookupKey() {return "db" + DBContextHolder.getDBKey();}}
