前言
本文是针对AllData数据中台部分功能点的技术分析。
元数据管理
数据源
Q:连通性检测是怎么实现的呢?
A:
- 根据数据库类型、主机、端口和数据库名称,生成正确的JDBC连接URL;
- 使用生成的URL、用户名和密码,创建一个DataSource对象;
- 调用DataSource对象的
getConnection()方法,若没有报错,则连接成功;否则连接失败。
Q:数据同步是如何实现的呢?
A:
- 调用元数据同步接口,立即响应,底层调用一个异步方法;
- 将数据源同步状态改为true,防止反复调用;
- 异步方法首先查询了该数据源下所有的表信息,如MySQL执行的查询语句是:
select
column_name AS COLNAME,
ordinal_position AS COLPOSITION,
column_default AS DATADEFAULT,
is_nullable AS NULLABLE,
data_type AS DATATYPE,
character_maximum_length AS DATALENGTH,
numeric_precision AS DATAPRECISION,
numeric_scale AS DATASCALE,
column_key AS COLKEY,
column_comment AS COLCOMMENT
from information_schema.columns
where table_schema = 'fastsearch' and table_name = 't_comic'
order by ordinal_position
- 将表信息全部存储到元数据库信息表
metadata_table内,过程中需要检查数据是否已存在(不存在新增,存在更新一下,比如表字段有更新,或者变更更新时间); - 然后遍历表信息,根据数据源和表名,查询所有的字段信息,sql语句如下示例:
select
column_name AS COLNAME,
ordinal_position AS COLPOSITION,
column_default AS DATADEFAULT,
is_nullable AS NULLABLE,
data_type AS DATATYPE,
character_maximum_length AS DATALENGTH,
numeric_precision AS DATAPRECISION,
numeric_scale AS DATASCALE,
column_key AS COLKEY,
column_comment AS COLCOMMENT
from information_schema.columns
where table_schema = 'fastsearch' and table_name = 't_comic'
order by ordinal_position
- 若表的字段不为空,保存到元数据信息表
metadata_column内(同样需判断数据是否存在,不存在新增,存在更新) - 若异步任务执行出错,记录错误日志,更新数据源数据同步状态为异常
- 最后,刷新缓存
Q:刷新缓存是怎么实现的?
A:
- 也是执行了一个异步任务
- 判断redis中是否存在key
data:metadata:sources,存在删除。随后查询数据源所有数据,存入redis中。 - 判断redis中是否存在key
data:metadata:tables,存在删除。随后查询所有表信息,按照数据源进行分组,数据源标识作为key,表实体列表作为value,构建成一个hashmap,然后以hash数据类型存入redis中。 - 判断redis中是否存在key
data:metadata:columns,存在删除。随后查询所有字段信息,按照表名进行分组,表名作为key,字段列表作为value,构建hashmap,存入hash数据类型的redis中。
SQL控制台
Q:sql窗口如何绘制的?
A:输入sql的窗口引入了codemirror实现。
Q:格式化如何实现的?
A:引入js模块实现的。
Q:sql语句如何运行的?
A:
- 点击运行的时候,后端会接收到时间戳、sql语句和数据源标识
- sql字符串转换为sql语句
- 查询数据源
- 可能解析出多个sql语句,创建一个固定大小的线程池去执行,线程全部执行完成后,关闭线程池,返回结果。
- 每一个线程执行前都会预先创建一个连接,将时间戳作为key,放入一个hashmap里。
Q:sql语句如何停止的?
A:其实就是把线程池的所有连接都关闭,这样就停止了。
问题:
JSqlParser会将SQL语句(多个SQL语句以英文;分隔)解析为一个抽象语法树(AST),你可以通过遍历AST来处理SQL语句。但是对于一些数据库特定的语法,无法做到屏蔽,如 MySQL 的 LIMIT、Oracle 的 ROWNUM、PostgreSQL 的 ILIKE 等,源码未考虑到这一点;既然它不是一个完善的功能,就不应该添加到项目经验里。
数据市场
数据接口
Q:限流配置、执行配置、请求参数、响应参数在数据库是如何存储的,如何映射到Java DTO上?
A:MySQL对应的字段使用的数据类型为json,而在后端,通过Jackson直接将这些json格式的字段映射成了DTO。
Q:如何进行发布和注销?
A:
默认的数据接口是待发布的,点击发布之后,执行如下逻辑:
- 新建一个map,放入两个元素,其一key为id,value为数据接口标识,其二为type,value为字符串1;
- 将该map发送给交换机(广播fanout类型);
- 修改数据接口状态为已发布。
已发布的接口还可以撤销,逻辑为:
- 组装map,type为字符串2;
- 将map发送给交换机(广播fanout类型);
- 修改数据接口状态为撤销。
Q:数据市场的RabbitMQ配置是为了什么?
A:
在发布或注销接口的时候,通过rabbitTemplate.convertAndSend(RabbitMqConstant.FANOUT_EXCHANGE_API, "", map);将消息推送到名字为RabbitMqConstant.FANOUT_EXCHANGE_API的交换机。
实际推送到了数据市场的mapping服务。
Q:数据市场中mapping服务的MQ监听是为了什么?
A:
项目中的RabbitMQ进行了如下配置:
@RabbitListener是Spring AMQP提供的一个注解,用于标记一个方法作为RabbitMQ消息的监听器。当消息到达指定的队列时,注解标记的方法会被自动调用,处理消息。- 指定绑定的交换机为fanout广播类型,可持久化(默认配置),且不会自动删除(默认配置)。
- 指定消息队列,该队列绑定到交换机。该队列可持久化、不是独占队列,且不会自动删除。
当数据接口发布或撤销时,mq就可以监听到,并执行如下逻辑:
-
提取map中的id和type;
-
通过feign(mq在微服务dataxMapping内)调用数据市场微服务的接口,查询数据接口实例;
-
bean内定义全局静态的ConcurrentHashMap,名为
mappings; -
如果实例存在,则发布或撤销;
-
撤销逻辑为:
- 根据数据接口实例提取mappingKey:
请求方式大写:/services/v1.0.0/请求路径,例如:GET:/services/v1.0.0/query_comic_list; - 如果mappingKey是存在的,从
mappings中移除; - 将数据接口组合成一个SpringMVC对象RequestMappingInfo(Spring MVC框架中用于定义和匹配请求映射(如
@RequestMapping注解)的底层数据结构,此处封装了请求方式和请求路径) - 调用RequestMappingHandlerMapping(SpringMVC中的核心组件)的unregisterMapping方法,参数为RequestMappingInfo对象,完成动态的注销请求映射。
- 根据数据接口实例提取mappingKey:
-
发布逻辑为:
-
根据数据接口实例提取mappingKey:
请求方式大写:/services/v1.0.0/请求路径,例如:GET:/services/v1.0.0/query_comic_list; -
如果
mappings包含了mappingKey,就先走撤销逻辑; -
将mappingKey存入
mappings,value是数据接口实体; -
组装RequestMappingInfo;
-
调用RequestMappingHandlerMapping的registerMapping方法,参数有三:
- RequestMappingInfo对象,完成动态的注册请求映射。
- handler:处理请求的目标对象(源码中是自定义的RequestHandler);
- method:处理请求的目标方法(源码中是method)。
-
-
关于method:在成员代码块中使用反射,将RequestHandler的invoke方法绑定到method变量。
-
总结:数据接口的控制器就是RequestHandler,处理方法就是method。
Q:在调用已发布的数据接口时,控制器RequestHandler做了什么?
A:
RequestHandler是所有数据接口统一的控制器,它有三个参数,都是Map类型:
- 请求路径中的参数:
@PathVariable(required = false) Map<String, Object> pathVariables - 完成请求参数和控制器的映射:
@RequestParam(required = false) Map<String, Object> requestParams - 请求体中的JSON文件:
@RequestBody(required = false) Map<String, Object> requestBodys
RequestHandler内部的执行逻辑为:
-
新建一个Map,将接收的三个Map都放进去,形成一个总参数Map;
-
从
HttpServletRequest对象中解析得到数据接口实体,具体步骤为:- 将
HttpServletRequest对象构建为NativeWebRequest对象; - 使用最佳路径模式从
NativeWebRequest对象中提取请求路径,通常是@RequestMapping注解中配置的路径,属性的作用域为当前请求;比如:/api/users/{id},可以通过这种方式完整提取;如果使用request.getRequestURI()获取的是/api/users/1。 - 将请求方式和请求路径组合起来,生成一个mappingKey;
- 从mappings中取出mappingKey对应的数据接口实体
- 将
-
深拷贝数据接口实体:
api = objectMapper.readValue(objectMapper.writeValueAsString(api), DataApiEntity.class)。无法理解干吗要这么做,因为针对数据接口实体只有查询,没有增删改操作。 -
执行前置拦截器
- 密钥校验:检查
api_key和secret_key是否存在,不存在抛出异常“api_key或secret_key空”,存在使用md5解码,分别获得api标识和用户标识; - 黑名单校验:提取数据接口实体的黑名单字段(英文逗号分割的ip列表),转换为字符串数组,若当前接口ip(通过HttpServletRequest对象获得)在其内,抛出异常“xxx已被加入IP黑名单”;
- 参数校验:校验传入参数和数据接口配置参数列表类型是否一一匹配,此外还应该根据数据接口的参数配置判断可空(源代码没写);
- 限流校验:如果数据接口实体的限流选项开启了,提取请求次数字段和时间范围,默认分别是5次和60秒,借助redis实现。缓存key是
user:用户标识:api:api标识,若不存在,value设置为1,且超时时间为60秒;若存在,且次数小于上限,次数+1;若次数达到上限,抛出异常:API调用过于频繁。
- 密钥校验:检查
-
调用接口(数据接口实体,总参数map)
- 查询数据源实体:根据数据接口实体配置信息中的数据源标识查询数据源实体,如果为空,抛出异常“API调用查询数据源出错”;
- 获取数据源连接信息:实体中
db_schema是json类型,通过Jackson转换为实体类型 - 构建表数据查询接口
- 提取分页参数:pageNum默认为1,pageSize默认为20
- 构建动态sql:根据数据接口中的sql语句和参数列表构建完整的动态sql
- 查询脱敏规则:如果数据接口存在脱敏规则,查询出来
- 执行sql
- 并行流式数据脱敏:如果存在脱敏规则,使用Stream API的
parallelStream().forEach让集合元素并行执行脱敏逻辑。这里的脱敏,就是针对字符串的一种加密逻辑,如AES、DES、预设正则等。 - 响应:查询结果设置页码和每页数据量,进行响应
-
执行后置拦截器:源代码中没有逻辑
-
进行响应,返回接口执行结果
Q:项目如果重启,接口如何动态注册?
A:
当项目启动时,查询所有已发布的数据接口,遍历它们,执行数据接口发布逻辑。
创建一个类并实现 ApplicationRunner 接口,然后重写 run 方法。run 方法会在应用程序启动后自动调用。
数据接口关键配置
WebSecurityConfiguration定义web应用的安全规则:
声明在该类上的注解为:
@Configuration@EnableWebSecurity(debug = false)=> 启用Spring Security的Web安全支持,它会自动加载Spring Security的核心配置。debug = false:表示关闭Spring Security的调试模式。
继承WebSecurityConfigurerAdapter类:这是Spring Security提供的一个适配器类,用于简化安全配置。通过继承该类,可以重写其方法来定义自定义的安全规则。
源码进行了三种配置:
- 禁用了CSRF保护。
- 允许所有请求(无需认证或授权)。
- 允许所有用户访问注销端点。
这类配置适用于公开的api,允许任何请求访问,且不考虑安全控制。
动态数据源配置类DynamicDSConfiguration:
声明在该类上的注解为:
@Slf4j@Configuration@AllArgsConstructor@EnableConfigurationProperties(DynamicDataSourceProperties.class):启用动态数据源的自动配置@AutoConfigureBefore(DataSourceAutoConfiguration.class):让DynamicDSConfiguration在DataSourceAutoConfiguration之前被加载;@Import(value = {DruidDynamicDataSourceConfiguration.class, DynamicDataSourceCreatorAutoConfiguration.class}):将DruidDynamicDataSourceConfiguration和DynamicDataSourceCreatorAutoConfiguration作为配置类导入到Spring容器中,以便支持动态数据源的相关功能@ConditionalOnProperty(prefix = "spring.datasource.dynamic", name = "enabled", havingValue = "true", matchIfMissing = true):根据条件加载,如果配置文件中没有spring.datasource.dynamic.enabled或spring.datasource.dynamic.enabled为true,则加载带有此注解的配置类,否则不加载
读取多数据源配置,注入到spring容器中。@ConditionalOnMissingBean表示在Spring容器没有该Bean时才会创建,否则跳过。下面的properties变量是DynamicDataSourceProperties,来自MyBatis-Plus。
@Bean
@ConditionalOnMissingBean
public DynamicDataSourceProvider dynamicDataSourceProvider() {
Map<String, DataSourceProperty> datasourceMap = properties.getDatasource();
return new YmlDynamicDataSourceProvider(datasourceMap);
}
注册自己的动态多数据源DataSource,就是使用了MyBatisPlus的相关配置。
@Bean
@ConditionalOnMissingBean
public DataSource dataSource(DynamicDataSourceProvider dynamicDataSourceProvider) {
DynamicRoutingDataSource dataSource = new DynamicRoutingDataSource();
dataSource.setPrimary(properties.getPrimary());
dataSource.setStrict(properties.getStrict());
dataSource.setStrategy(properties.getStrategy());
dataSource.setProvider(dynamicDataSourceProvider);
dataSource.setP6spy(properties.getP6spy());
dataSource.setSeata(properties.getSeata());
return dataSource;
}
AOP切面,对DS注解过的方法进行增强,达到切换数据源的目的。
@Role(value = BeanDefinition.ROLE_INFRASTRUCTURE) // 标记为基础设施 Bean,表明它仅用于框架内部,不会对用户的应用程序逻辑产生直接影响
@Bean
@ConditionalOnMissingBean
public DynamicDataSourceAnnotationAdvisor dynamicDatasourceAnnotationAdvisor(DsProcessor dsProcessor) {
DynamicDataSourceAnnotationInterceptor interceptor = new DynamicDataSourceAnnotationInterceptor(properties.isAllowedPublicOnly(), dsProcessor);
DynamicDataSourceAnnotationAdvisor advisor = new DynamicDataSourceAnnotationAdvisor(interceptor);
advisor.setOrder(properties.getOrder());
return advisor;
}