⚠️ 本文为掘金社区首发签约文章,未获受权禁止转载java
做者:小傅哥
博客:bugstack.cn面试
沉淀、分享、成长,让本身和他人都能有所收获!😄算法
什么?Java 面试就像造火箭🚀
spring
单纯了! 之前我也一直想 Java 面试就好好面试呗,嘎哈么总考一些工做中也用不到的玩意,会用 Spring
、MyBatis
、Dubbo
、MQ
,把业务需求实现了不就好了!数据库
但当工做几年后,须要提高本身(要加钱)的时候,居然开始以为本身只是一个调用 API 攒接口的工具人。没有知识宽度,没有技术纵深,也想不出来更没有意识,把平常开发的业务代码中通用的共性逻辑提炼出来,开发成公用的组件,更没有去思考平常使用的一些组件是用什么技术实现的。设计模式
因此有时候你说面试好像就是在造火箭,这些技术平常根本用不到,其实不少时候不是这个技术用不到,而是由于你没用(嗯,之前我也没用)。当你有这个想法想突破本身的薪资待遇瓶颈时,就须要去了解了解必备的数据结构
、学习学习Java的算法逻辑
、熟悉熟悉通用的设计模式
、再结合像 Spring、ORM、RPC,这样的源码实现逻辑,把相应的技术方案赋能到本身的平常业务开发中,把共性的问题用聚焦和提炼的方式进行解决,这些才是你在 CRUD 以外的能力体现(加薪筹码)。数组
怎么? 好像听上去有道理,那么举个栗子,来一场数据库路由
的需求分析和逻辑实现!markdown
若是要作一个数据库路由,都须要作什么技术点?
数据结构
首先咱们要知道为何要用分库分表,其实就是因为业务体量较大,数据增加较快,因此须要把用户数据拆分到不一样的库表中去,减轻数据库压力。app
分库分表操做主要有垂直拆分和水平拆分:
而本章节咱们要实现的也是水平拆分的路由设计,如图 1-1
那么,这样的一个数据库路由设计要包括哪些技术知识点呢?
综上,能够看到在数据库和表的数据结构下完成数据存放,我须要用到的技术包括:AOP
、数据源切换
、散列算法
、哈希寻址
、ThreadLoca
l以及SpringBoot的Starter开发方式
等技术。而像哈希散列
、寻址
、数据存放
,其实这样的技术与 HashMap 有太多类似之处,那么学完源码造火箭的机会来了 若是你有过深刻分析和学习过 HashMap 源码、Spring 源码、中间件开发,那么在设计这样的数据库路由组件时必定会有不少思路的出来。接下来咱们一块儿尝试下从源码学习到造火箭!
在 JDK 源码中,包含的数据结构设计有:数组、链表、队列、栈、红黑树,具体的实现有 ArrayList、LinkedList、Queue、Stack,而这些在数据存放都是顺序存储,并无用到哈希索引的方式进行处理。而 HashMap、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
复制代码
f(k) = ((k * 2654435769) >> X) << Y对于常见的32位整数而言,也就是 f(k) = (k * 2654435769) >> 28
,黄金分割点:(√5 - 1) / 2 = 0.6180339887
1.618:1 == 1:0.618
public static int disturbHashIdx(String key, int size) {
return (size - 1) & (key.hashCode() ^ (key.hashCode() >>> 16));
}
复制代码
定义
@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);
}
复制代码
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);
}
}
复制代码
在结合 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 的实现类,这个类里能够存放和读取相应的具体调用的数据源信息。在 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();
}
}
复制代码
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;
复制代码
<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>
复制代码
${tbIdx}
用于写入当前的表ID。@DBRouter(key = "userId")
User queryUserInfoByUserId(User req);
@DBRouter(key = "userId")
void insertUser(User req);
复制代码
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
复制代码
数据库路由 method:queryUserInfoByUserId dbIdx:2 tbIdx:3
综上 就是咱们从 HashMap、ThreadLocal、Spring等源码学习中了解到技术内在原理,并把这样的技术用在一个数据库路由设计上。若是没有经历过这些总被说成造火箭的技术沉淀,那么几乎也不太可能顺利开发出一个这样一个中间件,全部不少时候根本不是技术没用,而是本身没用上没机会用而已。不要总惦记那一片片重复的 CRUD,看看还有哪些知识是真的能够提高我的能力的!若是你对中间件的设计和实现
感兴趣,也能够参考这个掘金小册:juejin.cn/book/694099…