架構詳解——淘系圈品進化史

引言


商品數據是營銷的基礎,很多營銷工具最終都會涉及到商品數據的處理,比如打標、修改商品的feature、調用各種下游系統,單個商品可以通過同步方式處理,實際業務上會依據一定業務規則圈定大量商品並對其進行處理,因此,卡券商品設置引擎應運而生。

卡券商品設置引擎(俗稱圈品)的作用是,依據一定的業務規則從數據源獲取商品,篩選符合規則的商品並按照業務自定義的操作設置商品優惠。設置商品優惠主要是圍繞商品中心、營銷中心等多個域進行操作,圈品的一個重要能力就是保障商品優惠設置後各個域的數據一致性。商品數據經常發生變化,變化後可能會使商品不符合圈品規則,圈品另外一個重要能力就是能夠監聽全量的商品中心變更。卡券商品設置引擎全局視角圖如下所示。

圈品三個關鍵要素:數據源、規則、業務處理,三要素都支持橫向擴展。數據源是圈品的數據來源,不同的數據源接入方式和查詢方式不同。規則用於數據過濾,只有符合規則的數據才能接下去處理。符合規則的數據在業務上需要進行一定的處理,業務處理可以自定義。

從2017年發展至今,圈品經歷了4個雙11以及數不清的大促和日常活動,圈品目前擁有千萬級商品實時處理能力、數據一致性保障能力、監聽全量商品變更能力以及平臺化能力等。

本文將圈品的發展劃分爲兩個階段,第一個階段,奠基了圈品的架構,第二階段,提升了系統的穩定性和性能、增加了一致性保障能力。



第一階段



▐  概述


生命週期

圈品通過活動概念來進行生命週期的管理,圈品池關聯了規則和業務,圈品池詳情是商品的集合,商品處理完成後會保存到商品池詳情中,活動、圈品池、圈品池詳情模型如圖2.1所示。

一個活動可以關聯多個圈品池,一個圈品池只屬於一個活動,圈品池設置圈品規則後會按照業務自定義的動作完成商品的處理,活動過程中商品發生變化時會產生商品變更消息,圈品會通過監聽商品變更消息動態處理商品,活動結束後會觸發結束後的動作,生命週期如圖2.2所示。

首次設置圈品池規則後會觸發圈品從數據源拉取全量商品進行處理,我們稱之爲全量圈品。在全量圈品完成後,數據源會發生變化或者商品信息發生變化,這些發生變化的商品需要重新經過圈品處理,稱之爲增量圈品。

圖2.1 卡券商品設置引擎模型圖

圖2.2 卡券商品‍p‍設置引擎生命週期


系統架構

圈品框架圖如圖2.3所示,圈品可以劃分爲四個模塊,分別是數據源模塊、動作模塊、規則模塊和業務處理模塊,設置端劃分爲三個部分:活動設置、圈品池設置、規則設置。接下來將圍繞圈品四個核心模塊進行講解。

圖2.3 圈品框架圖


▐  數據源模塊


數據源模塊是圈品的數據源來源,下面主要分四類進行講解,分別是商品列表、商家列表、同步庫表以及商品變更消息。

這些數據源又衍生出了多種圈品方式,比如商品列表圈品方式、賣家列表圈品方式、大促現貨圈品方式、營銷站點圈品方式、賣家大促商品圈品方式、飛豬賣家圈品方式、新零售攤位圈品方式等等。拿大促現貨圈品方式舉例,每次大促營銷平臺招商都要招現貨商品,大促現貨圈品池方式可以圈指定大促的全部現貨商品,也可以結合類目、商品標等其他規則過濾商品。


商品列表

商品列表是最簡單的數據源,直接通過商品ID指定數據源的商品範圍,因爲是直接填商品ID的方式,網絡傳輸限制最大支持10W商品。全量圈品過程中從圈品規則中獲取全量商品進行處理,增量圈品是通過監聽商品變更消息進行處理。


賣家列表

賣家列表數據源是賣家ID的集合,通過指定賣家ID來確定商品範圍。全量圈品的過程中根據賣家ID從店鋪搜索接口中獲取賣家商品,增量圈品是通過監聽變更消息,賣家新發布商品或者變更商品都會觸發商品變更消息,通過監聽商品變更消息便可以進行增量圈品。


同步庫表

同步庫表是值使用精衛同步原數據庫到新的數據庫供圈品使用,採用這種方式比較靈活且不會對原數據源產生影響。全量圈品是通過掃表方式獲取全量的數據。增量圈品有兩個渠道,第一是通過精衛監聽數據庫的變更,第二是監聽商品變更消息。

根據數據源的特性又可以衍生出多種圈品方式,營銷平臺招商數據源支持大促現貨圈品方式、營銷平臺站點方式、賣家大促商品圈品方式等,新零售商品數據源支持攤位和業務身份圈品方式。


商品變更消息

由於商品信息變更會導致商品不符合規則,需要對變更的商品進行增加或刪除,比如小二設置圈選某個類目的商品,賣家可以對商品類目進行編輯,原來符合類目規則的商品變得不符合類目需要刪除,原來不符合類目的商品現在符合類目需要進行增加。商品信息的變更都會觸發商品變更消息,所以增量圈品中都有一種途徑就是處理商品變更消息。

商品變更消息日常平均qps在1w左右,峯值QPS可達4w多。在這一階段,因爲每一個圈品池規則都是獨立的且無法確定一個商品與商品池的關係,所以每個圈品池都處理了全量商品變更消息。假設商品變更消息QPS是1w,當前有效圈品池有5000個,那麼圈品系統實際處理商品變更消息QPS是5000W。因此只有進行本地計算的規則才能支持處理商品變更消息,即使是這樣,圈品系統也嚴重消耗機器性能,曾經圈品系統有600多臺機器,CPU使用率也達到了60%以上。


▐  規則模塊

框架設計

圈品規則模型類圖如圖2.4所示,ItemPoolRule是圈品池規則類,其中relationRuleList是圈選規則,exclusionRuleList是排除規則,一個商品必須符合圈選規則且沒有命中排除規則,這個商品纔算符合圈品池的規則。RelationRule是具體規則內容,RuleHandler是規則處理接口,所有規則必須實現RuleHandler,如ItemTagRuleHandler——商品標規則handler、SellerRuleHandler——賣家規則handler等。

圖2.4 規則模型類圖


規則樹

規則樹設計如圖2.5所示,每個節點表示一個規則節點,頂級規則必須是可以做爲數據源的規則,如商品列表規則、賣家列表規則等。判斷商品是否符合規則可以定義爲:一個商品如果符合從頂級規則到某個葉子鏈路上所有規則節點(即從規則樹中可以找到一條從頂級規則通往任意葉子節點的鏈路),則認爲該商品符合規則。

圖2.5 規則樹設計

爲了更好的理解,舉個例子,如下圖2.6所示,運營通過商品列表方式進行圈品,左邊鏈路是圈商品列表中符合二級類目規則的商品,右邊鏈路是圈商品列表中符合一級類目規則以及指定商品標的商品。

圖2.6 規則樹舉例


頂級規則

由於該章節與“分批處理模塊”章節耦合較強,因此可以先看下面章節後,再看該章節。

頂級規則即是規則也是數據源,圈品從頂級規則中獲取數據源中所有的商品。商品列表圈品方式做爲頂級規則時,規則內容包含商品ID,這些商品ID就是數據源的商品。賣家列表圈品方式做爲頂級規則時,規則內容包含賣家ID,從頂級規則獲取商品ID時,根據賣家ID調用店鋪搜索接口獲取商品ID。大促現貨圈品方式做爲頂級規則時,規則內容包含的是大促現貨活動ID,從頂級規則獲取商品ID時,根據活動ID從同步過來的招商現貨表中拉取商品。


★ 侷限性

從“分批處理模塊”章節可以知道,這一階段圈品都是先count規則中包含商品總數,然後分頁處理,這種方式存在侷限性,當頂級規則變得複雜的時候,就沒辦法處理了。

舉個稍微複雜的例子,賣家列表圈品方式規則內容包含很多個賣家時,如何處理呢?這一階段圈品的處理方式跟圖9縱向處理方式一樣,找到所有賣家中擁有最大商品數量做爲count,然後分頁處理,每一頁的處理過程中都需要循環所有賣家,當賣家數量越大時,每頁包含的商品數量就越大,因此該圈品方式限制了最多隻能指定300個賣家。

再舉個複雜的例子,假設一個品牌團中有多個賣家,一個賣家有很多商品,現在需要圈選多個品牌團下面所有賣家的所有商品,如何做呢?這一階段圈品還無法處理這麼複雜的規則,具體做法詳見第二階段。


▐  分批處理模塊


分佈式處理

圈品將全量商品進行分頁處理拆分成很多部分,然後通過metaq進行分佈式處理,流程圖如圖2.7所示。

當觸發全量圈品的時候會產生一條記錄規則變化的metaq消息,規則變化消息通過規則變化動作模塊進行處理。規則變化動作模塊首先計算數據源中最大可能的商品數量,然後再通過分頁處理分成很多部分,每一部分產生一條商品增加類型的消息,商品增加消息通過商品增加動作模塊進行處理。商品增加動作模塊首先從數據源拉取該部分對應的商品ID集合,然後過濾圈品池規則,最後選擇對應的業務進行處理。

圖2.7 分佈式處理流程圖


分頁處理

分頁處理首先是計算全部最大可能的商品數量,然後按照固定間隔進行分頁,商品增加和商品刪除消息包含的關鍵信息是:start、end,

拿最簡單的商品列表圈品方式來舉例,假設運營填入了5w個商品ID,那麼分頁處理可以是500個商品ID做爲一頁,第一頁start=0、end=500,最後一頁start=49500,end=50000,每一頁需要處理的商品ID都是確定的。

但是數據源往往不是這麼簡單,拿一個稍微複雜的大促現貨圈品方式舉例,從招商同步的現貨商品存儲在64張表中,按照商品ID進行分庫分表,其中大促活動ID是索引字段,如何高效獲取指定大促活動的全部商品ID呢。數據量較小的時候,我們可以通過數據庫count和limit分批取出,數據量大的時候使用limit就會有大翻頁問題。

爲了避免使用limit在大翻頁時性能差的問題,圈品的處理方式如下圖2.8所示,把它看成橫向方式。首先通過大促活動ID,計算每張表的min(id)、max(id),總數count就等於所有表max(id)-min(id)相加,然後按間隔劃分任務,實際間隔是5000,爲了畫圖方便圖中間隔是10,因此每個商品增加消息包含的信息只需要start和end。

圖2.8 橫向分頁處理

在處理商品增加消息時,需要循環64張表中求min和max直到找到該start和end在哪張表中,然後在該表中根據start和end取出符合的商品,核心代碼邏輯如下所示。

public List<CampaignItemRelationDTO> getCampaignItemRelationList(int start, int end,
                                                                     Function<Integer, Long> getMaxId,
                                                                     Function<Integer, Long> getMinId,
                                                                     Function<CampaignItemRelationQuery, List<CampaignItemRelationDTO>> queryItems) {
        List<CampaignItemRelationDTO> relationList = Lists.newArrayList();
        for (int i = 0; i < 64; i++) {
            //min以及max的值均走緩存,不會對db產生壓力
            long minId = getMinId.apply(i);
            long maxId = getMaxId.apply(i);
            long tableTotal = maxId - minId + 1;
            if (minId <= 0 || maxId <= 0) {
                continue;
            }
            //起始減本表內總量,如果大於0,則一定是從下一張表開始的,直接跳出循環,降低start以及end繼續
            if (start - tableTotal > 0) {
                start -= tableTotal;
                end -= tableTotal;
                continue;
            }
            // 進入到這裏,說明一定已經有一部分落在這裏了,那麼繼續遍歷取值
            // 先判定是否是最後一張表,如果是,則去除需要的 ,然後返回,如果不是最後一張表,那麼需要取出本張表中所需的數據,然後進行下次迭代
            // 判定爲最後一張表的條件是 表的起始點+pageSize < maxId,即(minId+start)+(end-start) <= maxId,簡化爲 minId + end <= maxId
            if (minId + end <= maxId) {
                //如果minId + end 還小於本表的最大值,那麼說明min以及max均落入了表內,那麼只取本表的數據即可
                relationList.addAll(queryItems.apply(getQuery(start + minId, end + minId, i)));
                break;
            } else {
                //走入這裏,說明數據進行了跨表
                //首先取出本表符合條件的全部數據,然後將起始值設置爲0,然後降低
                relationList.addAll(queryItems.apply(getQuery(start + minId, maxId, i)));
                //新的結束值應該爲pageSize-當前表中取得的數量總量,即(end-start)-(tableTotal-start),簡化後得到end-tableTotal
                end = (int) (end - tableTotal);
                start = 0;
            }
        }
        return relationList;
    }

    這種處理方式存在幾個缺點:

    1. 對於數據集中的表來說是一種不錯的方法,但對於數據稀疏型表來說就非常低效,如果數據分佈很稀疏,count很大,分批處理後任務數量非常大,最後獲得的商品ID也就幾百個,比如,新零售圈品方式由於框架限制,也採用了一樣的分頁處理方式,一次全量圈品商品增加消息量可達20w,實際可能只獲得了幾百個商品。

    2. 每個消息處理都需要循環查詢很多張表直到start、end所在的那張表,通過max和min判斷start和end是否出自該表,頻繁取max、min也會給DB造成壓力,爲了避免對DB的壓力,又需要利用緩存max、min。

針對於第一個缺點:數據稀疏型的數據源消息數量過大,可以在不改動框架的同時進行改善,只需換個角度計算總數count,如下圖2.9所示,count取的是所有表中的最大值和最小值的差,這樣即使是稀疏型數據源,count值也不會很大,然後任務處理的時候根據start和end循環從所有表中取出對應的商品ID。而且這種方式也會稍微減少取max和min的次數。如果密集型數據源採用這種分頁處理方式,將會導致單頁數據量過大問題。

圖2.9 縱向分頁處理方式

針對於第二個缺點:頻繁取max和min問題,上面的處理方式是用全局的眼光計算count,然後分頁處理,因此無法直接定位start和end應該取自哪張表,其實,可以針對於每個表單獨分頁處理,消息中不僅包含start、end,還包含分表的index信息。但是這種方式依然存在對稀疏型數據源劃分任務數過多的問題,而且現在圈品分批框架也不支持這種方式。


▐  動作模塊


動作模塊的作用是處理圈品metaq消息,動作模塊與消息類型是一一對應的,動作模塊分爲:規則變化、商品增加、商品刪除。


規則變化動作

規則變化動作模塊處理規則變化類型的消息,該動作主要處理流程是,調用分批處理模塊進行分批,然後將每批包含的信息通過metaq發送出去,也就是產出商品增加和商品刪除消息。


商品增加動作

商品增加動作模塊處理商品增加類型的消息,動作處理流程圖如下圖2.10所示。

圖2.10 商品增加動作處理流程圖


商品刪除動作

商品刪除動作模塊處理商品刪除類型的消息,動作處理流程圖與圖2.10類似,只是最後業務處理模塊調用商品刪除處理的方法。


▐  業務處理模塊


業務處理模塊框架類圖如圖2.11所示,每一種業務都需要實現TargetHandler,其中handle方法處理圈品增加,rollback方法處理圈品刪除。目前已經接入的幾個大業務分別是:品類券、免息券、會員卡等。

圖2.11 業務處理類圖


▐  階段總結


這一階段,圈品從無到有,誕生於品類券,又脫胎於品類券,在業務方面,支撐了品類券、免息券、會員卡等業務,在性能方面,能處理百萬級甚至千萬級商品。系統是在不斷髮展中完善,這一階段的圈品存在以下不足點。


處理商品變更消息性能問題

2.2.4中講解了處理商品變更的必要性以及存在的問題,當有效的圈品池越來越多時,處理商品變更消息QPS越來越高,系統性能越來越差,而且很多規則需要調用HSF或者查詢緩存之類的耗時操作,因此這些規則無法支持處理商品變更消息。這一階段,承載圈品系統集羣CPU一直都在50%以上,即便集羣擁有600多臺機器。


複雜頂級規則處理問題

面對複雜頂級規則,圈品沒有很好的辦法處理,然而在業務快速變化情況下,圈品需要有能力應對複雜規則,即使目前沒有出現太複雜的頂級規則,圈品在處理賣家列表圈品方式也存在侷限性。


系統穩定性和可控性問題

  1. 穩定性問題:通過2.4節可以瞭解到,在進行大量圈品的時候,只要觸發圈品變更,規則變化消息立馬會裂變出更多的圈品消息,圈品metaq消息堆積量可達到百萬,由於下游系統限流導致大量異常,系統負載又高,消息處理又耗時長,metaq消息處理存在雪崩風險,有時一條消息重複處理上萬次。

  2. 可控性問題:由於觸發圈品變更時,會立馬裂變出更多的消息,消息大量堆積時,圈品不能選擇性處理、不能停止處理消息、不能選擇性忽略消息等等,這就意味着系統發生問題的時候,沒有抓手進行控制,只有眼巴巴的看着。舉個實例,兩條消息重複執行幾萬次,一個是刪除該商品,一個是增加該商品,不停的給商品打標去標,商品產生大量商品變更,導致搜索引擎同步延遲,當時就只能眼巴巴看着。再舉個例子,由於某一種圈品規則代碼有bug會導致fullGc,然後該規則相關的圈品池產生了大量消息,由於無法選擇性處理消息,導致整個圈品系統癱瘓。


分批處理缺陷問題

在2.4.2章節中講到了第一階段分頁處理的缺陷,不同數據源的分頁處理不能一概而論,框架應該給予更多的靈活性。


數據一致性問題

在進行大量圈品時,系統或下游系統異常無法避免,所以數據有可能存在不一致的情況。對於業務來說,該增加的商品沒有增加,可能還能接受,如果該刪除的商品沒有刪除,那麼就很可能資損了。



第二階段



▐  概述


第二階段,針對於第一階段的問題進行優化,新架構圖如圖3.1所示,其中黃色部分是新增部分。圈品總體可以劃分爲六大塊,分別是數據源模塊、動作模塊、規則模塊、業務處理模塊、調度模塊和設置端。調度模塊是新增部分中最重要的,首先新增了任務的模型,如圖3.2所示,任務會先保存到DB中,scheduleX秒級定時觸發調度邏輯,最後通過metaq分發任務進行分佈式處理。圖中紅色線條表示全量圈品的流程,圖中橘黃色表示增量圈品的流程。下面將分別詳細介紹新增部分。

圖3.1 第二階段圈品架構圖

圖3.2 任務模型


▐  商品變更消息優化


商品變更消息處理流程圖如圖3.3所示。第一步,建立圈品池與商品的泛化關係,第二步,通過Blink根據商品與圈品池的泛化關係過濾商品變更消息,剩下少量的商品變更消息,第三步,根據圈品池與商品的泛化關係判斷哪些圈品池需要處理該商品變更消息。


過濾後的商品變更消息日常平均qps在200左右,而且只有與該商品相關的圈品池才需要處理該商品變更消息,因此,具體到某些圈品池上來看,其處理商品變更消息的qps在100以內,同時系統性能消耗也大大降低,集羣機器從巔峯時期700多臺降低到現在300多臺(由於集羣還承載其他業務,實際圈品需要的機器數量可以壓縮到100臺以內)。

圖3.3 商品變更消息流程圖

商品變更消息過濾的關鍵點在於如何建立圈品池與商品的泛化關係,這裏的思想是根據具體規則儘量大範圍的圈定可能的商品。

比如賣家圈品方式,當小二填寫賣家列表後,這個圈品池與哪些賣家有關係就已經確定了,除此之外的賣家肯定不會跟這個圈品池發生關係,因此可以將賣家與圈品池的關係存入tair,供Blink過濾商品變更消息使用。賣家與圈品池的關係是比較通用的思路,其他圈品方式也可以轉化成這種關係,比如商品列表圈品方式,當小二填入商品ID後,這些商品屬於哪些賣家就確定了,除此之外的賣家的商品不會與該圈品池發生關係。

當然,賣家與圈品池的關係也有不適用的時候,比如大促活動圈品池方式,一次大促活動可能有幾十萬的賣家參與,而且賣家會不斷的報名參加大促,因此很難獲取圈品池與賣家的關係。針對大促活動圈品方式,可以建立tmc_tag與圈品池之間的關係,大促商品都有統一的tmc_tag,因此可以通過將tmc_tag與圈品池的關係存在diamonds供Blink過濾商品變更消息使用。總之,其他圈品方式根據具體規則找到圈品池與商品的泛化關係,可以通過商品上的信息和泛化關係判斷商品與商品池是否存在關係。


▐  複雜頂級規則處理


維度定義        

第一階段中已經解釋了頂級規則是能夠做爲數據源的規則,爲了更好支持複雜數據源規則,引入了維度的概念,然後通過降維將複雜規則變成簡單規則,最後的目的是從數據源中獲取所包含的商品ID。

定義1:單個商品ID爲零維,即沒有維度

定義2:能夠直接獲取多個商品ID的規則爲一維,例如商品列表規則,單個賣家規則

定義3:二維規則由多個一維規則組成,例如多個賣家規則

定義4:三維規則由多個二維規則組成,更高維規則由多個比它低一維的規則組成

從上面定義可以看出,賣家列表規則既有可能是一維規則,也有可能是二維規則,當規則只包含一個賣家時爲一維規則,當規則包含多個賣家時爲二維規則。爲了更好理解,拿上面的複雜規則來講解,一個賣家有很多商品,一個品牌團有很多商家報名,如果現在運營設置圈多個品牌團下面所有商品,下圖3.5所示是該規則降維的過程。

圖3.5 規則降維過程


規則變化動作調整

在第一階段,規則變化動作處理流程就是調用分批處理模塊進行分批,然後將每批包含的信息通過metaq發送出去,也就是產出商品增加和商品刪除消息。現在,規則變化動作處理流程調整爲如下圖3.6所示,首先需要判斷規則是否爲一維規則,只有一維規則才能直接通過分批處理,否則就要進行降維,產生的降維任務由規則降維動作進行處理。

圖3.6 新規則變化動作處理流程圖


增加規則降維動作

規則降維動作處理規則降維類型的任務,動作處理流程圖如下圖3.7所示。RuleHandler中自定義的降級方法指定了下一維度的規則,因此一次降維任務只能將規則降低一個維度。

降維只針對做爲數據源的頂級規則,因此,首先遞歸獲取頂級規則,接着調用自定義降維方法處理頂級規則後得到更低一維度的頂級規則集合,然後使用降維後的頂級規則替換規則樹中的頂級規則得到新的規則樹集合,最後,將新規則樹生成規則變化任務,由規則變化動作判斷是否繼續降維。

圖3.7 規則降維動作處理流程圖


  新增調度模塊


任務調度

新增任務模型如圖3.2所示,任務相當於第一階段中的圈品消息,第二階段中任務是需要先落庫,然後由調度器來進行調度的。       

調度器是任務扭轉的動力,所有類型的任務都會插入DB中由調度器統一調度。任務表中已完成的任務會隔一段時間清理,即使是這樣,任務表也有可能存在幾百萬任務,而且圈品的速度很大程度由調度器決定,因此對調度器的性能要求是很高的,不僅如此,調度器應該具備更多的靈活性。

調度器處理流程圖如圖3.8所示,通過scheduleX秒級定時觸發調度邏輯,然後通過metaq分發任務ID,其實分發任務ID也可以通過scheduleX來完成,最初的實現也就是通過scheduleX來進行任務ID的分發,最後還是改成了通過metaq來分發任務,因爲scheduleX分發大量任務時存在不可接受的延遲。

講回到圖3.8,任務調度的基礎是知道未完成任務的分佈,爲了避免統計未完成任務分佈時產生慢sql,這裏做了一個很重要的動作,即下文第一步。

第一步,首先獲取未完成任務所屬的圈品池ID的分佈,由於這裏只根據狀態統計圈品池ID,狀態和圈品池都有索引,利用了覆蓋索引,因此性能很高;

第二步,隨機選擇十個圈品池保證任務調度分配均衡,同時減少任務統計的耗時;

第三步,統計這十個圈品池的未完成任務數量的分佈;

第四步,根據第三步的數量統計以及系統配置,分配每個圈品池參與調度的任務數量;

第五步,根據任務分配數量獲取任務ID;

第六步,通過metaq將任務ID分批發送到不同的機器進行處理;

第七步,接收metaq消息;

第八步,將任務ID提交異步處理,這裏爲了提升處理速度,維護了一個線程池,任務ID只需要提交到阻塞隊列中;

第九步,任務ID提交異步處理後,立馬更新任務狀態爲處理中,避免任務再次被調度,處理中的任務不屬於未完成的任務。

圖3.8 任務調度流程圖

對比第一階段中圈品消息模式,第二階段任務首先保存到DB,然後由調度器進行調度,調度器能夠提供更多的靈活性,可以獲取以下優點:

  1. 任務優先級可根據圈品池進行調整,部分圈品池出現問題不會影響整體;

  2. 任務調度速度可調整、可暫停,可以根據任務類型分配處理速度;

  3. 任務調度可監控、可精確統計;

  4. 圈品過程可查詢、可追蹤;


任務統計

一個完善的平臺少不了系統可視化,任務處理進度是可視化中重要的部分。任務數量統計就少不了group by和count,任務表最大的時候可能存在上百萬的數據,同步方式進行統計肯定是不行的,因此採用如下圖3.9所示異步方式。利用覆蓋索引方式得到圈品池ID的分佈,每個圈品池的任務不會很大,因此每個圈品池分開統計將不會產生慢sql。

圖3.9 任務統計思路


分批處理新思路

在第一階段中提到了分批處理的缺陷問題,這裏將討論如何解決整個問題,新的分批處理方式還在開發當中,設計思路按照該章節所講。


★ 框架設計

圈品擁有各種各樣數據源,每種數據源的特性都有不同,所以無法用一種通用的分批方式處理所有數據源。因此圈品分批處理的框架應該更加通用,讓每種數據源都能自定義自己的分批處理方式。

在框架方面的調整如下圖3.11所示,新增基礎分批對象Pageable,考慮到和老框架到兼容,Pageable包含老框架的使用的分批參數start、end、pageSize,自定義分批對象TablePageable或其他都繼承自Pageable,RuleHandler增加自定義分批方式getPageabelList,老框架的分批方式可以寫在AbstractRuleHandler的getPageableList中,需要自定義分批方式的RuleHandler覆蓋getPageabelList便可。

圖3.11 分批處理新框架類圖


★ 分頁處理思路

在第一階段中,分頁處理爲了避免limit大翻頁問題,採用了通過主鍵id進行分頁的方式。在這裏先討論下爲什麼limit會存在大翻頁問題,以及優化方案。

如下sql所示,當N值很大時,這個sql的查詢效率會很差,併發查詢時甚至會拖垮數據庫,因爲執行這個sql時需要先回表查詢N+M行,然後根據limit返回M行,前面查詢的N行最後被丟棄(具體討論可參考limit爲什麼會慢)。一般遇到這種情況,業務上都是不允許大翻頁,應該根據條件過濾,但是圈品要分批取出所有數據,所以圈品就繞不開這個問題。

SELECT * FROM table WHERE campaing_id = 1024 LIMIT N,M

在這裏總結了兩種解決思路,圈品爲了獲取所有有效的商品,因此無需考慮數據整體的分頁,可以將分表獨立分頁處理。


id與limit組合優化

前面分析了使用limit大翻頁最大的問題是查詢前N(即offset)條數據所耗費的時間,在理想的情況下,id是連續自增,可以在where條件中使用id來代替offset,sql即如下所示。

SELECT * FROM table WHERE campaing_id = 1024 and id > N LIMIT M

優化思路中所說的理想情況,幾乎沒有場景能夠達到要求,但是這也不影響該思路的應用,根據上一次翻頁結果id使用limit查詢下一批,如果id不連續,limit將可以跳過很多不連續id,減少查詢次數。

結合圈品實際情況使用該思路,首先通過min和max得到數據在表中分佈的最小值和最大值,針對稀疏型數據分批間隔可以很大(爲了解決任務數量過多問題,比如間隔是2W,即end-start=2w),start和end分別是每批數據對應的開始id和結束id,然後根據id做爲where條件使用limit取下一頁數據,接着根據下一頁最大id做爲where條件使用limit取後面的數據,一直循環下去,直到id>end,對於稀疏型數據,也許循環1-2次就完成了。流程圖如下圖3.12所示。

圖3.12 圈品分頁處理優化思路


覆蓋索引優化

當sql查詢是完全命中索引,即返回參數和查詢條件都有索引時,利用覆蓋索引方式查詢性能很高。先通過limit查詢出對應的主鍵id,然後再根據主鍵id查詢對應的數據,由於無需從磁盤中取數據,所以limit方式比之前性能要高,sql如下所示。

SELECT * FROM table AS t1 
INNER JOIN (
  SELECT id FROM table WHERE campaing_id = 1024 LIMIT N,M 
) AS t2 ON t1.id = t2.id

“覆蓋索引優化“到底能優化到什麼程度呢,對此進行了一個測試,表item_pool_detail_0733包含10953646條數據,通過item_pool_id = 1129181 and status = -1條件篩選後剩下3865934條數據,item_pool_id和status建立了聯合索引。

測試1

我們看下offset較小的時候,sql和執行計劃如下所示,執行平均耗時83ms,可以看到在offset較小的時候,sql性能是可以的。

SQL:

SELECT *  FROM `item_pool_detail_0733` WHERE item_pool_id = 1129181 and status = -1  LIMIT 3860000,100

執行計劃:













id select_type table partitions type possible_keys key key_len ref rows filtered Extra
1 SIMPLE item_pool_detail_0733
ref idx_pool_status,idx_itempoolid idx_pool_status 12 const,const 5950397 100.00



測試2

當offset較大的時候,sql如下所示,執行計劃和上面是一樣的,執行平均耗時6371ms,這個時候sql性能就很差了。

SQL:

SELECT *  FROM `item_pool_detail_0733` WHERE item_pool_id = 1129181 and status = -1  LIMIT 3860000,100

測試3

現在使用“覆蓋索引優化”思路優化測試2的sql,sql和執行計劃如下所示,執行平均耗時1262ms,相比優化之前執行耗時6371ms,性能提高了5倍多,但是1s多的執行耗時對於圈品來說也是難以接受的。

實際情況下,分庫分表會把數據均勻分佈在所有表中,因此,單表過濾後還剩下300w多數據的情況是很少的,爲此,我接着測試數據量不同時該sql的性能。當LIMIT 2000000,100時,執行平均耗時697ms;當LIMIT 1000000,100時,執行平均耗時390ms;當LIMIT 500000,100時,執行平均耗時230ms;

SQL:

SELECT * FROM `item_pool_detail_0733` as t1 
INNER JOIN (
    SELECT id  FROM `item_pool_detail_0733` WHERE item_pool_id = 1129181 and status = -1 LIMIT 3860000,100
) as t2 on t1.id = t2.id

執行計劃:

id select_type table partitions type possible_keys key key_len ref rows filtered Extra
1 PRIMARY <derived2>
ALL



3860100 100.00
1 PRIMARY t1
eq_ref PRIMARY PRIMARY 8 t2.id 1 100.00
2 DERIVED item_pool_detail_0733
ref idx_pool_status,idx_itempoolid idx_pool_status 12 const,const 5950397 100.00 Using index

通過上面的測試可以看出,使用limit查詢50w數據時,性能還可以,而且圈品的數據源都是根據商品ID進行分庫分表,因此,根據過濾條件過濾後剩餘的數據幾乎都能在50w以內。如果圈品採用這個思路優化分頁處理,那麼將可以完全解決第一階段中的兩個缺點問題,而且分頁處理邏輯相比之前簡單很多。


▐  數據一致性保障

數據一致性保障解決方案充分複用了圈品框架,只需三步,第一,在動作模塊增加一致性檢查動作,第二,新增自動產出一致性檢查任務的scheduleX任務,第三,在業務處理模塊中增加自定義一致性檢查方法。


新增一致性檢查動作

一致性檢查動作處理一致性檢查任務,其處理流程圖如圖3.13所示。       

圖3.13 一致性檢查動作處理流程圖


自動產出一致性檢查任務

圈品當前有效的圈品池已經超過5000,如果一次性檢查5000個圈品池,那麼產出的檢查任務數量可能上百萬,因此,需要一個定時任務不斷監控任務表中未完成任務的數量,在數量較少的情況下一次選擇幾個圈品池產出一致性檢查任務。一致性檢查任務的產出依然是複用圈品規則變化處理流程。


業務自定義一致性檢查

新業務處理類圖在之前的基礎之上增加自定義一致性檢查方法consistencyCheck,需要自定義一致性檢查的業務實現該方法便可。

圖3.14 新業務處理類圖


▐  性能數據


卡券商品設置引擎的性能主要通過三個指標衡量,分別是:任務調度吞吐量、商品處理速度。


任務調度吞吐量

一個任務可能包含幾個商品也有可能包含上千個商品,取決於數據源的稀疏程度,當數據是稀疏的時候,任務數量將會很多,這個時候圈品的速度就取決於任務調度的速度。目前,任務調度的速度可以達到5w個/分鐘,這還並不是最大值,還有上升的空間。


商品處理速度

商品的處理速度受下游系統的影響需要限流,不考慮業務處理速度,理論上處理商品速度可以達到6w TPS。

✿  拓展閱讀

作者|周忠太(默達)

編輯|橙子君

出品|阿里巴巴新零售淘系技術

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