共计 8543 个字符,预计需要花费 22 分钟才能阅读完成。
作者:小傅哥
博客:https://bugstack.cn
积淀、分享、成长,让本人和别人都能有所播种!😄
一、前言
什么?Java 面试就像造火箭🚀
单纯了! 以前我也始终想 Java 面试就好好面试呗,嘎哈么 总考一些工作中也用不到的玩意,会用 Spring
、MyBatis
、Dubbo
、MQ
,把业务需要实现了不就行了!
但当工作几年后,须要晋升本人 ( 要加钱 ) 的时候,居然开始感觉本人只是一个调用 API 攒接口的工具人。没有常识宽度,没有技术纵深,也想不进去更没有意识,把日常开发的业务代码中通用的共性逻辑提炼进去,开发成专用的组件,更没有去思考日常应用的一些组件是用什么技术实现的。
所以有时候你说面试如同就是在造火箭,这些技术日常基本用不到,其实很多时候不是这个技术用不到,而是因为你没用 ( 嗯,以前我也没用 )。当你有这个想法想冲破本人的薪资待遇瓶颈时,就须要去 理解理解必备的数据结构
、 学习学习 Java 的算法逻辑
、 相熟相熟通用的设计模式
、再联合像 Spring、ORM、RPC,这样的源码实现逻辑,把相应的技术计划赋能到本人的日常业务开发中,把共性的问题用聚焦和提炼的形式进行解决,这些才是你在 CRUD 之外的能力体现( 加薪筹码)。
怎么? 如同听下来有情理,那么举个 栗子 ,来一场 数据库路由
的需要剖析和逻辑实现!
二、需要剖析
如果要做一个数据库路由,都须要做什么技术点?
首先咱们要晓得为什么要用分库分表,其实就是因为业务体量较大,数据增长较快,所以须要把用户数据拆分到不同的库表中去,加重数据库压力。
分库分表操作次要有垂直拆分和程度拆分:
- 垂直拆分:指依照业务将表进行分类,散布到不同的数据库上,这样也就将数据的压力分担到不同的库下面。最终一个数据库由很多表的形成,每个表对应着不同的业务,也就是专库专用。
- 程度拆分:如果垂直拆分后遇到单机瓶颈,能够应用程度拆分。绝对于垂直拆分的区别是:垂直拆分是把不同的表拆到不同的数据库中,而程度拆分是把同一个表拆到不同的数据库中。如:user_001、user_002
而本章节咱们要实现的也是程度拆分的路由设计,如图 1-1
那么,这样的一个数据库路由设计要包含哪些技术知识点呢?
- 是对于 AOP 切面拦挡的应用,这是因为须要给应用数据库路由的办法做上标记,便于解决分库分表逻辑。
- 数据源的切换操作,既然有分库那么就会波及在多个数据源间进行链接切换,以便把数据调配给不同的数据库。
- 数据库表寻址操作,一条数据调配到哪个数据库,哪张表,都须要进行索引计算。在办法调用的过程中最终通过 ThreadLocal 记录。
- 为了能让数据平均的调配到不同的库表中去,还须要思考如何进行数据散列的操作,不能分库分表后,让数据都集中在某个库的某个表,这样就失去了分库分表的意义。
综上,能够看到在数据库和表的数据结构下实现数据寄存,我须要用到的技术包含:AOP
、数据源切换
、 散列算法
、 哈希寻址
、ThreadLoca
l 以及SpringBoot 的 Starter 开发方式
等技术。而像 哈希散列
、 寻址
、 数据寄存
,其实这样的技术与 HashMap 有太多相似之处, 那么学完源码造火箭的机会来了 如果你有过深入分析和学习过 HashMap 源码、Spring 源码、中间件开发,那么在设计这样的数据库路由组件时肯定会有很多思路的进去。 接下来咱们一起尝试下从源码学习到造火箭!
三、技术调研
在 JDK 源码中,蕴含的数据结构设计有:数组、链表、队列、栈、红黑树,具体的实现有 ArrayList、LinkedList、Queue、Stack,而这些在数据寄存都是顺序存储,并没有用到哈希索引的形式进行解决。而 HashMap、ThreadLocal,两个性能则用了哈希索引、散列算法以及在数据收缩时候的拉链寻址和凋谢寻址,所以咱们要剖析和借鉴的也会集中在这两个性能上。
1. ThreadLocal
@Test
public void test_idx() {
int hashCode = 0;
for (int i = 0; i < 16; i++) {
hashCode = i * 0x61c88647 + 0x61c88647;
int idx = hashCode & 15;
System.out.println("斐波那契散列:" + idx + "一般散列:" + (String.valueOf(i).hashCode() & 15));
}
}
斐波那契散列:7 一般散列:0
斐波那契散列:14 一般散列:1
斐波那契散列:5 一般散列:2
斐波那契散列:12 一般散列:3
斐波那契散列:3 一般散列:4
斐波那契散列:10 一般散列:5
斐波那契散列:1 一般散列:6
斐波那契散列:8 一般散列:7
斐波那契散列:15 一般散列:8
斐波那契散列:6 一般散列:9
斐波那契散列:13 一般散列:15
斐波那契散列:4 一般散列:0
斐波那契散列:11 一般散列:1
斐波那契散列:2 一般散列:2
斐波那契散列:9 一般散列:3
斐波那契散列:0 一般散列:4
- 数据结构:散列表的数组构造
- 散列算法:斐波那契(Fibonacci)散列法
- 寻址形式:Fibonacci 散列法能够让数据更加扩散,在产生数据碰撞时进行凋谢寻址,从碰撞节点向后寻找地位进行寄存元素。公式:
f(k) = ((k * 2654435769) >> X) << Y 对于常见的 32 位整数而言,也就是 f(k) = (k * 2654435769) >> 28
,黄金分割点:(√5 - 1) / 2 = 0.6180339887
1.618:1 == 1:0.618
- 学到什么:能够参考寻址形式和散列算法,但这种数据结构与要设计实现作用到数据库上的构造相差较大,不过 ThreadLocal 能够用于寄存和传递数据索引信息。
2. HashMap
public static int disturbHashIdx(String key, int size) {return (size - 1) & (key.hashCode() ^ (key.hashCode() >>> 16));
}
- 数据结构:哈希桶数组 + 链表 + 红黑树
- 散列算法:扰动函数、哈希索引,能够让数据更加散列的散布
- 寻址形式:通过拉链寻址的形式解决数据碰撞,数据寄存时会进行索引地址,遇到碰撞产生数据链表,在肯定容量超过 8 个元素进行扩容或者树化。
- 学到什么:能够把散列算法、寻址形式都使用到数据库路由的设计实现中,还有整个数组 + 链表的形式其实库 + 表的形式也有类似之处。
四、设计实现
1. 定义路由注解
定义
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE, ElementType.METHOD})
public @interface DBRouter {String key() default "";
}
应用
@Mapper
public interface IUserDao {@DBRouter(key = "userId")
User queryUserInfoByUserId(User req);
@DBRouter(key = "userId")
void insertUser(User req);
}
- 首先咱们须要自定义一个注解,用于搁置在须要被数据库路由的办法上。
- 它的应用形式是通过办法配置注解,就能够被咱们指定的 AOP 切面进行拦挡,拦挡后进行相应的数据库路由计算和判断,并切换到相应的操作数据源上。
2. 解析路由配置
- 以上就是咱们实现完数据库路由组件后的一个数据源配置,在分库分表下的数据源应用中,都须要反对多数据源的信息配置,这样能力满足不同需要的扩大。
- 对于这种自定义较大的信息配置,就须要应用到
org.springframework.context.EnvironmentAware
接口,来获取配置文件并提取须要的配置信息。
数据源配置提取
@Override
public void setEnvironment(Environment environment) {
String prefix = "router.jdbc.datasource.";
dbCount = Integer.valueOf(environment.getProperty(prefix + "dbCount"));
tbCount = Integer.valueOf(environment.getProperty(prefix + "tbCount"));
String dataSources = environment.getProperty(prefix + "list");
for (String dbInfo : dataSources.split(",")) {Map<String, Object> dataSourceProps = PropertyUtil.handle(environment, prefix + dbInfo, Map.class);
dataSourceMap.put(dbInfo, dataSourceProps);
}
}
- prefix,是数据源配置的结尾信息,你能够自定义须要的结尾内容。
- dbCount、tbCount、dataSources、dataSourceProps,都是对配置信息的提取,并存放到 dataSourceMap 中便于后续应用。
3. 数据源切换
在联合 SpringBoot 开发的 Starter 中,须要提供一个 DataSource 的实例化对象,那么这个对象咱们就放在 DataSourceAutoConfig 来实现,并且这里提供的数据源是能够动静变换的,也就是反对动静切换数据源。
创立数据源
@Bean
public 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);
return dynamicDataSource;
}
- 这里是一个简化的创立案例,把基于从配置信息中读取到的数据源信息,进行实例化创立。
- 数据源创立实现后寄存到
DynamicDataSource
中,它是一个继承了 AbstractRoutingDataSource 的实现类,这个类里能够寄存和读取相应的具体调用的数据源信息。
4. 切面拦挡
在 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);
// 设置到 ThreadLocal
DBContextHolder.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 中,用于传递在办法调用过程中能够提取到索引信息。
5. 测试验证
5.1 库表创立
create database `bugstack_01`;
DROP TABLE user_01;
CREATE TABLE user_01 (id bigint NOT NULL AUTO_INCREMENT COMMENT '自增 ID', userId varchar(9) COMMENT '用户 ID', userNickName varchar(32) COMMENT '用户昵称', userHead varchar(16) COMMENT '用户头像', userPassword varchar(64) COMMENT '用户明码', createTime datetime COMMENT '创立工夫', updateTime datetime COMMENT '更新工夫', PRIMARY KEY (id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8;
DROP TABLE user_02;
CREATE TABLE user_02 (id bigint NOT NULL AUTO_INCREMENT COMMENT '自增 ID', userId varchar(9) COMMENT '用户 ID', userNickName varchar(32) COMMENT '用户昵称', userHead varchar(16) COMMENT '用户头像', userPassword varchar(64) COMMENT '用户明码', createTime datetime COMMENT '创立工夫', updateTime datetime COMMENT '更新工夫', PRIMARY KEY (id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8;
DROP TABLE user_03;
CREATE TABLE user_03 (id bigint NOT NULL AUTO_INCREMENT COMMENT '自增 ID', userId varchar(9) COMMENT '用户 ID', userNickName varchar(32) COMMENT '用户昵称', userHead varchar(16) COMMENT '用户头像', userPassword varchar(64) COMMENT '用户明码', createTime datetime COMMENT '创立工夫', updateTime datetime COMMENT '更新工夫', PRIMARY KEY (id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8;
DROP TABLE user_04;
CREATE TABLE user_04 (id bigint NOT NULL AUTO_INCREMENT COMMENT '自增 ID', userId varchar(9) COMMENT '用户 ID', userNickName varchar(32) COMMENT '用户昵称', userHead varchar(16) COMMENT '用户头像', userPassword varchar(64) COMMENT '用户明码', createTime datetime COMMENT '创立工夫', updateTime datetime COMMENT '更新工夫', PRIMARY KEY (id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8;
- 创立雷同表构造的多个库存信息,bugstack_01、bugstack_02
5.2 语句配置
<select id="queryUserInfoByUserId" parameterType="cn.bugstack.middleware.test.infrastructure.po.User"
resultType="cn.bugstack.middleware.test.infrastructure.po.User">
SELECT id, userId, userNickName, userHead, userPassword, createTime
FROM user_${tbIdx}
where userId = #{userId}
</select>
<insert id="insertUser" parameterType="cn.bugstack.middleware.test.infrastructure.po.User">
insert into user_${tbIdx} (id, userId, userNickName, userHead, userPassword,createTime, updateTime)
values (#{id},#{userId},#{userNickName},#{userHead},#{userPassword},now(),now())
</insert>
- 在 MyBatis 的语句应用上,惟一变动的须要在表名前面增加一个占位符,
${tbIdx}
用于写入以后的表 ID。
5.3 注解配置
@DBRouter(key = "userId")
User queryUserInfoByUserId(User req);
@DBRouter(key = "userId")
void insertUser(User req);
- 在须要应用分库分表的办法上增加注解,增加注解后这个办法就会被 AOP 切面治理。
5.4 单元测试
22:38:20.067 INFO 19900 --- [main] c.b.m.db.router.DBRouterJoinPoint : 数据库路由 method:queryUserInfoByUserId dbIdx:2 tbIdx:3
22:38:20.594 INFO 19900 --- [main] cn.bugstack.middleware.test.ApiTest : 测试后果:{"createTime":1615908803000,"id":2,"userHead":"01_50","userId":"980765512","userNickName":"小傅哥","userPassword":"123456"}
22:38:20.620 INFO 19900 --- [extShutdownHook] o.s.s.concurrent.ThreadPoolTaskExecutor : Shutting down ExecutorService 'applicationTaskExecutor'1
- 以上就是咱们应用本人的数据库路由组件执行时的一个日志信息,能够看到这里蕴含了路由操作,在 2 库 3 表:
数据库路由 method:queryUserInfoByUserId dbIdx:2 tbIdx:3
五、总结
综上 就是咱们从 HashMap、ThreadLocal、Spring 等源码学习中理解到技术外在原理,并把这样的技术用在一个数据库路由设计上。如果没有经验过这些总被说成 造火箭 的技术积淀,那么简直也不太可能顺利开发出一个这样一个中间件,所有很多时候基本不是技术没用,而是本人没用上没机会用而已。不要总惦记那一片片反复的 CRUD,看看还有哪些常识是真的能够晋升集体能力的!参考资料:https://codechina.csdn.net/MiddlewareDesign
六、系列举荐
- 《手撸 Spring》PDF,全书 260 页 6.5 万字,整顿分享
- 开发一个分布式 IM 即时通信零碎!
- Spring Bean IOC、AOP 循环依赖解读
- 毕业前写了 20 万行代码,让我从成为同学眼里的面霸!