管理後臺服務通用化設計拙見

前言

在公司實習兩個多月,主要接手的都是管理後臺的需求。一般情況下,管理後臺很容易和CRUD聯想到一起,這一類操作的特點就是代碼基本相似,做一些簡單的增刪改查接口,不同的只是對於不同數據表的更改。不過針對這樣的一個管理後臺,相當於業務配置中心的服務,功能簡單但又十分重要,往往會花費較多的經歷在寫一些重複相似的代碼上,本文就來講講相關的改進工作。
爲了避免涉及商業隱私,我們依然以上一篇文章大型工程微服務架構設計拙見的情景來引入。

需求

祺平咖啡館生意越來越火爆,業務也越來越龐大。爲了支持整個咖啡館系統的正常營運,管理後臺服務需要支撐所有的配置信息,比如所有的商品信息,優惠券信息,抽獎獎品信息等等,整個服務需要配置的數據漸漸擴大。每次需要在管理後臺配置的數據,都需要前後端實現一套操作系統,用於管理整個配置數據。同時配置數據還要提供RPC或者HTTP的接口用於其他上游微服務的調用。

思考

對於一個新來的配置數據需求,按照流程我們需要做以下幾件事:

  • 設計並新建數據源(包括db/es等)
  • 後端新增對數據源的增刪改查接口(可能會包含一些業務特有的邏輯)
  • 前端實現操作數據頁面,調用後端接口(可能會有一些個性化展示數據的需求)

對於第一步來說,可能無法避免,我們必須針對特殊的業務新建特殊的數據源來存儲,無論是關係型數據庫MySQL,還是搜索數據庫ElasticSearch。但創建數據源對我們的工作複雜度來說還算OK。
對於第二步來說,後端的增刪改查接口其實對應於數據源來說就是一些基本固定的代碼,比如數據庫就是sql,ES就是操作API,只是其中的一些參數不一樣,例如不同的庫表/不同的索引,但增刪改查語句的大致結構還是不變的。
對於第三步來說,前端會調用後端的增刪改查接口進行操作數據,屏蔽後端的差異,其實每個操作數據頁面模樣是一樣的,只是對應的按鈕觸發的請求是不一樣的,不一致的部分標記了唯一要操作的數據源。

通用化設計

經過上一塊對於整個流程的思考,我們可以如下方式對整個實現流程進行改造,着重抓住通用步驟,剝離變化的部分。

創建數據源

對於數據庫,我們往往先設計好關係型的庫表結構,然後書寫數據庫表創建SQL語句。有一些圖形化工具也能夠讓我們創建出庫表。對於ES,也非常相似,通過相關的API代碼我們便可以創建Index和Type。所以,這一塊的通用化設計,我們可以仿照Navicat這樣的圖形化界面,用於創建修改刪除數據源,針對不同的存儲類型可以生成對應的數據源。
當然在公司不同的環境,可能有對應的權限限制,對應的數據源增刪改查可能需要工單審批等等。根據實際情況,我們可以考慮是否實現數據源的通用操作功能。

如下圖所示,我們可以先選擇好數據源的存儲類型,然後選擇填好對應的級別設置即可,在後端我們會利用數據庫表管理所有的數據源信息。
創建修改數據源
後端數據表設計成下面所示意的結構。datasource 主要用於管理數據源,包括了上圖中出去表格部分以外的所有信息,一個數據源有多列,比如數據庫的一張表有很多列,ES的一個Type裏包含很多Field。
數據庫表設計
這樣我們就能將所有可用的數據源信息管理起來。便於之後操作具體某個數據源數據時,獲取到數據源所有的詳細信息。

當然,前面主要說的是新增數據源的時候我們是如何利用頁面操作方式操作的。那麼對於已有的數據源信息我們如何接入呢?

MySQL中可以利用JDBC拿到所有的表、字段信息,ES中也有對應的API可以獲取所有Index、Type以及Field信息。通過這種方式,我們也可以實現獲取所有的元數據信息後進行落庫管理起來。

後端接口

後端可以設計一套通用的增刪改查接口,用於操作配置數據。我們可以簡單的設計如下的HTTP接口:

接口名稱 接口形式 備註
新增數據 /generic/add/{id}
刪除數據 /generic/del/{id}
修改數據 /generic/edit/{id}
查詢數據 /generic/query/{id}
發佈數據 /generic/publish/{id} 數據的修改可能需要通知上游

接口路徑上的 id 用於區分具體是哪個數據源,它能夠和上面所說的 datasource 表中的主鍵 id 一一對應。當前端請求HTTP接口時,我們可以知道需要操作哪個數據源,然後對相應的數據源進行數據操作即可。

針對不同的存儲類型,數據操作層的實現也是不一樣的。上面其實相當於定義了 controller 層的實現,service 層可以使用策略模式,針對不同的存儲類型,實現不同的數據操作。

在實現過程中,我們發現對於MySQL的數據操作,我們可以使用Spring JDBC。Spring JDBC支持直接的sql操作,而且對於數據類型(java.sql.Types)的支持非常到位。至於sql,我們可以根據HTTP請求參數拼裝成sql,表名列名之類的信息我們都保存在了之前的數據源信息表中。當然,數據庫的寫操作往往需要事務,事務也依賴於數據源,我們可以利用Spring編程式事務自定義一個事務註解AOP,這樣能達到不同的數據源管理各自的事務。而對於ES的數據操作,我們可以直接利用Java API即可,十分方便。

前面主要說的是要讀寫的數據已經準備好,我們常常在寫數據的時候需要校驗數據內容,包含數據本身屬性限制的,例如日期、數字等,也包含業務上的含義限制,比如優惠券批次號、獎品id號、活動時間等。關於數據的校驗,我們可以單獨爲每一列綁定好一些預設的校驗器,這些校驗器我們可以參考Spring的攔截器,一層層校驗,一旦不通過就直接返回。

我們可以給 datasoruce_columns 表添加一個字段 check_list,存放所有的 checkerid 數組。
校驗器

前端頁面

對於前端來說,不同數據的操作數據的基本相似。數據的展示可能需要一些個性化定製,對於這些個性化的設置,前端可以保存一份頁面樣式配置,保存在後端。在展示頁面之前,需要先從後端獲取配置文件,然後渲染頁面即可。

所有的頁面信息,我們也可以用一張數據表來進行管理。其中 page_style 用於存儲前端特有的樣式配置,datasource_id 表示該頁面綁定的數據源。這樣前端的每一個動作,後端根據其傳來的頁面 id 知道綁定的數據源,這樣所有的頁面動作都能反應的正確的數據源上。
頁面信息

RPC/HTTP服務

上一部分,我們實現了通用管理服務配置數據的實現,包括增刪改查功能。但是數據是爲其他微服務進行服務的,我們必須要將數據暴露出去。由於管理服務基本數據的操作直接操作數據源本身,QPS不能過高。但是其他服務的調用量往往非常大,所以我們需要着重設計好這一塊。

引入緩存是解決讀性能瓶頸的重要手段之一,緩存的中間件有很多,常見的如Redis等,但考慮該場景下,我們並不需要分佈式緩存。單機緩存足夠應付這種場景,我們只需要將配置數據緩存到本地即可,每次讀取數據先走緩存。一旦數據發生變更,我們要通知其他服務,數據發生更新,更新緩存。

當然,這樣做的弊端是,每個從通用管理服務獲取數據的上游來說,都需要實現自己的緩存邏輯,以及處理緩存更新問題。我們可以寫一個SDK工具,讓各個業務方進行使用。在SDK中我們實現基本的緩存,緩存更新邏輯,而特有的邏輯比如如何獲取數據、篩選數據、校驗邏輯等則交給業務方自定義實現。這樣我們可以很容易利用模板模式來實現。

緩存更新問題,涉及到通知事件。現有的一些配置中間件例如攜程的Apllo、ZooKeeper等都可以實現實時更新操作,我們可以利用配置中間件充當中間人。當管理服務數據更新時,通過配置中間件推送上游服務數據更新。爲了防止配置中間件掛掉,我們可以設置上游服務每隔一段時間主動拉取管理服務數據,推拉結合保證數據的更新。

僞代碼如下:

public abstract class AbstractGenericCache {

    private String NOTIFY_KEY;
	private Integer POLLING_INTERVAL;

	private volatile Map cache = new HashMap<>();

	private GenericRpc rpcServer;

	public AbstractGenericCache(GenericRpc rpcServer, String notifyKey, Integer pollingInterval) {
		this.rpcServer = rpcServer;
		this.NOTIFY_KEY = notifyKey;
		this.POLLING_INTERVAL = pollingInterval;
		init();
	}

	/**
	 * 初始化方法
	 */
	private void init() {
		// 實現好緩存推拉結合代碼
		
		// 更新緩存通過下面這句話
		updateCache();

	}

	private void updateCache() {
		Map cache = getConfigDataFromDataSource();
		if (!CollectionUtils.isEmpty(cache)) {
			this.cache = cache;
		}
	}


	/**
	 * 獲取數據——供其他服務調用
	 * @param condition
	 * @return
	 */
	public Map getConfigData(Condition condition) {
		return getConfigDataFromCache(condition);
	}
	
	private Map getConfigDataFromCache(Condition condition) {
		// 具體實現緩存中獲取數據
		return cache;
	}

	/**
	 * 模板方法——通過rpc獲取全量配置數據
	 * @return
	 */
	protected abstract Map getConfigDataFromDataSource();	
} 

對於其他服務來說,只需要繼承該抽象緩存類,實現對應一些特殊的方法即可,便可以方便的實現配置數據的獲取。

總結

在整個實習過程中,後面主要參與了這一塊服務的設計與實現,最終實現了大致本文的效果。其實整個實現技術難度不是特別難,做出來的效果也還是可以的,應對新需求的來臨,基本消除了代碼的編寫,思想還是很不錯的。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章