作者:小傅哥

博客:https://bugstack.cn

沉淀、分享、成长,让自己和他人都能有所收获!

一、前言

什么?Java 面试就像造火箭

单纯了! 以前我也一直想 Java 面试就好好面试呗,嘎哈么总考一些工作中也用不到的玩意,会用 SpringMyBatisDubboMQ,把业务需求实现了不就行了!

但当工作几年后,需要提升自己(要加钱)的时候,竟然开始觉得自己只是一个调用 API 攒接口的工具人。没有知识宽度,没有技术纵深,也想不出来更没有意识,把日常开发的业务代码中通用的共性逻辑提炼出来,开发成公用的组件,更没有去思考日常使用的一些组件是用什么技术实现的。

所以有时候你说面试好像就是在造火箭,这些技术日常根本用不到,其实很多时候不是这个技术用不到,而是因为你没用(嗯,以前我也没用)。当你有这个想法想突破自己的薪资待遇瓶颈时,就需要去了解了解必备的数据结构学习学习Java的算法逻辑熟悉熟悉通用的设计模式、再结合像 Spring、ORM、RPC,这样的源码实现逻辑,把相应的技术方案赋能到自己的日常业务开发中,把共性的问题用聚焦和提炼的方式进行解决,这些才是你在 CRUD 之外的能力体现(加薪筹码)。

怎么? 好像听上去有道理,那么举个栗子,来一场数据库路由的需求分析和逻辑实现!

二、需求分析

如果要做一个数据库路由,都需要做什么技术点?

首先我们要知道为什么要用分库分表,其实就是由于业务体量较大,数据增长较快,所以需要把用户数据拆分到不同的库表中去,减轻数据库压力。

分库分表操作主要有垂直拆分和水平拆分:

  • 垂直拆分:指按照业务将表进行分类,分布到不同的数据库上,这样也就将数据的压力分担到不同的库上面。最终一个数据库由很多表的构成,每个表对应着不同的业务,也就是专库专用。
  • 水平拆分:如果垂直拆分后遇到单机瓶颈,可以使用水平拆分。相对于垂直拆分的区别是:垂直拆分是把不同的表拆到不同的数据库中,而水平拆分是把同一个表拆到不同的数据库中。如:user_001、user_002

而本章节我们要实现的也是水平拆分的路由设计,如图 1-1

那么,这样的一个数据库路由设计要包括哪些技术知识点呢?

  • 是关于 AOP 切面拦截的使用,这是因为需要给使用数据库路由的方法做上标记,便于处理分库分表逻辑。
  • 数据源的切换操作,既然有分库那么就会涉及在多个数据源间进行链接切换,以便把数据分配给不同的数据库。
  • 数据库表寻址操作,一条数据分配到哪个数据库,哪张表,都需要进行索引计算。在方法调用的过程中最终通过 ThreadLocal 记录。
  • 为了能让数据均匀的分配到不同的库表中去,还需要考虑如何进行数据散列的操作,不能分库分表后,让数据都集中在某个库的某个表,这样就失去了分库分表的意义。

综上,可以看到在数据库和表的数据结构下完成数据存放,我需要用到的技术包括:AOP数据源切换散列算法哈希寻址ThreadLocal以及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

六、系列推荐

基于AOP和HashMap原理学习,开发Mysql分库分表路由组件!的更多相关文章

  1. mysql分库分表(一)

    mysql分库分表 参考: https://blog.csdn.net/xlgen157387/article/details/53976153 https://blog.csdn.net/cleve ...

  2. 高可用Mysql架构_Mysql主从复制、Mysql双主热备、Mysql双主双从、Mysql读写分离(Mycat中间件)、Mysql分库分表架构(Mycat中间件)的演变

    [Mysql主从复制]解决的问题数据分布:比如一共150台机器,分别往电信.网通.移动各放50台,这样无论在哪个网络访问都很快.其次按照地域,比如国内国外,北方南方,这样地域性访问解决了.负载均衡:M ...

  3. mysql分库分表(二)

    mysql分库分表 参考: https://www.cnblogs.com/dongruiha/p/6727783.html https://www.cnblogs.com/oldUncle/p/64 ...

  4. Java互联网架构-Mysql分库分表订单生成系统实战分析

    概述 分库分表的必要性 首先我们来了解一下为什么要做分库分表.在我们的业务(web应用)中,关系型数据库本身比较容易成为系统性能瓶颈,单机存储容量.连接数.处理能力等都很有限,数据库本身的“有状态性” ...

  5. MYSQL分库分表和不停机更改表结构

    在MYSQL分库分表中我们一般是基于数据量比较大的时间对mysql数据库一种优化的做法,下面我简单的介绍一下mysql分表与分库的简单做法. .分库分表 很明显,一个主表(也就是很重要的表,例如用户表 ...

  6. 你们要的MyCat实现MySQL分库分表来了

    ❝ 借助MyCat来实现MySQL的分库分表落地,没有实现过的,或者没了解过的可以看看 ❞ 前言 在之前写过一篇关于mysql分库分表的文章,那篇文章只是给大家提供了一个思路,但是回复下面有很多说是细 ...

  7. Mysql分库分表方案

    Mysql分库分表方案 1.为什么要分表: 当一张表的数据达到几千万时,你查询一次所花的时间会变多,如果有联合查询的话,我想有可能会死在那儿了.分表的目的就在于此,减小数据库的负担,缩短查询时间. m ...

  8. MySQL分库分表备份脚本

    MySQL分库备份脚本 #脚本详细内容 [root@db02 scripts]# cat /server/scripts/Store_backup.sh #!/bin/sh MYUSER=root M ...

  9. 【分库、分表】MySQL分库分表方案

    一.Mysql分库分表方案 1.为什么要分表: 当一张表的数据达到几千万时,你查询一次所花的时间会变多,如果有联合查询的话,我想有可能会死在那儿了.分表的目的就在于此,减小数据库的负担,缩短查询时间. ...

随机推荐

  1. Vue $refs无法操作element-ui组件

    比如我要操作这个dom元素↓↓↓ <el-badge :value="1" :max="99" class="message"> ...

  2. 37.qt quick- 高仿微信实现局域网聊天V3版本(添加登录界面、UDP校验登录、皮肤更换、3D旋转)

    1.版本介绍(已上传至群里) 版本说明: 添加登录界面. UDP校验登录. 皮肤更换. 3D旋转(主界面和登录界面之间切换) . 效果图如下所示: 如果效果图加载失败,可以去哔哩哔哩 https:// ...

  3. Java程序设计(2021春)——第一章课后题(选择题+编程题)答案与详解

    Java程序设计(2021春)--第一章课后题(选择题+编程题)答案与详解 目录 Java程序设计(2021春)--第一章课后题(选择题+编程题)答案与详解 第一章选择题 1.1 Java与面向对象程 ...

  4. 『无为则无心』Python函数 — 26、Python函数参数的传递方式

    目录 1.位置参数 2.关键字参数 3.缺省参数(默认参数) 4.不定长参数(可变参数) (1)包裹位置传递 (2)包裹关键字传递 5.位置参数.默认参数.可变参数的混合使用 6.拓展:参数解包 提示 ...

  5. cobbler安装4个报错汇总及解决方法:ImportError

    安装完,检查cobbler:cobbler check发现这个问题ImportError: No module named yaml 具体报错如下: [root@www localdisk]# cob ...

  6. TCP/IP 5层协议簇/协议栈

    TCP/IP 5层协议簇/协议栈 数据/PDU 应用层 PC.防火墙 数据段/段Fragment 传输层 防火墙 报文/包/IP包packet 网络层 路由器 帧Frame 数据链路层 交换机.网卡 ...

  7. Java+Selenium3.3.1环境搭建

    一.背景和目的 selenium从2.0开始,加入了webdriver,实际上,我们说的selenium自动化测试,大部分情况都是在使用webdriver的API.现在去Selenium官网,发现最新 ...

  8. python numpy 求数组的百位分数

    百分位数,统计学术语,如果将一组数据从小到大排序,并计算相应的累计百分位,则某一百分位所对应数据的值就称为这一百分位的百分位数.运用在教育统计学中,例如表现测验成绩时,称PR值.分位数是以概率将一批数 ...

  9. java集合(5)-List集合

    序章 List集合是一个元素有序且可以重复的集合,每个元素对应一个索引,类似于数组的方式,默认是按照添加顺序,所以第一次添加元素索引为0,第二次为1.... 一 Java8改进的List接口和List ...

  10. React组件三大属性之state

    React组件三大属性之state 组件被称为"状态机", 页面的显示是根据组件的state属性的数据来显示 理解1) state是组件对象最重要的属性, 值是对象(可以包含多个数 ...