QPS降低80%,去哪兒網業務快取體系的升級改造

陶然陶然發表於2022-11-03

  在高效能的系統開發設計過程中,快取的開發與使用是必不可少的內容,對於較複雜的業務系統往往並不是一個快取方案就能解決所有效能問題,而是根據業務場景的不同選擇不同的快取方案進行搭配產出更好的整體效能。

  去哪兒的增值業務系統是一個承接多業務線貫穿多種售賣場景的三高系統,2021 年上半年我們對系統進行了快取體系化改造升級,改造後的快取不僅容易擴充和維護,效能上的直接收益非常可觀,資料庫 qps 下降 80% 以上,詳情介面響應時長從 150ms 下降到了 90ms ,內部子系統間的 rpc 呼叫減少了 40% 以上,關鍵部分快取命中率維護在 80% 以上 。那麼這樣的一個快取體系是如何設計開發出來的呢?本文將給以詳細解答。

   一、第一個快取元件的開發

  1、開發背景

  2020 聖誕節前後業務流量受疫情恢復開始反彈,增值業務系統 db 的 qps 單庫上漲至 3 萬以上,核心介面延遲增加,增值業務交易系統 load 節節升高,生單存在效能安全隱患。經調查發現大量請求處在 IO 阻塞中且系統執行緒數(2000+)和活動執行緒過高,系統上下文切換開銷大。為此,我們團隊做了很多效能最佳化的措施,其中最重要一個動作就是給資料庫層加了一個特殊的快取方案,這個快取方案就是我們開發的第一個快取元件。

  之前系統已有在用redis和guava快取的,為什麼這次還要再加一個針對 db 層的快取方案呢?主要源於以下幾點:

  本次效能問題突出在 db 上,db 的連線過高也在報警。

  系統體量是很大的,僅交易系統有 30 萬以上程式碼,系統內部流程和分支很多,直接在入口處做快取命中率不會很高,而系統最關鍵表結構只有 6 個,在此處做快取更好收口。

  這是個迭代過多年的系統,內部流程很長,上下文間經常有些對 db 重複的查詢,改業務流程工作量大需要回歸的 case 太多。

  交易系統對快取的一致性要求極高,需要專用的快取方案。

  除此之外,傳統的與資料庫有關的快取方案在我們的業務場景 下是存在缺限的:

  mysql 資料庫端的查詢快取,這個快取很雞肋,只要資料發生變更相關快取資料全部失效,對於一個含訂單交易的系統而言變更可是常態。

  mybatis 二級存在,不僅一致性較差,其快取粒度是基於 mapper , 一條資料的變更會將整個 mapper 的快取資料清空。

  使用 aop 和註解,基於方法直接在 dao 請求和 mysql 間加一個 redis ,這個如果沒有更細節的設計,分散式環境下的全域性一致性是得不到保證的。

  基於 binglog 同步做快取,這個是有秒級內延遲的,不適用於交易系統。

  2、設計思考

  我們設計的這個快取元件命名為 daoCache ,原於其加在 dao 層上,其主作用於分擔資料庫的 qps ,以近 1:1 的比例將 mysql 的查詢請求轉移至 redis ,透過一套固化的流程和機制確保在分散式環境下快取資料與 db 具有較高一致性,同時儘可能對業務使用透明。

  分散式場景的一致性理論基礎 cap 原則和 base 理論告訴我們在分散式環境下無法在強一致性上去死磕,分散式快取完整的強制一致是一個不可解的問題, 工程實踐時應主要考慮一致性、開發成本與效能開銷之間的平衡。由於這是一個重要的快取方案,為了降低實踐風險,我們做了充分的分析和思考。

  1)內部情況考慮

  資料層的編碼有專人負責,一般業務開發只需呼叫資料層人員提供好的介面即可,這樣的分工在專案人員較多時有明顯優點,DB 和 SQL 能力強的同學寫出的資料層程式碼質量更好,也可以集中做一些最佳化,缺點就是業務鏈路長的時候業務開發人員不太會去從整體和底層關注資料層的效率;為了降低方案後續接入改造的成本,設計的方案要儘量減少對資料層以上編碼的要求或關注。為了降低 DB 的壓力,團隊已經做了分庫分表相關的操作,並對某些高頻繁的查詢做過一些快取,但不太系統,只是基於一些點片。已有的分庫分表是後續做快取分片時應該重點考慮到的內容。

  2)外部情況考慮

  這裡主要是考慮公司對 redis 的 api 在安全使用上的限制。比如公司的 redis 客戶端是不支援很多批次處理操作的,這就決定了減少互動次數不好進行批處理最佳化;另外服務端禁用了 lua 指令碼,沒辦法進行 cas 操作。

  3)一致性的考慮

  在快取與 db 之間的更新一般有四種組合:先更新資料庫,再更新快取;先更新快取,再更新資料庫;先刪快取,再更新資料庫;先更新資料庫,再刪快取。仔細想想這四種更新組合在併發場景下都存在一致性問題,本文限於篇幅就不詳細展開討論了。

  另外,要做到高一致性對快取與 db 進行更新一般也四種思路:a、分散式同步加鎖;b、對映到單執行緒+佇列;c、監聽資料庫 binlog 同步更新快取(秒級內延遲);d、獨立的快取更新平臺統一控制, 快取 key 分片與客戶端監聽變化。我們的方案沒有使用以上的任務何一種,這個在後文會詳細介紹。

  4)效能的考慮

  為了做好效能,在具體方案實現流程中關注了以下幾個點:

  ① 資源消耗,平均資源呼叫次數

  這裡主要是指要儘量減少接入快取的例項與redis服務端的互動次數,當然不必要的db互動也要減少。

  ② 快取命中率

  快取目標透過配置可以選擇,對快取命中率低的key儘量不快取, 一般而言快取命中30%以下就要考慮是否去除快取。

  ③ 異常流程

  一些異常過程是否會造成極端的髒資料,是否有清洗手段,清洗是否對效能有影響,redis服務端網路的波動是否會影響到業務服務

  ④ 加鎖與並行度

  在並行更快取下無鎖化設計優於加鎖,儘可減少加鎖操作,特別是主幹流程,但是不要因為網路延遲而造成髒資料。

  5)成本的考慮

  這裡主要是時間成本,因為業務流量還在上漲中,所以開發出的方案一定是輕量級的。

  3、實現原理詳解

  1)流程

  本快取是使用 redis 作為快取中介軟體(也可以換其分散式程式外快取),透過 mybatis 外掛加 dao 層的註解加啟動掃描實現的。在應用啟動時掃描 dao 層所有加了 daoCache 註解的快取,分析要對哪些方法進行攔截(預分析而不是在訪問 dao 層時實時分析,也是為了效能的考慮);新增一個 mybatis 的外掛對資料的增刪改查做攔截,其攔截過程與快取處理流程如下圖:  

  

  上圖中我們將 sql 語句分為 4 種,重點關注的是圖中第二種和第三種 sql ,對於第四種 sql 我們只關心新加資料時清理快取兜空的情況,而對於第一種 sql ,由於多對多的處理太複雜,而且我們的專案中這樣的 sql 的 qps 很低,不處理不影響大局,只需要對 qps 較高的一對多進行處理即可。仔細看這張圖上有幾個點要注意:

  讀的流程,如果快取命中開銷是很低了,沒有多餘的互動和確保動作,而如果 miss,除了有 db 操作,還增加了兩三次的 redis 操作; 所以,對於命中率較低的sql可以用註解忽略使用快取。

  快取不進主動更新,只是主動刪除,只有被動 miss 時才向 redis 加入資料。

  整個流程沒有分散式鎖,保證併發時ABA不發生的關鍵在於打個自動過期的標記,關聯同一個key的更新操作的標記是可以相互覆蓋,打標持續期間是不允許向快取中存放資料, 向快取放資料的那個執行緒遇到標記是不阻塞的,而是直接丟棄本次操作; 標記存活時間可配置,預設長達10秒, 這麼處理,可能從單條資料來看快取長時間沒被使用,但是從宏觀整體去看,損失的miss則是佔比很小的,幾乎可以忽略。

  快取都設定了過期時間,用以作最終一致性保障的兜底,一般時長為10分鐘至20分鐘,這個時長和使用者單次使用我們系統時長相差不多。

  為了進一步處理極端情況,還做了很多處理,比如: put完資料後馬上又check一下標識,如果標識又存在則清理快取;對於一些退款判定類的sql,直接忽略快取; 為了保證生單過程的不被可能的髒快取影響,對於生單執行緒做特殊放行。

  該方案的核心思想是:

  對於一致性,不是完全 case 解決,而是儘量處理多數 case ,對於不好處理的部分就直接扔給資料庫。

  以主流程為基本保證一致性手段,補充例外手段,執行緒維度放行與方法維護放行相結合。

  按實際流量排行,優先解決大流量的查詢。

  2)關鍵資料結構

  整個流程中 key 對映處理非常關鍵,因為只有對映關係準確了,在對應的dao 執行更新時才有可能將快取中所有相關聯的資料清理掉,具體對映關係如下圖:  

  圖中的欄位含義解釋如下:

  bizKey:業務主鍵,在我們場景下一般就是定單號 orderId。

  bizValue:業務主鍵對應的值,後加數字表示不同的值。

  selectKey:通常我們的查詢條件中會帶有 bizKey, 如果不包含則需要指明一個能與 bizKey 產生關係的 key ,它們之間的關聯是在miss後透過返回值建立起來的。

  selectKeyValue:selectKey 對應的值,後加數字表示不同的值。

  methodReturnValue:表示的是被攔截的 dao 方法的返回值。

  其他欄位大家可以顧名思義一下。set 結構表示它們之間的對映關係在 redis 中是透過 set 結構維護的,hash 結構表示它們之間的關係在 redis 中是透過hash結構維護的。通常如果我們的查詢條件中帶有業務主鍵,那麼透過 redis 的 hash 結構可以一次查詢到,如果不帶則需要先從 set 介面中拿到關係的鍵再透過主鍵獲取資料,這個過程有些類似資料庫的非聚合索引查詢資料需要回表的過程。

  這麼設計使得我們只需要給那個 bizValue 相關的快取打個標記並將其快取內容刪 除即等價於所有與這個 bizValue 相關的快取都被刪除了(此時透過 selectKey 能檢索到 bizKey ,但是對應的 selectValue 被刪除了,返回結果還是空,等價於沒有命中)。同一個 bizValue 儲存著多個快取值是因為對於同一條資料的查詢其返回的資料結構可能是不一樣的,所以快取不是按表中記錄一條條儲存的,而是按每個方法的返回值儲存。另外,我們的方案要求對於被快取管理的 dao 的所有更改操作都能帶上 bizKey ,以便程式更新資料時能關聯清除掉所有相關的資料。

  需要注意的是,目前該對映關係僅能處理表和 dao 是一一對映的場景,在高 qps 場景,多表聯合查詢的設計很少,至少在我們當前應用上不存在的。越複雜的查詢需要的這個對映關係也越複雜,快取端的資料結構也要進行定製,資料結構的複雜也會影響快取的效能,理論實現成本也很高,redis 現有的資料結構無法滿足,可能需要開發專用快取中介軟體。

  4、功能上線

  做好上線,要把以下兩個問題處理好。

  1)如何平滑升級?

  由於快取是以 dao 介面為粒度進行配置的,所以可以先選擇一些不太重要的、更新頻度低的先上線進行驗證,做好配置與切換的開關,一旦問題可以切換回無快取狀態。在正式開量之前先進行充分的 diff ,也就是拿快取資料和拿 db 資料進行對比,看看差異,如果存在差異再透過日誌查詢造成差異的原因。

  這個驗證過程可以查詢出細節沒有處理好的點,或者漏處理的點。實際上我們在漸進上線過程和後續維護過程中就透過 diff ,查日誌,看調棧的方式最佳化了一些細節才使得快取一致性達到了線上可接受的狀態,這裡就不詳細說明了。

  2)出現意想不到的問題時如何處理,有沒有反制手段?

  首先在編碼處理的細節上將 redis 做了弱依賴處理,也就是即使redis服務掛了也不影響業務的正常運轉,在編碼的每一步就考慮到了 redis 掛了後程式的繼續執行。我們調短了 redis 連線超時時間,使用滑動視窗演算法對 redis 超時和異常進行分流,redis 越不穩定或異常越多時,直接走 db 的流量也就越多,當然為 db 的安全,正常分流最大值是 50% 直接走 db ,這個分流調整幾乎是毫秒級的,畢竟 redis 不穩定,哪怕只是阻塞 1 秒,我們的應用也會產生數萬條的錯誤日誌。

  上線過程中的那個 diff 功能一直保留,它有額外的兩個作用:一是上線後留置 5% 流量 diff ,一旦發現快取不一致會觸發報警; 二是當開 100% diff 的時候其實是在做快取清洗的,因為 diff 的邏輯是出現差異會以 db 的資料為準,並清理快取,當有意想不到的問題出現時,比如跑 sql 洗數了,可以針對性的把相關 dao 的 diff 全開。

   二、全域性快取體系設計與規劃

  1、提出背景

  在做了第一個快取方案之後雖然解了 db 之憂,但後續有出專案啟動過慢,系統效能抖動等問題,經調查發現一部分原因是由於商品快取定時全量載入所致。這個商品快取同時存在多個微服務系統之間,為此我們提出了共享快取的概念。不過,在提出共享快取概念之前,為了避免又有其它類似問題出現,先用了一個臨時方案的方案堵住了表象問題,然後再對系統已有快取作了一個全盤梳理,透過全盤梳理發現以下問題:

  快取的使用很零散,在長期專案跌代過程中變得很維護,無法整體觀察,也無法進行全域性調參配置。

  有些快取key可有多個子應用共用著,導致開發人員在不是很瞭解各應用程式碼的情況下不敢輕易對這些快取key做下線清理,時間一長,redis中可能殘存著很多無用的資料,浪費了大量的快取資源;

  其實這些問題是有一些代表性的,我想它不僅僅存在於我們增值業務系統中。

  2、快取體系簡介  

  圖中由上至下是從縱向按請求流對快取進行橫向劃分。

  一級快取

  指程式內部的快取,使用一級快取完全是記憶體操作沒有 io 開銷,但是在分佈場景下一級快取分隔在各個程式中,一致性沒有任何保證,它適用於週期較短的無狀態化請求,或者是其資料不一致性但業務可接受且在程式設計師可控範圍內的,最理想的情況是被訪問資源能從整體架流程上做到被某個程式獨享。一級快取分成兩類,如圖中 a 類一般是不需要考慮全域性一致性的,而 b 類很多情況是要考慮一致性的;所有快取都要考慮容量、命中率、過期等問題;在某一個被快取的條目上,a 類快取和 b 類快取一般是二選一,兩者同時使用會使快取設計過於複雜,快取一致性問題難解。

  二級快取

  是指程式外部被一個應用多個例項或多個應用共享的快取,一般會共享資料,這裡我們暫只用 redis 做二級快取;將 redis 快取按功能用途分為以下四塊,這四塊在部署的時候不一定需要四個獨立的名稱空間,只是一種邏輯劃分以便於編碼上的區別對待,如部署時被放入同一名稱空間編碼時可考慮用命名字首加以區分。

  下面從右至左解釋一下這四塊記憶體:

  通用 service 快取

  類似 spring 的註解,其快取的資料量可能較大,要有記憶體限制,要考慮崩了不影響程式邏輯和正常流程,主要作用是對一般快取使用場景的統一收口。

  daoCache 快取

  也稱為資料庫快取,加在 dao 層,以方法簽名和引數內容為 key ,記憶體較冗餘,一般只 cache 部分表及部分方法;該快取有較強一致性要求。

  應用專項快取

  被一個應用所獨享的專用快取,一般不做批次資料儲存,多用做一些標識、計量、功能性佇列、鎖、冪等判定,對延遲要求敏感,redis 故障或延遲會影響到系統功能; 我們現在大部分功能性快取都屬於這種情況,其中不屬於但使用的部分以後要考慮遷走。

  共享記憶體

  一般是被多個應用共享,其中至少一個應用對記憶體具備維護的許可權(一般是誰建立誰維護),這種快取一般適合用於被多個應用訪問的高頻熱資料,資料的變更很少或不變更,接受一定時間的資料不一致,但資料必須最終一致。使用這種快取,一般會配合一級快取中的b類,二級快取做全量,一級快取做熱點,同時要設計一二級快取的一致性協議;對於我們的系統來講,商品快取明顯適用這一情況。

  3、快取元件相關間關係

  上文介紹的各種快取,它們除了可單獨使用外,還能整合起來使用,達到 1+1 大於 2 的效果。實現這一點得益於兩方面:

  上文的整體規劃上定義了各方案元件之間的邊界;

  元件之間在實現上有明確的依賴關係,這個依賴關係如下圖:  

  圖中箭頭的指向為依賴方向,中間綠色的部分收斂所有快取中介軟體的和工具的使用,抽象出一致的快取定義、配置、構建、基本介面,它自帶兩個快取方案, 即通用快取 service 快取方案和條件上下快取方法;其它快取方案是位於這個介面的定義和管理之上的,每一個上層的快取方案是解一個特定快取場景問題。

  這樣處理帶來很多的好處:

  所有快取方案都不依賴具體的快取中介軟體,快取中介軟體可以很方便的按上層具體場景甚至具體應用進行更換( redis 有多種資料結構可以形成多個抽象例項,將其功能性和儲存性部分分離)。

  有統一的基礎監控和基礎功能,比如基於訊息中介軟體做本地快取更新的通知。

  便於各方案間相互打通,由於增值系統中使用了多種快取方案,當快取因一些特殊原因(如手動改資料庫資料了)而出現不一致時,使得問題非常難排查,特別是在不太健狀且複雜多變的測試環境上時,此時只需要在通用快取元件部分加一個禁用快取上下文的功能就可以很方便有選擇的禁用或全部禁用快取,進行差異 diff 。

  通用 service 快取有一些基礎功能,上層快取方案是可以直接複用的,比如使用 mq 做基本的訊息同步(這裡只是一個介面定義,實際可以根據場景換),各種序列化方案的選擇與使用。

  鑑於本文篇幅有限,下面只詳細介紹共享快取。

   三、共享快取元件開發

  微服務環境下大專案會被拆分為多個小專案,原來在一個單體中的快取可能會面臨同時存在多個子應用中,這時就產生了一致性問題,將本地快取直接改寫成用 redis 這種程式外共享的快取往往有效能上的風險。共享快取的核心理念是多應用共享資料,以本地快取作為一級快取,redis 作為二級快取,透過 mq 訊息或其它擴充套件實現快取變更的增量更新,它的內容不單單只是如此,接下來我們先對它的架構作個簡要的瞭解,然後再看看它到底有哪些特點。

  1、架構簡述  

  共享快取的接入方分為 Provider 和 Consumer 兩個角色。Provider 方是持有快取相關的資料來源的一方(不一定要求是 db 資料來源,但必須有更新閉環),可對快取進行讀寫;Consumer 方是隻讀快取資料的一方, 只有在二級快取 miss 的時候 consumer 才可透過隱式的 rpc 介面觸發 provider 快取資料的填充。

  provider 和 consumer 都是可選,只使用 consumer 相當於對第三方介面進行了快取, 只使用 provider 的場景多為同一應用多例項節點,資料無需跨應用共享。

  redis 既作為二級快取,也作為註冊中心。

  為什麼要分為 Provider 和 Consumer 兩個角色呢? 這是照顧跨團隊的場景,在 api 的便用上 consumer 做得儘可能簡潔,只需要呼叫即可,而 provider 則要關心更新閉環和快取的維護; 另外,在微服務的場景下,服務間的關係也存在這樣的角色,也是職能分工的需要。多應用共享一個 redis 叢集和拆分後繼續共享在我們團隊裡是個常見的現象,而這時共享 key 的設定非常重要,參與應用可能要手工維護 key 的一致性,有了共享快取 key 一致性就再也不用擔心出問題了。

  2、價值特色

  共存快取應對的是低頻更新極高頻訪問的場景,是最終一致性快取方案,同步通常在 2 秒內完成,高峰時 10 秒內,異常時 5 分鐘,極端情況 20 分鐘,自身升級有 bug 時有手工同步後門。

  共享快取方案無論是在 redis 還是在本地都只有一條原資料,這樣做不僅節省記憶體,在一致性上也好處理,實現細節上我們也做了很多最佳化工作。

  支援事務,支援按條件查詢條件快取。

  真增量更新,全量資料本地快取時,當新加某條資料或某條資料發生變更時,所有節點新增當條資料索引或僅過期當條資料。

  自動分包與合包,為了避免 qmq 訊息過載,短時間內大量相同訊息會在傳送端和接收端去重,並儘可合併訊息,減少訊息傳送量。

  支援 mq 訊息丟失, 多種機制防止訊息丟失後導致快取結果不一致。

  我們接下來看來實現這些價值特點的原理。

  3、核心實現原理

  在沒有做共享快取之前,團隊裡其實有同學已經用 mq 做本地快取資料同步了,也有些團隊用資料庫的 binlog 日誌做同步; 但是沒有將這個過程進行模版化,很難進行持續最佳化,方案很不好複用。共享快取實現的一個基本原理就是將 mq 同步快取的這個過程進行模版化了。那麼如何進行模版化的呢?

  簡單的說就是將與快取相關的幾個流程進行分解,將其中不變的部分提取出來放到框架中,將業務的部分留給使用時注入。共享快取將快取操作相關的流程分為:預熱、讀取、更新、接收同步訊息和處理同步、兜底檢查五個主流程,其中只有前三個流程需要使用者去註冊業務動作。本文限於篇幅詳細流程就不再展述了。下面我們看看實現共享快取最關鍵的一個設計,正是這個設計使其與一般的透過 mq 進行同步的快取有著根本的不同。

  1)共享快取的核心資料結構  

  共享快取與 daoCache 一樣也有一個業務主鍵的概念,與 daoCache 不同的是共享快取無論是在本地還是在 redis 對於同一個業務主鍵通常只對應一條資料, daoCache 儲存的是 db 中資料的對映,共享快取理論上可以存使用者定義的任何資料。我們來詳細看看這張圖上的快取結構。

  在 reids 這邊, 存著註冊相關的後設資料,全量的業務資料,升級過程中可能同時存在多個全量資料的版本,它們之間透過位元組碼字首加以區別。

  在本地,共享快取維護著當前熱點資料(當前沒有開發熱推送平臺,這個熱點指的是經常被拉上來的資料),維護四種索引:單條主鍵索引、條件集合索引(可選)、全量索引(可選)、反轉索(有條件集合時才存在)。由於資料在本地和遠端都只有一份,它們的最終一致性就很好維護。而對於索引的維護最壞的情況就是重建索引,在記憶體中重建索引的效率遠高於重建相關的資料。為了做到更好的增量更新,資料更新都是先對比最新更新的 key 相關的內容是否符合索引條件,再決定是否向下取資料的。

  2)共享快取的一致性

  有以下 5 個點來保證資料的一致性:

  mq 進行本地快取同步,mq 僅通知發生變化資料的業務主鍵,本地接收後對相關快取進行刪除,然後業務訪問時重新抽取資料即可,這個拉取資料的過程要儘量不要拉取到舊資料,由於這個流程遠比 daoCache 要複雜,這裡就不詳細解釋了。

  provider 端對快取進行更新的前後會在 redis 進行打標和刪標,leader 節點會定時檢查最近的標記,如果發現存在 30 秒前未刪除的標記,則認為相關的快取更新可能出現異常,隨即清理相關的快取。

  provider 端更新或新增資料時會將相關聯的業務主鍵登記到 redis 裡,各節點會在本地記錄最近收到的需要變更的業務主鍵,每間隔 5 分鐘會和 redis 裡進行對比(僅 5 分鐘內變更的業務主鍵),對於差異化的部分進行快取清理。

  leader 節點一般每間隔(開發時可編碼設定)20 分鐘會將資料庫中的資料全量更新至 redis 做二級快取一致性的兜底, 本地快取也可以設定過期時間做一致性的兜底,一般預設是半小時。

  本地快取全量滾動更新後門,可以手動呼叫,也可以配置 QSchedule 定時呼叫。(該方式可先,一般用於大版本更新,或應用初接時)

  3)共享快取的典型使用場景  

  圖中放入共享快取的資料是中間的那一部分,這個資料是由兩個表構造出來的(實際使用時也可以來源於 qconfig 或其他介面, 但前提是應用能掌握資料更新的閉環),圖中業務容器資料是根據被快取資料建立的(這個建立過程可以穿插別的資料來源),當節點 1 把表 1 或表 2 的資料更新時會觸發所有節點被快取資料的更新,當某個節點快取資料更新完成之後會回撥註冊的業務程式碼,由業務程式碼重新構建基於快取資料的上層資料。

  這個場景如果使用其它快取方案是需要很多編碼才能解決的,而透過我們共享快取只需要業務定義好被快取的象的讀取構建過程,上層關聯構建過程,相關表的 Dao 方法上增加註解即可。事實上這些過程在接入共享快取之前業務就有類似的程式碼,只需要按共享快取的方式修改一下即可。這一點並不是本方案最大特點,最大的特點是,短時間內多次被快取物件的多個表的多條資料更新,共享快取只會觸發一條 mq 訊息進行資料同步。其它一般基於 mq 訊息同步或 binlog 同步的快取,可能需要大量的網路開銷。

  這裡舉個例子,我們某個應用幾個商品資料的更新,可能會更新到一批 ext 表的資料又有主表的更新,主表的更新可能又會造成所屬分組的更新,所屬分組更新會又引起上層分類容器的更新,如果直接使用傳統的 mq 訊息同步方案可能瞬時產生幾千條 mq 訊息給所有節點消費,而這些節點又會可能全部重新拉取資料,引起網路 IO 風暴。共享快取在框架內部的傳送端和接收端都基於業務主鍵進行了合併去重的操作,在這些基礎之上又進行資料合包,使得最終發出的 mq 訊息降了幾個量級,除此之外對於關聯的的上層容器構建的觸發也作了處理,當短期內收到大量更新觸發時也會只執行最新的有效更新。

  另外,由於共享快取是基於通用快取之上做了快取方案,是不依賴具體的底層元件的,本地預設的 caffine 可以更換效能更高的 Map,redis 也可以在某些場景換成 memcached 。沒錯,在我們的實踐中 caffine 在單機百萬級跌代下效能會有明顯消耗(耗秒級,我們搜尋介面 20ms 超時),在這種場景下直接換成 map 丟失了快取過期失效(可以透過主動失效補償)換取了效能優勢。

   四、總結

  這些快取上線了不僅直接減少了 db 的 qps ,同時也消除了 DB 峰值的抖動,一些應用間的介面因為共享快取的存在也直接由高頻qps到直接下線。資料庫快取自上線以來並沒有出現過明顯快取不一致,僅在一些改造升級期出現過零星不一致,也沒有給業務造成負面影響,幾乎沒有收到與此相關的問題單。但是資料庫快取方案有很多的侷限,主要表現在:

  快取在請求流的末端,快取效益是很低的,給系統做快取是分層的,越靠近靠近請求入口收益越高(不過命中率不好做高)。

  使用上對 dao 有一些要求和約束,如:只能單表查詢,更新必帶業務主鍵,對一對多的支援需要 @DaoCacheSelectMethod 指明一些引數,否則不會攔截這種方法,也並不是支援所有的一對多,這個細節要了解原碼才比較清楚,最重要的是配錯了是沒有任何提示的,需開發人員自己小心。

  特殊放行邏輯需要業務額外編碼配合。

  一直認為該快取方案較高的命中率和我們專案的編碼特點有關(本身也是按專案已有的編碼特點產生的),換成別的專案快取命中率可能會降低。

  不過本文分享的重點不是該方案的實現細節,而是產生和設計這個方案的思想,這個設計思想是可以用作別的快取方案的。

  而文中描述的共享快取其適用於極高 qps 與低 tps 的最終一致性倒是在使用侷限上面要小很多,目前的實現完全滿足我們增值系統的需求,在對外推廣上還有很大的成長空間,需要更多的業務場景去驅動完善。總之,基於快取體系的全域性規劃,我們力爭在後續的業務驅動開發過程中將所有遇到的快取方案的整合成一個快取方案池,不斷最佳化迭代使其成為更通用的快取方案庫。

來自 “ Qunar技術沙龍 ”, 原文作者:陳力;原文連結:http://server.it168.com/a2022/1103/6772/000006772243.shtml,如有侵權,請聯絡管理員刪除。

相關文章