MetrAutoAPI系统架构设计

开发 前端
商用数据库不是开发者的玩具,而是要被用户长期使用的,不断升级的技术不应该让使用者的知识不断的过期,保持知识与接口的兼容性方面,我们的国产数据库还是有很多地方要多学学Oracle。​

1.背景

公司正在进行数据战略转型,因此我们面临的数据需求越来越多,而我们拥有的指标数据越来越丰富。不过,仅仅拥有数据并不够,我们需要能够更加灵活高效的使用这些数据,以应对频繁多变的数据需求。传统的数据业务开发流程效率低下,无法及时响应变化的需求。因此,我们必须找到一种快速、灵活的解决方案,以满足我们快速变化的需求。

2.MetrAutoAPI设计

2.1平台介绍

MetrAutoAPI(Metric Automate API即指标自动化API)将指标数据与应用层做分离,MetrAutoAPI仅负责管理和处理数据信息,对外提供了一个通用的API接口,所有的数据请求和响应都通过这个接口进行传输和处理。

MetrAutoAPI支持多个数据源集成,其API建模功能灵活可配置,可通过页面拖拽和配置生成SQL查询语句,帮助用户快速准确地获取所需数据。此外,规则引擎服务可以对查询结果进行灵活的运算,帮助用户实现自动化计算和分析,从而提高效率。

2.2架构设计

图片图片

物理查询层:通过统一查询引擎,实现对不同来源数据库的查询

语义模型层:负责指标元数据的管理,并对API-SQL模型进行管理。

统一服务层:提供指标维度的API构建功能,以及基于指标维度的数据查询和规则引擎配置服务。

统一接口层:提供一个对外的API接口,所有的数据请求和响应都通过这个接口进行传输和处理。

此方案对比传统开发模式:

图片图片

2.3使用场景

以下是MetrAutoAPI的一些使用场景的介绍:

►数据看板展示

图片图片

2.4核心功能

►2.4.1 SQL建模服务

利用Zealot与MySqlStatementParser实现SQL建模功能

可以根据不同的查询条件和参数动态生成对应的SQL语句,从而避免手动拼接SQL语句带来的代码冗余和错误,可以很好地支持参数的绑定和传递,可以通过占位符或命名参数的方式传递参数,同时支持参数类型的自动转换

SQL建模过程如下:

public static List<FieldVo> getFieldName(String sqlStr) {  
        MySqlStatementParser mySqlStatementParser = new MySqlStatementParser(sqlStr);  
        //使用parpser解析生成的AST,这里sqlStatement是AST,AST为抽象语法树  
        SQLStatement sqlStatement = mySqlStatementParser.parseStatement();  
        MySqlSchemaStatVisitor visitor = new MySqlSchemaStatVisitor();  
        sqlStatement.accept(visitor);  
  
        //list是存储表名和字段名的集合  
        List<FieldVo> list = new ArrayList<>();  
        Collection<TableStat.Column> columns = visitor.getColumns();  
        //通过循环将表名和字段名解析出,并存储到list集合中  
        columns.stream().forEach(row -> {  
            if (row.isSelect()) {  
                FieldVo fieldVo = new FieldVo();  
                fieldVo.setTableName(row.getTable());  
                fieldVo.setFieldName(row.getName());  
                list.add(fieldVo);  
            }  
        });  
  
        List<FieldVo> aliasList = getAliasField(sqlStr);  
        for (int i = 0 ; i < list.size() ; i ++) {  
            FieldVo vo = list.get(i);  
            FieldVo aliasVo = aliasList.get(i);  
            if (Objects.isNull(aliasVo.getAliasName())) {  
                vo.setAliasName(vo.getFieldName());  
            } else {  
                vo.setAliasName(aliasVo.getAliasName());  
            }  
        }  
        return list;  
    }

自动生成API-SQL接口文档说明:

图片图片

►2.4.2 API-aviator规则引擎服务

Aviator是一个高性能、轻量级的java语言实现的表达式求值引擎,主要用于各种表达式的动态求值,使用规则引擎可以把复杂、重复的业务规则同各个业务系统分离开,以提高业务逻辑的复用能力和开发效率。

图片图片

规则表达式设置如下:

图片图片

初始化规则引擎:

public class AviatorEvaluatorUtils {  
    private static AviatorEvaluatorInstance instance = AviatorEvaluator.getInstance();  
  
    public AviatorEvaluatorUtils() {  
    }  
  
    public static AviatorEvaluatorInstance getInstance() {  
        return instance;  
    }  
  
    static {  
        instance.addFunction(new TransNullToZeroRule());  
        instance.addFunction(new IsNullRuleFunction());  
        instance.addFunction(new CrrRadioRule());  
        instance.addFunction(new CrrRule());  
    }  
}

规则引擎服务会解析并执行规则表达式。

private List<Map<String, Object>> expResult(List<RestApiVo> fieldList, List<Map<String, Object>> dataList) {  
       Stopwatch started = Stopwatch.createStarted();  
       List<Map<String, Object>> resultList = new ArrayList<>();  
       for (Map<String, Object> map : dataList) {  
           Map<String, Object> dataMap = new HashMap<>();  
           fieldList.forEach(e -> {  
               try {  
                   Expression exp = AviatorEvaluatorUtils.getInstance().compile(e.getFieldExp(), true);  
                   Object value = exp.execute(map);  
                   if (Objects.isNull(value)) {  
                       dataMap.put(e.getFieldName(), null);  
                   } else {  
                       if (e.getIsFormat().intValue() == 1) {  
                           BigDecimal decimal = new BigDecimal(value.toString());  
                           BigDecimal scale = decimal.setScale(e.getNumberFormat().intValue(), BigDecimal.ROUND_HALF_UP);  
                           dataMap.put(e.getFieldName(), scale);  
                       } else {  
                           dataMap.put(e.getFieldName(), value);  
                       }  
                   }  
               } catch (Exception ex) {  
                   log.error("解析表达式异常,字段:{},结果:{}", JsonUtil.serialize(e), JsonUtil.serialize(map), ex);  
                   dataMap.put(e.getFieldName(), null);  
               }  
           });  
           resultList.add(dataMap);  
       }  
       log.warn("转换结果耗时:{}", started.stop());  
       return resultList;  
   }

►2.4.3 API统一查询引擎

提供标准化的接口和协议,使得调用端可以通过一致的方式来请求和响应多个不同的 API。这样做可以简化开发人员的工作,加快应用程序的开发速度,同时提高系统的可靠性和可维护性。

/** 
     * 统一API接口 
     * @param _appId appId 
     * @param params 入参 
     * @return 
     */  
    @PostMapping("/restApi")  
    public Protocol<List<Map<String, Object>>> restApi(String _appId, @RequestBody Map<String, Object> params, String apiKey) {  
        ParamsValid valid = new ParamsValid();  
        valid.validNotNull("params", params)  
                .valid("apiId与apiKey不能同时为空", () -> {  
                    if (Objects.isNull(params.get("apiId"))  && Objects.isNull(params.get("apiKey"))) {  
                        return false;  
                    }  
                    return true;  
                }).valid("apiKey值不正确", () -> {  
                    if (Objects.nonNull(params.get("apiKey"))) {  
                        if (Strings.isNullOrEmpty(params.get("apiKey").toString())) {  
                            return false;  
                        }  
                    }  
                    return true;  
                });  
        if (!valid.isValid()) {  
            return valid.showInValidMessage();  
        }  
        Protocol<List<Map<String, Object>>> protocol = null;  
        try {  
            protocol = targetAutoService.restApi(params, _appId, 1);  
        } catch (DataSelfException ex) {  
            log.error("查询数据异常:param:{},apiKey:{}", JsonUtil.serialize(params), apiKey, ex);  
            return new Protocol<>(-1, ex.toString());  
        } catch (Exception e) {  
            log.error("查询信息异常:param:{}", JsonUtil.serialize(params), e);  
            return new Protocol<>(-1, "查询异常,请重试");  
        }  
        return protocol;  
    }

流程如下:

图片图片

3.实践过程中问题及解决方案

3.1使用过程中遇到的难题

接口性能差:接口性能差,分析日志发现从数据库中读取配置与规则引擎信息耗时较长,性能较差。

解决方案:使用redis作为缓存,存储模型元数据信息,统一API引擎在读取配置数据前先从redis中获取,如果获取不到再从业务库中读取,并将读取到的数据写入redis缓存,设置过期时间,定期清除redis缓存中过期的数据,避免占用过多的内存;通过以上优化,可以有效减少从数据库中读取配置数据的时间,提高接口性能。

使用缓存后,性能对比:

图片图片

上线成本高:测试环境建模并验证完成后,还需要在线上环境再次建模,不仅重复操作并且可能因为人为疏忽造成线上线下模型不一致,从而造成严重后果。

解决方案:使用信息复制可以简化测试环境到线上环境的配置过程,从而提高工作效率。具体实现步骤如下:

在测试环境中建模,并将模型元数据信息保存为一个JSON格式的数据,通过粘贴板复制功能,将JSON信息复制到线上环境,通过权限控制来进行安全控制(配置简单化),避免人为异常。

多API接口合并:由于调用方可能需要的指标过于繁琐,可能涉及多个指标API接口的调用,造成调用方调用次数过多,造成并发多,压力大,影响调用方的使用或者造成调用链过长

解决方案:采用接口聚合的方式来解决。接口聚合是将多个API接口的数据聚合到一个API接口中,使得调用方只需要调用一个API接口就能获取到需要的所有指标数据,避免了多次调用导致的并发过多和响应时间过长的问题。同时,也可以提高接口的可用性,避免接口出错或者异常导致调用失败。

4.参考文献

ApiJson:http://apijson.cn/doc/zh/

Mybatis:ttps://github.com/mybatis/mybatis-dynamic-sql

Zealot:ps://gitee.com/chenjiayin1990/zealot

Aviator:https://www.yuque.com/boyan-avfmj/aviatorscript

作者简介

李贺晓

  • 经销商技术部-i车商团队
  • 2018年加入汽车之家,任职于经销商技术部-i车商团队,目前主要负责数据类产品开发和探索。
责任编辑:武晓燕 来源: 之家技术
相关推荐

2014-05-19 10:08:36

IM系统架构设计

2017-12-12 08:40:00

2015-10-16 14:35:05

SaaSCRM架构设计

2013-05-27 10:58:28

Tumblr架构设计雅虎收购

2023-08-16 12:34:16

同步备份异步备份

2024-08-16 14:01:00

2024-10-17 08:26:53

ELKmongodb方案

2023-07-06 00:41:03

SQLNoSQL数据库

2018-05-17 10:10:17

架构设计优化

2015-06-02 04:17:44

架构设计审架构设计说明书

2023-07-03 17:15:12

系统架构设计

2023-07-09 15:20:00

缓存平衡性能

2015-11-13 10:25:04

京东商品搜索架构

2010-08-10 10:10:28

系统架构

2023-07-05 00:36:38

系统架构设计

2011-04-22 16:23:16

ASP.NET动态应用系统

2009-01-11 20:52:35

2009系统架构设计师考试大纲

2023-07-17 18:39:27

业务系统架构

2015-06-02 04:34:05

架构设计

2023-07-02 06:47:42

LOFTER系统架构
点赞
收藏

51CTO技术栈公众号