作者:小傅哥
博客: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
@Testpublic 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 "";}
应用
@Mapperpublic interface IUserDao { @DBRouter(key = "userId") User queryUserInfoByUserId(User req); @DBRouter(key = "userId") void insertUser(User req);}
- 首先咱们须要自定义一个注解,用于搁置在须要被数据库路由的办法上。
- 它的应用形式是通过办法配置注解,就能够被咱们指定的 AOP 切面进行拦挡,拦挡后进行相应的数据库路由计算和判断,并切换到相应的操作数据源上。
2. 解析路由配置
- 以上就是咱们实现完数据库路由组件后的一个数据源配置,在分库分表下的数据源应用中,都须要反对多数据源的信息配置,这样能力满足不同需要的扩大。
- 对于这种自定义较大的信息配置,就须要应用到
org.springframework.context.EnvironmentAware
接口,来获取配置文件并提取须要的配置信息。
数据源配置提取
@Overridepublic 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 来实现,并且这里提供的数据源是能够动静变换的,也就是反对动静切换数据源。
创立数据源
@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); 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:322: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万行代码,让我从成为同学眼里的面霸!