AllData数据中台技术分析

66 阅读11分钟

前言

本文是针对AllData数据中台部分功能点的技术分析。

元数据管理

数据源

Q:连通性检测是怎么实现的呢?

A

  1. 根据数据库类型、主机、端口和数据库名称,生成正确的JDBC连接URL;
  2. 使用生成的URL、用户名和密码,创建一个DataSource对象;
  3. 调用DataSource对象的getConnection()方法,若没有报错,则连接成功;否则连接失败。

Q:数据同步是如何实现的呢?

A

  1. 调用元数据同步接口,立即响应,底层调用一个异步方法;
  2. 将数据源同步状态改为true,防止反复调用;
  3. 异步方法首先查询了该数据源下所有的表信息,如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
  1. 将表信息全部存储到元数据库信息表metadata_table内,过程中需要检查数据是否已存在(不存在新增,存在更新一下,比如表字段有更新,或者变更更新时间);
  2. 然后遍历表信息,根据数据源和表名,查询所有的字段信息,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
  1. 若表的字段不为空,保存到元数据信息表metadata_column内(同样需判断数据是否存在,不存在新增,存在更新)
  2. 若异步任务执行出错,记录错误日志,更新数据源数据同步状态为异常
  3. 最后,刷新缓存

Q:刷新缓存是怎么实现的?

A

  1. 也是执行了一个异步任务
  2. 判断redis中是否存在keydata:metadata:sources,存在删除。随后查询数据源所有数据,存入redis中。
  3. 判断redis中是否存在keydata:metadata:tables,存在删除。随后查询所有表信息,按照数据源进行分组,数据源标识作为key,表实体列表作为value,构建成一个hashmap,然后以hash数据类型存入redis中。
  4. 判断redis中是否存在keydata:metadata:columns,存在删除。随后查询所有字段信息,按照表名进行分组,表名作为key,字段列表作为value,构建hashmap,存入hash数据类型的redis中。

SQL控制台

Q:sql窗口如何绘制的?

A:输入sql的窗口引入了codemirror实现。


Q:格式化如何实现的?

A:引入js模块实现的。


Q:sql语句如何运行的?

A

  1. 点击运行的时候,后端会接收到时间戳、sql语句和数据源标识
  2. sql字符串转换为sql语句
  3. 查询数据源
  4. 可能解析出多个sql语句,创建一个固定大小的线程池去执行,线程全部执行完成后,关闭线程池,返回结果。
  5. 每一个线程执行前都会预先创建一个连接,将时间戳作为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

默认的数据接口是待发布的,点击发布之后,执行如下逻辑:

  1. 新建一个map,放入两个元素,其一key为id,value为数据接口标识,其二为type,value为字符串1;
  2. 将该map发送给交换机(广播fanout类型);
  3. 修改数据接口状态为已发布。

已发布的接口还可以撤销,逻辑为:

  1. 组装map,type为字符串2;
  2. 将map发送给交换机(广播fanout类型);
  3. 修改数据接口状态为撤销。

Q:数据市场的RabbitMQ配置是为了什么?

A

在发布或注销接口的时候,通过rabbitTemplate.convertAndSend(RabbitMqConstant.FANOUT_EXCHANGE_API, "", map);将消息推送到名字为RabbitMqConstant.FANOUT_EXCHANGE_API的交换机。

实际推送到了数据市场的mapping服务。


Q:数据市场中mapping服务的MQ监听是为了什么?

A

项目中的RabbitMQ进行了如下配置:

  1. @RabbitListenerSpring AMQP提供的一个注解,用于标记一个方法作为RabbitMQ消息的监听器。当消息到达指定的队列时,注解标记的方法会被自动调用,处理消息。
  2. 指定绑定的交换机为fanout广播类型,可持久化(默认配置),且不会自动删除(默认配置)。
  3. 指定消息队列,该队列绑定到交换机。该队列可持久化、不是独占队列,且不会自动删除。

当数据接口发布或撤销时,mq就可以监听到,并执行如下逻辑:

  1. 提取map中的id和type;

  2. 通过feign(mq在微服务dataxMapping内)调用数据市场微服务的接口,查询数据接口实例;

  3. bean内定义全局静态的ConcurrentHashMap,名为mappings

  4. 如果实例存在,则发布或撤销;

  5. 撤销逻辑为:

    1. 根据数据接口实例提取mappingKey:请求方式大写:/services/v1.0.0/请求路径,例如:GET:/services/v1.0.0/query_comic_list
    2. 如果mappingKey是存在的,从mappings中移除;
    3. 将数据接口组合成一个SpringMVC对象RequestMappingInfo(Spring MVC框架中用于定义和匹配请求映射(如@RequestMapping注解)的底层数据结构,此处封装了请求方式和请求路径)
    4. 调用RequestMappingHandlerMapping(SpringMVC中的核心组件)的unregisterMapping方法,参数为RequestMappingInfo对象,完成动态的注销请求映射。
  6. 发布逻辑为:

    1. 根据数据接口实例提取mappingKey:请求方式大写:/services/v1.0.0/请求路径,例如:GET:/services/v1.0.0/query_comic_list

    2. 如果mappings包含了mappingKey,就先走撤销逻辑;

    3. 将mappingKey存入mappings,value是数据接口实体;

    4. 组装RequestMappingInfo;

    5. 调用RequestMappingHandlerMapping的registerMapping方法,参数有三:

      1. RequestMappingInfo对象,完成动态的注册请求映射。
      2. handler:处理请求的目标对象(源码中是自定义的RequestHandler);
      3. method:处理请求的目标方法(源码中是method)。
  7. 关于method:在成员代码块中使用反射,将RequestHandler的invoke方法绑定到method变量。

  8. 总结:数据接口的控制器就是RequestHandler,处理方法就是method。


Q:在调用已发布的数据接口时,控制器RequestHandler做了什么?

A

RequestHandler是所有数据接口统一的控制器,它有三个参数,都是Map类型:

  1. 请求路径中的参数:@PathVariable(required = false) Map<String, Object> pathVariables
  2. 完成请求参数和控制器的映射:@RequestParam(required = false) Map<String, Object> requestParams
  3. 请求体中的JSON文件:@RequestBody(required = false) Map<String, Object> requestBodys

RequestHandler内部的执行逻辑为:

  1. 新建一个Map,将接收的三个Map都放进去,形成一个总参数Map;

  2. HttpServletRequest对象中解析得到数据接口实体,具体步骤为:

    1. HttpServletRequest对象构建为NativeWebRequest对象;
    2. 使用最佳路径模式从NativeWebRequest对象中提取请求路径,通常是@RequestMapping注解中配置的路径,属性的作用域为当前请求;比如:/api/users/{id},可以通过这种方式完整提取;如果使用request.getRequestURI()获取的是/api/users/1
    3. 将请求方式和请求路径组合起来,生成一个mappingKey;
    4. 从mappings中取出mappingKey对应的数据接口实体
  3. 深拷贝数据接口实体:api = objectMapper.readValue(objectMapper.writeValueAsString(api), DataApiEntity.class)。无法理解干吗要这么做,因为针对数据接口实体只有查询,没有增删改操作。

  4. 执行前置拦截器

    1. 密钥校验:检查api_keysecret_key是否存在,不存在抛出异常“api_keysecret_key空”,存在使用md5解码,分别获得api标识用户标识
    2. 黑名单校验:提取数据接口实体的黑名单字段(英文逗号分割的ip列表),转换为字符串数组,若当前接口ip(通过HttpServletRequest对象获得)在其内,抛出异常“xxx已被加入IP黑名单”;
    3. 参数校验:校验传入参数和数据接口配置参数列表类型是否一一匹配,此外还应该根据数据接口的参数配置判断可空(源代码没写);
    4. 限流校验:如果数据接口实体的限流选项开启了,提取请求次数字段和时间范围,默认分别是5次和60秒,借助redis实现。缓存key是user:用户标识:api:api标识,若不存在,value设置为1,且超时时间为60秒;若存在,且次数小于上限,次数+1;若次数达到上限,抛出异常:API调用过于频繁。
  5. 调用接口(数据接口实体,总参数map)

    1. 查询数据源实体:根据数据接口实体配置信息中的数据源标识查询数据源实体,如果为空,抛出异常“API调用查询数据源出错”;
    2. 获取数据源连接信息:实体中db_schemajson类型,通过Jackson转换为实体类型
    3. 构建表数据查询接口
    4. 提取分页参数:pageNum默认为1,pageSize默认为20
    5. 构建动态sql:根据数据接口中的sql语句和参数列表构建完整的动态sql
    6. 查询脱敏规则:如果数据接口存在脱敏规则,查询出来
    7. 执行sql
    8. 并行流式数据脱敏:如果存在脱敏规则,使用Stream API的parallelStream().forEach让集合元素并行执行脱敏逻辑。这里的脱敏,就是针对字符串的一种加密逻辑,如AES、DES、预设正则等。
    9. 响应:查询结果设置页码和每页数据量,进行响应
  6. 执行后置拦截器:源代码中没有逻辑

  7. 进行响应,返回接口执行结果


Q:项目如果重启,接口如何动态注册?

A

当项目启动时,查询所有已发布的数据接口,遍历它们,执行数据接口发布逻辑。

创建一个类并实现 ApplicationRunner 接口,然后重写 run 方法。run 方法会在应用程序启动后自动调用。

数据接口关键配置

WebSecurityConfiguration定义web应用的安全规则:

声明在该类上的注解为:

  1. @Configuration
  2. @EnableWebSecurity(debug = false) => 启用Spring Security的Web安全支持,它会自动加载Spring Security的核心配置。debug = false:表示关闭Spring Security的调试模式。

继承WebSecurityConfigurerAdapter类:这是Spring Security提供的一个适配器类,用于简化安全配置。通过继承该类,可以重写其方法来定义自定义的安全规则。

源码进行了三种配置:

  1. 禁用了CSRF保护。
  2. 允许所有请求(无需认证或授权)。
  3. 允许所有用户访问注销端点。

这类配置适用于公开的api,允许任何请求访问,且不考虑安全控制。


动态数据源配置类DynamicDSConfiguration

声明在该类上的注解为:

  1. @Slf4j
  2. @Configuration
  3. @AllArgsConstructor
  4. @EnableConfigurationProperties(DynamicDataSourceProperties.class):启用动态数据源的自动配置
  5. @AutoConfigureBefore(DataSourceAutoConfiguration.class):让DynamicDSConfigurationDataSourceAutoConfiguration之前被加载;
  6. @Import(value = {DruidDynamicDataSourceConfiguration.class, DynamicDataSourceCreatorAutoConfiguration.class}):将DruidDynamicDataSourceConfigurationDynamicDataSourceCreatorAutoConfiguration作为配置类导入到Spring容器中,以便支持动态数据源的相关功能
  7. @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;

}