⚠️ 本文爲掘金社區首發簽約文章,未獲受權禁止轉載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…