路由组件构建方案(分库分表)V1
路由组件构建方案V1
实现效果:通过注解实现数据分散到不同库不同表的操作。
实现主要以下几部分:
- 数据源的配置和加载
- 数据源的动态切换
- 切点设置以及数据拦截
- 数据的插入
涉及的知识点:
- 分库分表相关概念
- 散列算法
- 数据源的切换
- AOP切面
- Mybatis拦截器
数据源的配置和加载
获取多个数据源我们肯定需要在yaml
或者properties
中进行配置。所以首先需要获取到配置信息;
定义配置文件中的库和表:
server:
port: 8080
# 多数据源路由配置
router:
jdbc:
datasource:
dbCount: 2
tbCount: 4
default: db00
routerKey: uId
list: db01,db02
db00:
driver-class-name: com.mysql.jdbc.Driver
url: jdbc:mysql://xxxxx:3306/xxxx?useUnicode=true
username: xxxx
password: 111111
db01:
driver-class-name: com.mysql.jdbc.Driver
url: jdbc:mysql://xxxxx:3306/xxxxx?useUnicode=true
username: xxxxx
password: 111111
db02:
driver-class-name: com.mysql.jdbc.Driver
url: jdbc:mysql://xxxxx:3306/xxxx?useUnicode=true
username: xxxxx
password: 111111
mybatis:
mapper-locations: classpath:/com/xbhog/mapper/*.xml
config-location: classpath:/config/mybatis-config.xml
为了实现并且使用自定义的数据源配置信息,启动开始的时候让SpringBoot定位位置。
首先类加载顺序:指定自动配置;
org.springframework.boot.autoconfigure.EnableAutoConfiguration=com.xbhog.db.router.config.DataSourceAutoConfig
针对读取这种自定义较大的信息配置,就需要使用到 org.springframework.context.EnvironmentAware
接口,来获取配置文件并提取需要的配置信息。
public class DataSourceAutoConfig implements EnvironmentAware {
@Override
public void setEnvironment(Environment environment){
......
}
}
属性配置中的前缀需要跟路由组件中的属性配置:
这里设置成什么,在配置文件中就要设置成对应名字
String prefix = "router.jdbc.datasource.";
根据其前缀获取对应的库数量dbCount
、表数量tbCount
以及数据源信息dataSource
;
//库的数量
dbCount = Integer.valueOf(environment.getProperty(prefix + "dbCount"));
//表的数量
tbCount = Integer.valueOf(environment.getProperty(prefix + "tbCount"));
//分库分表数据源
String dataSources = environment.getProperty(prefix + "list");
针对多数据源的存在,使用Map
进行存储:Map<String,Map<String,Object>> daraSources
;
for(String dbInfo : dataSources.split(",")){
Map<String,Object> dataSourceProps = PropertyUtil.handle(environment, prefix + dbInfo, Map.class);
dataSourceMap.put(dbInfo,dataSourceProps);
}
通过dataSource
方法实现数据源的实例化:把基于从配置信息中读取到的数据源信息,进行实例化创建。
将获得的信息放到DynamicDataSource
类(父类:DataSource
)中进行实例化(setTargetDataSources
,setDefaultTargetDataSource
);
将我们自定义的数据源加入到Spring
容器管理中。
//创建数据源
Map<Object, Object> targetDataSource = new HashMap<>();
//遍历数据源的key和value
for(String dbInfo : dataSourceMap.keySet()){
Map<String, Object> objectMap = dataSourceMap.get(dbInfo);
targetDataSource.put(dbInfo,new DriverManagerDataSource(objectMap.get("url").toString(),
objectMap.get("username").toString(),objectMap.get("password").toString()));
}
//这是数据源
DynamicDataSource dynamicDataSource = new DynamicDataSource();
dynamicDataSource.setTargetDataSources(targetDataSource);
//defaultDataSourceConfig的输入点
dynamicDataSource.setDefaultTargetDataSource(new DriverManagerDataSource(defaultDataSourceConfig.get("url").toString(),
defaultDataSourceConfig.get("username").toString(),defaultDataSourceConfig.get("password").toString()));
return dynamicDataSource;
到这里前置的配置都在spring中完成,后续是对数据的插入,也就是mybatis
的操作:包含库表的随机计算和数据拦截器的实现。
动态切换数据源
路由切换的实现通过AbstractRoutingDataSource
抽象类,该类充当了DataSource
的路由中介, 在运行的时候, 根据某种key值来动态切换到真正的DataSource
上。继承了AbstractDataSource
且AbstractDataSource
实现了DataSource
;
在AbstractRoutingDataSource
根据方法determineTargetDataSource
:
检索当前目标数据源。确定当前查找键,在
targetDataSources
映射中执行查找,必要时退回到指定的默认目标数据源。
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;
}
里面使用determineCurrentLookupKey
方法来确定当前查找的键(数据源key);
抽象方法
determineCurrentLookupKey()
返回DataSource
的key值,然后根据这个key从resolvedDataSources
这个map里取出对应的DataSource
,如果找不到,则用默认的resolvedDefaultDataSource
。
/**
*确定当前查找键。这通常用于检查线程绑定的事务上下文。
*允许任意键。返回的键需要匹配由resolveSpecifiedLookupKey方法解析的存储查找键类型
*/
@Nullable
protected abstract Object determineCurrentLookupKey();
所以我们只需要重写determineCurrentLookupKey
,指定我们切换数据源的名字即可;
public class DynamicDataSource extends AbstractRoutingDataSource {
@Override
protected Object determineCurrentLookupKey() {
return "db"+ DBContextHolder.getDBKey();
}
}
在这部分对应上了前面创建数据源的操作,实现的该DynamicDataSource
,并传入了默认数据源(setDefaultTargetDataSource
)和目标数据源(setTargetDataSources
);
自定义切点
前期数据源的配置和信息已经放到Spring
容器中,可随时使用;根据注解通过拦截器拦截方法中的数据。进行分库分表的操作,通过扰动函数进行计算,将结果保存到ThreadLocal
中,方便后续读取。
注解实现:
分库注解:首先设置三要素。
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE,ElementType.METHOD})
public @interface DBRouter {
/** 分库分表字段 */
String key() default "";
}
通过自定义切点@Around(**"aopPoint()&&@annotation(dbRouter)"**)
,实现使用注解的时候就拦截对应的值:
在环绕处理的时候,判断方法上注解是否对应有值,有的话通过注解传入的value
和方法传入的参数进行路由计算:
计算规则:
- 获取方法传入的参数
- 计算库表总数量:
dbCount*tbCount
- 计算idx:
**int **idx = (size -1) & (Key.hashCode() ^ (Key.hashCode() >>> 16))
- 简单说明:与运算标识符后面,通过混合高位和低位,增大随机性
**int **dbIdx = idx / dbCount() + 1
**int **tbIdx = idx - tbCount() * (dbIdx - 1)
通过上述操作,将计算的记过保存到ThreadLocal
中。
获取方法传入的参数:
private String getAttrValue(String dbKey, Object[] args) {
if(1 == args.length){
return args[0].toString();
}
String filedValue = null;
for(Object arg : args){
try{
if(StringUtils.isNotBlank(filedValue)){
break;
}
filedValue = BeanUtils.getProperty(arg,dbKey);
}catch (Exception e){
log.info("获取路由属性失败 attr:{}", dbKey,e);
}
}
return filedValue;
}
自定义拦截器
我们定义了Interceptor将拦截StatementHandler
(在SQL
语法构建处理拦截)中参数类型为Connection的prepare方法,具体需要深入mybatis
源码;
主要功能:在执行SQL语句前拦截,针对相关功能实现SQL的修改
在上述文章中主要是针对分库分表前做准备,下面才是决定数据入哪个库哪张表
通过StatementHandler
(MyBatis直接在数据库执行SQL脚本的对象)获取mappedStatement
(MappedStatement维护了一条<select|update|delete|insert>节点的封装),根据maperdStatement
获取自定义注解dbRouterStrategy
,判断是否进行分表操作;
Class<?> clazz = Class.forName(className);
DBRouterStrategy dbRouterStrategy = clazz.getAnnotation(DBRouterStrategy.class);
if (null == dbRouterStrategy || !dbRouterStrategy.splitTable()){
return invocation.proceed();
}
dbRouterStrategy
注解默认是false
不分表,直接进行数据的插入【更新】;
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE,ElementType.METHOD})
public @interface DBRouterStrategy {
boolean splitTable() default false;
}
如果分表注解存在或者分表参数是true
,则进行以下四步:
获取SQL
BoundSql
:表示动态生成的SQL
语句以及相应的参数信息。
//获取SQL
BoundSql boundSql = statementHandler.getBoundSql();
String sql = boundSql.getSql();
- 匹配SQL
通过正则匹配分割【insert/select/update】和表名,方便后续表名的拼接。
//替换SQL表名USER为USER_3;
Matcher matcher = pattern.matcher(sql);
String tableName = null;
if(matcher.find()){
tableName = matcher.group().trim();
}
- 拼接SQL
则通过反射修改SQL
语句,并且替换表名;其中filed.set()
将指定对象实参上由此field对象表示的字段设置为指定的新值。如果基础字段具有基元类型,则自动解开新值
assert null != tableName;
String replaceSQL = matcher.replaceAll(tableName + "_" + DBContextHolder.getTBKey());
//通过反射修改SQL语句
Field filed = boundSql.getClass().getDeclaredField("sql");
filed.setAccessible(true);
filed.set(boundSql,replaceSQL);
参考文章
https://www.cnblogs.com/aheizi/p/7071181.html
https://blog.csdn.net/wb1046329430/article/details/111501755
https://blog.csdn.net/supercmd/article/details/100042302
https://juejin.cn/post/6966241551810822151
路由组件构建方案(分库分表)V1的更多相关文章
- TSharding:用于蘑菇街交易平台的分库分表组件
tsharding TSharding is the simple sharding component used in mogujie trade platform. 分库分表业界方案 分库分表TS ...
- 分库分表后跨分片查询与Elastic Search
携程酒店订单Elastic Search实战:http://www.lvesu.com/blog/main/cms-610.html 为什么分库分表后不建议跨分片查询:https://www.jian ...
- mysql分库分表,做到永不迁移数据和避免热点
作者:老顾聊技术 搜云库技术团队 来源:https://www.toutiao.com/i6677459303055491597 一.前言 中大型项目中,一旦遇到数据量比较大,小伙伴应该都知道就 ...
- java 取模运算% 实则取余 简述 例子 应用在数据库分库分表
java 取模运算% 实则取余 简述 例子 应用在数据库分库分表 取模运算 求模运算与求余运算不同.“模”是“Mod”的音译,模运算多应用于程序编写中. Mod的含义为求余.模运算在数论和程序设计中 ...
- 001---mysql分库分表
mysql分库分表 一.整体的切分方式 1.分库分表:即数据的切分就是通过某种特定的条件,将我们存放在同一个数据库中的数据分散存放到多个数据库(主机)中,以达到分散单台设备负载的效果 2.数据的切分根 ...
- 分库分表技术演进&最佳实践
每个优秀的程序员和架构师都应该掌握分库分表,这是我的观点. 移动互联网时代,海量的用户每天产生海量的数量,比如: 用户表 订单表 交易流水表 以支付宝用户为例,8亿:微信用户更是10亿.订单表更夸张, ...
- 基于AOP和HashMap原理学习,开发Mysql分库分表路由组件!
作者:小傅哥 博客:https://bugstack.cn 沉淀.分享.成长,让自己和他人都能有所收获! 一.前言 什么?Java 面试就像造火箭 单纯了! 以前我也一直想 Java 面试就好好面试呗 ...
- 数据库分库分表(sharding)系列(五) 一种支持自由规划无须数据迁移和修改路由代码的Sharding扩容方案
作为一种数据存储层面上的水平伸缩解决方案,数据库Sharding技术由来已久,很多海量数据系统在其发展演进的历程中都曾经历过分库分表的Sharding改造阶段.简单地说,Sharding就是将原来单一 ...
- DB 分库分表(5):一种支持自由规划无须数据迁移和修改路由代码的 Sharding 扩容方案
作为一种数据存储层面上的水平伸缩解决方案,数据库Sharding技术由来已久,很多海量数据系统在其发展演进的历程中都曾经历过分库分表的Sharding改造阶段.简单地说,Sharding就是将原来单一 ...
随机推荐
- 点击>>>解锁Apache Hadoop Meetup 2021!
" 10月16日,属于开源发烧友的狂欢日来啦! Apache Hadoop Meetup 2021 开源大数据行业交流盛会盛大开启!让我们相约北京,一起嗨翻初秋~ 在当今信息化时代,逐渐成熟 ...
- Luogu3870 [TJOI2009]开关 (分块)
线段树做法很简单,但分块好啊 #include <iostream> #include <cstdio> #include <cstring> #include & ...
- Luogu1856 [USACO5.5]矩形周长Picture (线段树扫描线)
对于横轴,加上与上一次扫描的差值:对于竖轴,加上高度差与区间内不相交线段\(*2\)的积: 难点在pushdown,注意维护覆盖关系.再就注意负数 #include <iostream> ...
- 解决使用 Eruda 绑定 dom 未在指定位置显示问题
前言 开发项目中,使用到 Eruda 打印控制台信息显示 文档:https://github.com/liriliri/eruda 安装 Eruda npm install eruda --save ...
- 客户流失?来看看大厂如何基于spark+机器学习构建千万数据规模上的用户留存模型 ⛵
作者:韩信子@ShowMeAI 大数据技术 ◉ 技能提升系列:https://www.showmeai.tech/tutorials/84 行业名企应用系列:https://www.showmeai. ...
- js运算符和逻辑分支
运算符 1.拼接运算符:+,加号两边只要有一边出现字符串就是拼接 2.算术运算符 如:2+3: 3.赋值运算符+=,-=,/=,*= 4.关系运算符>,<,==,=== != !== ! ...
- Excel 文本函数(一):LEFT、RIGHT 和 MID
文本函数 LEFT.RIGHT 以及 MID 是非常常用的,它们用于截取文本字符串. LEFT(text, [num_chars]) 是从文本字符串的左边开始截取:RIGHT(text, [num_c ...
- Excel 统计函数(五):MINIFS 和 MAXIFS
MINIFS [语法]MINIFS(min_range, criteria_range1, criteria1, [criteria_range2, criteria2], ...) [参数] min ...
- Spring 05: 用DI(依赖注入)优化Spring接管下的三层项目架构
背景 用注解改造前面Spring博客集里(指 Spring 02)Spring接管下的三层项目架构 对前面Spring博客集里(指 Spring 04)@Controller + @Service + ...
- Excelize 发布 2.6.1 版本,支持工作簿加密
Excelize 是 Go 语言编写的用于操作 Office Excel 文档基础库,基于 ECMA-376,ISO/IEC 29500 国际标准.可以使用它来读取.写入由 Microsoft Exc ...