MetrAutoAPI系統架構設計
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車商團隊,目前主要負責數據類產品開發和探索。