愛奇藝會員交易團隊在系統(tǒng)擴展性方面的探索

注:

本文首發(fā)于“愛奇藝技術(shù)產(chǎn)品團隊”公眾號。作為本文的作者,我將此文在自己的簡書上再次發(fā)表,希望更多人閱讀,并歡迎提出問題和意見。

本文介紹的設(shè)計并非最佳,而是考慮到歷史問題后的折中方案,歡迎大家提出自己的設(shè)計。

一、前言

互聯(lián)網(wǎng)是一個強調(diào)速度的行業(yè),各公司都希望自己的開發(fā)人員能盡可能快地完成需求開發(fā)。但隨著系統(tǒng)和業(yè)務(wù)復(fù)雜度的增加,單靠程序員加班也越來越難以滿足產(chǎn)品提出的業(yè)務(wù)需求,而且單純強調(diào)速度也會留下越來越多的技術(shù)債。那如何設(shè)計開發(fā)業(yè)務(wù)系統(tǒng)才能滿足速度和質(zhì)量的雙重要求呢?

解決這一問題的一個重點是系統(tǒng)的架構(gòu)設(shè)計是否具有良好的擴展性,是否能夠使后來的業(yè)務(wù)開發(fā)能夠輕松而又高質(zhì)量地完成。接下來我將從理論、實踐等幾個方面,介紹我們會員團隊是如何解決這一問題的。

二、微內(nèi)核模式

業(yè)務(wù)系統(tǒng)同其它軟件系統(tǒng)其實并無本質(zhì)的區(qū)別,所以在如何打造高擴展性的業(yè)務(wù)系統(tǒng)這個問題上,可以從優(yōu)秀的同行身上得到借鑒。其中一個很重要的借鑒是微內(nèi)核模式。

很多開源項目,如 Linux、Spring Framework 等,都有一個小而精的內(nèi)核。比如 Linux 的 Kernel、Spring Framework 的 ApplicationContext。這些軟件系統(tǒng)(或框架)在這個核心基礎(chǔ)之上,通過組件(插件)的形式實現(xiàn)更加豐富的功能。這樣的設(shè)計滿足了高內(nèi)聚低耦合、開閉等設(shè)計原則,這就是微內(nèi)核模式的重要意義。

那微內(nèi)核模式是什么呢?這個問題并沒有完整統(tǒng)一的答案。對于一個業(yè)務(wù)系統(tǒng)的微內(nèi)核,我理解它應(yīng)當定義出基本業(yè)務(wù)流程,僅包含基本的業(yè)務(wù)功能實現(xiàn)(最好基本功能也由插件的形式實現(xiàn),而業(yè)務(wù)流程最好也是可定制的)。并且,為業(yè)務(wù)流程上的每一步定義出組件定義。不同場景通過提供不同的組件接口實現(xiàn)來完成。

所以,歸納來說,業(yè)務(wù)系統(tǒng)的微內(nèi)核應(yīng)當做如下的事情:

  1. 流程定義
  2. 組件定義
  3. 組件調(diào)用

這樣,系統(tǒng)即可以實現(xiàn)基本的功能,又可以通過組件的靈活實現(xiàn)和組合滿足不同業(yè)務(wù)場景下的需求。

舉個例子

舉例來說,在電商交易的結(jié)算頁服務(wù)中,需要記載各種信息,包括購物車、收貨人等等信息。這些操作可以抽象為如下的接口:

interface ReceiverLoader {
    // 根據(jù) userId 加載收貨人信息
    Receiver load(Long userId);
}

interface InventoryChecker {
    // 根據(jù)商品 SKU 查詢庫存數(shù)據(jù)
    InventorySummary check(List<String> skus);
}

interface DeliveryLoader {
    // 返回用戶和商品類型(實體、虛擬等)所對應(yīng)的可用的遞送方式
    DeliverySummary load(Long userId, CommodityType commodityType);
}

interface PayTypeLoader {
    // 根據(jù) userId 返回改用所能使用的支付方式
    List<PayType> load(Long userId);
}

對于不同的電商業(yè)務(wù),上述功能會有不同的業(yè)務(wù)需求:

例如,實現(xiàn)收貨人信息加載功能時,如果是對應(yīng)實物購買,需要返回收貨人真實地址;如果是虛擬物品,則不需要真實地址。實現(xiàn)庫存查詢功能時,如果是自營的實體物品,需要查詢自己的庫存系統(tǒng);如果是商旅產(chǎn)品,則需要查詢對應(yīng)的航空公司、酒店等;如果是一些虛擬商品,則可能不需要查詢庫存,因為虛擬物品可能是無上限的。

通過為上述接口提供不同的實現(xiàn)類,再組合搭配,就可以靈活實現(xiàn)不同的場景。

三、組件的組合

上一節(jié)并沒有介紹微內(nèi)核如何將不同的組件組合在一起,如何調(diào)用,以實現(xiàn)不同場景下的需求。本節(jié)就來介紹這方面的內(nèi)容。

簡單來說,組件的組合方式分為兩類:

  1. 靜態(tài)組合
  2. 動態(tài)組合

靜態(tài)組合

首先介紹靜態(tài)組合方式。

所謂靜態(tài)組合,就是軟件在編譯部署之后,組件之間的關(guān)系不再發(fā)生變化的一種組合方式。

靜態(tài)組合的優(yōu)點是實現(xiàn)起來相對簡單,配置也比較簡單。缺點是只能用于一些變化不難么復(fù)雜的業(yè)務(wù)場景。當業(yè)務(wù)場景變的復(fù)雜多變,需要根據(jù)參數(shù)的不同靈活調(diào)用不同組件時,靜態(tài)配置就無法滿足需求了。

對靜態(tài)組合的一個簡單解釋可以通過下面這段 Spring XML 配置來呈現(xiàn):

<!-- 普通商品結(jié)算服務(wù) -->
<bean id="checkoutService" class="com.company.team.CheckoutServiceImpl">
    <property name="receiverLoader" value="defaultReceiverLoader" />
    <property name="inventoryChecker" value="defaultInventoryChecker" />
    <property name="payTypeLoader" value="defaultPayTypeLoader" />
    <!-- more properties -->
</bean>

<!-- 虛擬商品結(jié)算服務(wù) -->
<bean id="virtualCheckoutService" class="com.company.team.CheckoutServiceImpl">
    <property name="receiverLoader" value="virtualReceiverLoader" />
    <property name="inventoryChecker" value="virtualInventoryChecker" />
    <property name="payTypeLoader" value="defaultPayTypeLoader" />
    <!-- more properties -->
</bean>

上面例子中,兩個 bean 是用來實現(xiàn)結(jié)算服務(wù) (CheckoutService) 的,這兩個 bean 使用了同一個實現(xiàn)類 CheckoutServiceImpl。這個類可以看作是結(jié)算服務(wù)的內(nèi)核。它通過與不同的組件(ReceiverLoader、InventoryCheckerPayTypeLoader 等)組合,實現(xiàn)不同場景下的需求。

這樣的設(shè)計方式反映出了一個面向?qū)ο笤O(shè)計原則 —— 組合優(yōu)于繼承

但靜態(tài)組合因為在編譯部署之后不再變化,無法適應(yīng)更加復(fù)雜的業(yè)務(wù)場景,這是靜態(tài)組合的不足。

動態(tài)組合

上面講到了靜態(tài)組合的不足,所以我們需要另一種更加靈活的組合方式。這種組合方式我們可以稱為動態(tài)組合。

如何實現(xiàn)呢?

顯然,在直接使用條件語句調(diào)度組件是不合適的,原因在于它不滿足開閉原則,無法滿足我們對擴展性的要求。

要實現(xiàn)易于擴展的動態(tài)組合設(shè)計,可以使用一些設(shè)計模式:例如表驅(qū)動模式、職責(zé)鏈模式等等。具體如何使用這些設(shè)計模式實現(xiàn)動態(tài)組合這里就不介紹。接下來看看這么實現(xiàn)之后,開發(fā)人員如何去實現(xiàn)新的組件以實現(xiàn)新的業(yè)務(wù)場景:

class NewComponent implements Component {
    @Autowired
    private ComponentRegistry registry;
    
    public boolean canProcess(Request request) {
        return request.getParam1().equals("abc") && request.getParam2().equals("def");
    }
    
    @PostConstruct
    public void init() {
        register();
    }
    
    public void register() {
        registry.register(Component.class, this);
    }    
}

上述形式的組件定義再結(jié)合職責(zé)鏈模式,就能夠?qū)崿F(xiàn)組件的動態(tài)組合和調(diào)度。但是,這樣的形式并不是很理想。原因在于:

  1. 組件的調(diào)度規(guī)則配置既不直觀也不方便
  2. 組件劃分的粒度無法得到統(tǒng)一

這樣的問題,放在比較復(fù)雜的系統(tǒng)里,就會導(dǎo)致系統(tǒng)難以理解和維護的問題。

那如何實現(xiàn)一個直觀方便,易于維護,能夠統(tǒng)一組件劃分粒度的動態(tài)組合方式呢?接下來講結(jié)合實踐給予介紹。

四、實踐

接下來介紹一下愛奇藝會員交易團隊在構(gòu)建易于擴展的業(yè)務(wù)系統(tǒng)方面的實踐。

先簡單介紹一下會員交易系統(tǒng)的業(yè)務(wù)。會員交易系統(tǒng)是整個會員系統(tǒng)中重要組成部分,承載了絕大部分的 VIP 會員收入,功能包括:

  1. VIP 商品的展示和售賣
  2. 影片商品的售賣
  3. VIP 和影片訂單的查詢
  4. 自動續(xù)費的開通、執(zhí)行和管理
  5. 相關(guān)后臺業(yè)務(wù)

同很多實體和商旅類的交易服務(wù)相比,會員交易在庫存、發(fā)貨、物流等方面的需求被簡化了,但另一些需求卻變得復(fù)雜了,例如:

  • 需要支持更加豐富多樣的支付方式和客戶端平臺
  • 需要支持更加豐富多樣的優(yōu)惠促銷活動
  • 需要支持多種形式的自動續(xù)費
  • 需要支持各級別會員的升級購買

這些復(fù)雜的需求再加上緊張的業(yè)務(wù)排期,使得代碼越來越復(fù)雜和難以維護擴展。靜態(tài)代碼檢查發(fā)現(xiàn),代碼的復(fù)雜度和代碼行數(shù)之比接近1比3,代碼重復(fù)率達到了20%,而實際情況則更為嚴重

這些問題導(dǎo)致了新需求實現(xiàn)緩慢、技術(shù)優(yōu)化長期難以推進等問題。

為了解決這些問題,我們重新設(shè)計了訂單支付服務(wù)

首先我們重新設(shè)計了訂單支付服務(wù)的架構(gòu)。將訂單支付的核心流程交由核心模塊實現(xiàn),各個場景下的特殊邏輯由支付處理類或配置文件的方式定制實現(xiàn):

image

然后梳理核心流程

image

首先,在第4步,訂單支付服務(wù)會根據(jù)支付處理器上的注解配置或支付處理配置文件的定義,選擇出合適的支付處理類。具體的配置方式如下:

注解形式的配置

@PayRequestMapping(
    payTypes = {ALIPAY, ALIPAY_V3},
    platformTags = {PlatformTag.H5}
)
@Component
class AliPayH5OrderPayHandler implements OrderCustomizer, GatewayPayRequestCustomizer {
  @Override
  public void customize(Order order, PayContext payContext) {
    ...
  }
    
  @Override
  public void customize(GatewayPayRequest gatewayPayRequest, PayContext payContext) {
    ...
  }    
}

文件形式的配置

{
  "handlers": [
    {
      "name": "WeChatOrderContract",
      "mapping": {
        "payTypes": [
          379
        ],
        "platformTags": [
          "h5"
        ]
      },
      "gatewayPayRequest": {
        "extendParameters": {
          "isFirstSign": "yes"
        }
      },
      "resultType": "DIRECT_AND_JSON"
    }
  ]
}

注解形式的配置常用于需要代碼實現(xiàn)動態(tài)邏輯的場景,而對于比較簡單的場景,僅需通過配置文件即可完成適配。

在找到合適的支付處理類之后,會根據(jù)其代碼實現(xiàn)(注解形式)或配置文件的內(nèi)容(配置形式)進行自定義操作。在上面流程圖的第6、9步都會進行相應(yīng)的自定義操作。

在進行上述改進之后,大部分簡單的訂單支付服務(wù)的業(yè)務(wù)實現(xiàn)就只需增加新的配置即可。而比較復(fù)雜的場景,因為擴展組件的明確定義和聲明式的組件路由,因此開發(fā)效率也得到的極大的提升,代碼復(fù)雜度大大降低。

五、Navi 項目

由來

在重新設(shè)計訂單支付服務(wù)之后,我們發(fā)現(xiàn)其它的一些服務(wù)也需要類似的設(shè)計:即通過聲明式的規(guī)則定義,動態(tài)地選擇合適的組件。

那如何讓其它項目也能輕松的擁有這樣的能力呢?于是便有了設(shè)計開發(fā)一個通用框架實現(xiàn)這樣功能的想法。這就是 Navi 項目的由來。

項目地址:https://github.com/yanglifan/navi

Navi 項目能做什么呢?簡單來說,Navi 能夠通過簡單、靈活、豐富的聲明式配置,實現(xiàn)動態(tài)的組件選擇功能。

接下來我將舉幾個栗子。

簡單示例

interface OrderCreateHandler { // 1
    void handle(Order order, OrderCreateRequest request);
}

@EqualMatcher(property = "clientType", value = "android") // 2
@VersionMatcher(range = "[1.0.0,2.0.0)") // 2
@Component
class AndroidV1Handler implments OrderCreateHandler { // 2
    void handle(Order order, OrderCreateRequest request) {
    }
}

@EqualMatcher(property = "clientType", value = "android") // 2
@VersionMatcher(range = "[2.0.0,3.0.0)") // 2
@Component
class AndroidV2Handler implments OrderCreateHandler { // 2
    void handle(Order order, OrderCreateRequest request) {
    }
}

public class OrderService {
    public OrderCreateResult createOrder(OrderCreateRequest req) {
        // 略        
        OrderCreateHandler handler = selector.select(req, OrderCreateHandler.class); // 3
        if (handler != null) {
            handler.handle(order, req);
        }        
        // 略
    }
}

上面這段示例中,OrderCreateHandler 是一個業(yè)務(wù)組件的接口定義。它有兩個實現(xiàn)類:AndroidV1HandlerAndroidV2Handler,分別用來實現(xiàn)安卓客戶端 1.0 和 2.0 下不同的需求。即當 1.0 版本的 Android APP 調(diào)用時,選擇 AndroidV1Handler,2.0 版本的 Android APP 調(diào)用時,選擇 AndroidV2Handler。

在 Navi 中,組件選擇的過程通過執(zhí)行如下語句實現(xiàn):

 OrderCreateHandler handler = selector.select(req, OrderCreateHandler.class);

selector 的類型為 Selector,意為組件選擇器,簡稱選擇器。在 Spring 應(yīng)用中推薦使用其實現(xiàn)類 SpringBasedSelector。

組件選擇參數(shù):select 方法的第一個入?yún)?req 是組件選擇參數(shù),組件選擇器會根據(jù)其中的數(shù)據(jù)與組件上定義的匹配規(guī)則進行比較,從而選擇出滿足條件的組件。

匹配器注解

要想組件按照期望被選擇出來,就需要用到匹配器(Matcher)注解。以上面的代碼為例進行說明

  • @EqualMatcher(property = "clientType", value = "android")
    這個注解的意思是組件選擇參數(shù)(后續(xù)說明)中的 clientType 的值等于 android。此時,這條匹配條件就是命中,否則就會拒絕。
  • @VersionMatcher(range = "[2.0.0,3.0.0)")
    這個注解的意思是組件選擇參數(shù)中的 version 字段的值表示一個版本(格式必須為 major.minor.patch 格式,每一段必須為數(shù)字類型)。而這個版本符合 range 所定義的范圍之內(nèi)。方括號表示閉區(qū)間,小括號表示開區(qū)間。這個與數(shù)學(xué)中的定義相同。

Navi 自建的匹配器除了上面兩個外,目前還有:

  • ContainMatcher 組件選擇參數(shù)對應(yīng)字段的值包含期望的值(字符串 contains 操作)
  • IntersectMatcher 組件選擇參數(shù)對應(yīng)字段的值(集合類型或用分割符分割的字符串類型)與期望的值有交集

注:匹配器之間的關(guān)系都是「與」

組合匹配器

當需要在組件上同時使用多個匹配器的時候,可能會導(dǎo)致在同一個業(yè)務(wù)功能上,組件選擇的維度不統(tǒng)一,從而對功能的理解、維護造成困難。為了解決這個問題,達到簡化和統(tǒng)一匹配器使用的目的,Navi 引入了自定義匹配器的功能。

下面是一個例子:

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@IntersectMatcher(property = "platform.tags")
@VersionMatcher(property = "appVersion")
@CompositeMatcherType
public @interface StoreViewHandler {
    String ALL_PLATFORMS = "*";

    @AliasFor(annotationFor = EqualMatcher.class, attributeFor = "value")
    String[] platform() default ALL_PLATFORMS;

    @AliasFor(annotationFor = VersionMatcher.class, attributeFor = "range")
    String clientVersionRange() default "";
}

@StoreViewHandler(platform = "h5", clientVersionRange = "[1.0.0,2.0.0]")
class Component {    
}

通過自定義一個注解,并在其上配置 Navi 內(nèi)置的匹配器注解,你就能得到一個新的匹配器注解。

注:匹配器注解不能無限制疊加,只能有一層的復(fù)合關(guān)系。

后續(xù)計劃

目前 Navi 只發(fā)布了兩個版本,后續(xù)還有更多功能:

  1. 函數(shù)式的配置
    注解方式的配置簡單易用,但存在不能覆蓋的場景,所以計劃提供函數(shù)式的配置方式
  2. 透明化的組件選擇
    不再通過 Selector 顯式選擇組件,而是為組件生成代理,通過代理選擇實際的組件
  3. 組件多選
    目前選擇器只會返回單個組件,后續(xù)會支持選擇多個組件,根據(jù)優(yōu)先級依次調(diào)用的功能

六、總結(jié)

相比高可用、高并發(fā)等話題,擴展性是相對冷門的。但系統(tǒng)的擴展性與技術(shù)人員的日常工作關(guān)系更為緊密,對企業(yè)業(yè)務(wù)發(fā)展速度也有顯著的影響。

但如何設(shè)計易于擴展的系統(tǒng)并不像設(shè)計高可用、高并發(fā)系統(tǒng)那樣有著很多可供使用的技術(shù)、可供參考的設(shè)計模式。因為沒有哪兩家公司的業(yè)務(wù)是完全相同的,而業(yè)務(wù)上的差異就會帶來系統(tǒng)設(shè)計上的差異。所以,從這個角度講,是否能設(shè)計出易于擴展的系統(tǒng)更加能反映程序員的技術(shù)水平和設(shè)計能力。

任何公司的發(fā)展都是由小變大的,在公司發(fā)展初期,并不需要過分考慮系統(tǒng)擴展性的問題。但在公司不斷發(fā)展,業(yè)務(wù)不斷復(fù)雜的過程中,其背后的業(yè)務(wù)系統(tǒng)的可擴展性的重要性就會不斷提高。當發(fā)展到一定規(guī)模時,新提出的業(yè)務(wù)需求如果還是需要重復(fù)建設(shè)的話,那其所需的成本將會變得難以接受。

所以,有不少公司在推進業(yè)務(wù)平臺的建設(shè),通過提供完善的、可靈活定制的業(yè)務(wù)能力,以支持新業(yè)務(wù)的快速上線。而這些業(yè)務(wù)平臺的建設(shè),其實就反映出對系統(tǒng)擴展性不斷提高的要求。

本篇文章介紹了一些系統(tǒng)擴展性方面相關(guān)的理論與實踐。但實際情況其實遠比文中介紹的更加復(fù)雜,所以在系統(tǒng)擴展性方面,還有更多值得思考和討論的內(nèi)容。

我的技術(shù)公眾號“編走編想”
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • Android 自定義View的各種姿勢1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 179,351評論 25 708
  • 用兩張圖告訴你,為什么你的 App 會卡頓? - Android - 掘金 Cover 有什么料? 從這篇文章中你...
    hw1212閱讀 14,118評論 2 59
  • Spring Boot 參考指南 介紹 轉(zhuǎn)載自:https://www.gitbook.com/book/qbgb...
    毛宇鵬閱讀 47,288評論 6 342
  • 回想那個曾經(jīng)牽個小手都能臉紅的年代,一句話就是一輩子的年代,我送你一個小禮物便代表我愛你的年代,在池塘邊悄悄翻看情...
    戀愛跡閱讀 968評論 0 0
  • 最近被林丹出軌事件刷屏了,各種文章眾說紛紜。其實這類事件在我們身邊隨處可見,只是因為林丹頭戴光環(huán)而被過分關(guān)注。而我...
    Teresa999閱讀 231評論 0 0

友情鏈接更多精彩內(nèi)容