亚洲乱码中文字幕综合,中国熟女仑乱hd,亚洲精品乱拍国产一区二区三区,一本大道卡一卡二卡三乱码全集资源,又粗又黄又硬又爽的免费视频

基于context.Context的Golang?loader緩存請求放大問題解決

 更新時間:2023年05月12日 11:09:11   作者:ag9920  
這篇文章主要為大家介紹了基于context.Context的Golang?loader緩存請求放大解決方案,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪

請求放大的問題

同一請求鏈路中對下游的請求放大是現(xiàn)代微服務體系中經(jīng)常遇到的痛點。

舉個例子:某個業(yè)務流程中,需要獲取用戶的積分余額,從而進行后續(xù)判斷。但這個【請求余額】的行為,不僅僅在某個場景需要使用,而是在整個請求的生命周期,多處邏輯都可能需要,甚至負責開發(fā)的都不是同一個人。這個時候就很容易出問題了。小 A 在入口處就請求了余額,但只放在了自己的業(yè)務結構中。隨后小 B 也需要,又請求了一次余額。這就出現(xiàn)了請求放大。

為什么需要考慮這個問題?

  • 放大可不一定只有 2 倍,事實上,復雜的業(yè)務鏈路如果不仔細思考,調(diào)整,最終出現(xiàn) 4 - 5 次請求放大都是很常見的;

  • 下游的服務的負載是需要考量的,明明一次請求就可以拿到的數(shù)據(jù),你請求了多次,下游可能會被打掛,哪怕可以承受,也額外付出了更多的 CPU,通信成本;

  • 通常出現(xiàn)放大時,各個業(yè)務的處理邏輯是獨立的,也就意味著,一旦微服務不穩(wěn)定,后續(xù)請求網(wǎng)絡超時,你可能會因為一個明明此前已經(jīng)拿到的數(shù)據(jù),而導致整個鏈路返回了失敗。

所以,我們需要嚴肅地看待這件事。目標其實很明確:

  • 只拿需要的數(shù)據(jù);
  • 不重復拿同一份數(shù)據(jù)(如果數(shù)據(jù)可能會變,可以考慮放大,這不是絕對的);
  • 處理好強弱依賴,不因為一個明明可以接受,降級的失敗請求,導致整個處理流程中斷。

那我們怎么才能保證一個請求處理過程中,不去重復請求下游呢?我只是其中一環(huán),怎么知道此前流程里是不是已經(jīng)拿過數(shù)據(jù)了呢?就算知道,人家都放到了自己業(yè)務的結構體里,我怎么用?

中間件能解決么?

這里常見的思路是使用【接口中間件】,即:把一些通用的 loader 放到 middleware 中,比如請求用戶信息,租戶信息,鑒權等。我們這里舉的例子也可以這么處理。

接口中間件里我就把余額拿到,隨后作為一個公共的結構體,一路透傳。類似這樣:

type BizContext struct {
	Ctx context.Context
	UserInfo
	TenantInfo
	UserBalance
}
func ExecuteLogic(bc *BizContext, param interface{}) error {
	// TODO:業(yè)務邏輯
}

這樣,大家通過 BizContext 就能獲取到這些公共數(shù)據(jù)了。不需要重復請求。Problem solved!

但這個思路存在一個致命傷(并不是 struct 內(nèi)嵌 context.Context,你段位到了就可以這么用,背景參照我們此前的文章Golang context.Context 原理,實戰(zhàn)用法,問題 )。

問題在于,所有放到中間件里的 loader 邏輯,都是對整個接口的請求消耗。的確,我們可能在場景 A,D,F(xiàn) 要用到這個 UserBalance,但場景 B,C 呢?人家是不是白白的承擔了這種性能消耗,又沒有任何收益?

所有中間件里的邏輯一定是通用的,高性能的,具有普適性的。注定沒法覆蓋到所有業(yè)務場景。

一定不要濫用中間件,塞入大量個別場景需要的邏輯。中間件越重,接口性能就越不可控。

基于 context.Context 的解決方案

我們知道,context.Context 提供了 WithValue 函數(shù),支持將一些常見的上下文信息通過這個函數(shù)寫入 ctx。本質(zhì)是用 valueCtx 基于 parent Context 派生出來一個 child Context,形成了一條鏈。獲取 value 的時候是逆序的。

type BizContext struct {
	Ctx context.Context
	UserInfo
	TenantInfo
	UserBalance
}
func ExecuteLogic(bc *BizContext, param interface{}) error {
	// TODO:業(yè)務邏輯
}

我們可以利用這個能力,把請求結果 cache 到 context.Context 中,這樣就可以隨后復用了。但這樣本質(zhì)上和此前 BizContext 是一樣的,都是需要一個鏈路上都能獲取到的結構體。

loader 是一個數(shù)據(jù)加載器,下游可能是某個存儲,或是微服務。每個業(yè)務場景可能包含自己對應的 loader。

我們希望這個 loader cache 要具備下面的能力:

  • 適配任何數(shù)據(jù)加載器,和具體業(yè)務的架構不強綁定;
  • 按需加載,業(yè)務可以自行指定是否需要啟用 cache 能力,默認直接走 loader;
  • 高性能,不要帶來過高的性能消耗。

loader 定義

鑒于要實現(xiàn)一個通用的數(shù)據(jù) loader,我們不希望和特定結構綁定,所以勢必要返回 interface{},同時入?yún)⒔唤o業(yè)務自行判斷,通用定義里我們不做要求:

type loadFunc func(context.Context) (interface{}, error)

存儲結構

我們希望往 Context 里面放什么數(shù)據(jù),這一點很關鍵。鑒于我們希望支持多個業(yè)務場景,勢必會需要一個 map 結構,key 對應場景,value 是緩存的值。

同時,鑒于 Context 本身是支持并發(fā)的,而且整個 loader cache 會作為基礎的能力提供出來,我們希望這里的 map 也能在高并發(fā)下正常讀寫,所以回到了經(jīng)典的選型:

  • map + Mutex
  • map + RWMutex
  • sync.Map

選項一的鎖粒度比較粗,性能上會差一些。而 sync.Map 的 LoadOrStore 方法參數(shù)會逃逸到heap上,所以我們選擇 map + RWMutex,手動來控制讀寫鎖。

type callCache struct {
	m    map[string]*cacheItem
	lock sync.RWMutex
}

callCache 本身是外層的結構。我們從 Value(key interface{}) interface{} 接口就可以讀到。

這里 cacheItem 里面放什么,很關鍵!

  • 是不是直接就一個 interface{} 就可以了?

非也!如果我們完全不感知 cacheItem 的結構,會導致我們無法感知到這里到底是否已經(jīng)調(diào)用過 loader 拉取數(shù)據(jù)。即便可以置為 nil,但實際上 loader 也可能加載后發(fā)現(xiàn)沒有數(shù)據(jù),這一點不可行。

要實現(xiàn)只有一次調(diào)用 loader,后續(xù)調(diào)用都能復用結構。cacheItem 需要包含一個 sync.Once。

  • 錯誤如何感知?

我們對于每個場景,唯一能感知到的就是 cacheItem,所以除了正常的業(yè)務數(shù)據(jù),這里還需要有錯誤信息。否則 loader 調(diào)用出錯了都沒法給上游返回錯誤。

綜上兩點,一個可能的結構如下:

type cacheItem struct {
	ret  interface{}
	err  error
	once sync.Once
}

這樣我們就可以利用 sync.Once 的能力來控制,調(diào)用 loader 拿到結果和 error

func (ci *cacheItem) doOnce(ctx context.Context, loader loadFunc) {
	ci.once.Do(func() {
		ci.ret, ci.err = loader(ctx)
	})
}

sync.Once 保證了某個 goroutine 進入 Do 方法后,其他協(xié)程會阻塞等待。所以,我們可以假設,在 *cacheItem.doOnce 結束后,如果訪問 *cacheItem 是能夠拿到 ret 和 err 的最新值的。

好了,現(xiàn)在有了 cacheItem 的定義和 doOnce 能力,我們回到 callCache,完成調(diào)度邏輯:

type callCache struct {
	m    map[string]*cacheItem // sync.Map的LoadOrStore方法的參數(shù)會逃逸到heap上,這里用map+rwmutex
	lock sync.RWMutex
}

我們從 Context 直接獲取的結構是 callCache,那么當某個場景的 key 首次請求的時候,勢必需要對 cacheItem 進行初始化。

這個函數(shù): func (cache *callCache) getOrCreateCacheItem(key string) *cacheItem,如何實現(xiàn),這里很關鍵!

  • 既然用了 RWMutex,我們希望把讀寫粒度拆開,所以一上來應該判斷讀鎖,如果有值,直接返回;
  • 如果在讀鎖里沒獲取到,說明需要初始化,開始加寫鎖;
  • 在寫鎖中,完成初始化,寫入 callCache,并返回,defer 解掉寫鎖。
func (cache *callCache) getOrCreateCacheItem(key string) *cacheItem {
	cache.lock.RLock()
	cr, ok := cache.m[key]
	cache.lock.RUnlock()
	if ok {
		return cr
	}
	cache.lock.Lock()
	defer cache.lock.Unlock()
	if cache.m == nil {
		cache.m = make(map[string]*cacheItem)
	} else {
		cr, ok = cache.m[key]
	}
	if !ok {
		cr = &cacheItem{}
		cache.m[key] = cr
	}
	return cr
}

SDK 接口

好了,現(xiàn)在我們已經(jīng)具備底層能力了,思考一下我們希望開發(fā)者怎么用這個 lib。

WithCallCache

首先,ctx cache 不應該是默認啟用的,有可能業(yè)務就是需要有一些放大,這里需要開發(fā)者通過 SDK 接口顯式聲明。

此外,既然要往 Context 里面放,一定需要一個自己的 key,這里我們采用空結構體,用來與其他類型區(qū)分開。這也是經(jīng)典的操作。

type keyType struct{}
var callCacheKey keyType
// WithCallCache 返回支持調(diào)用緩存的context
func WithCallCache(parent context.Context) context.Context {
	if parent.Value(callCacheKey) != nil {
		return parent
	}
	return context.WithValue(parent, callCacheKey, new(callCache))
}

LoadFromCtxCache

這里是最核心的接口。我們需要支持開發(fā)者傳進來:1.業(yè)務場景;2.業(yè)務對應的 loader。

如果此前通過 WithCallCache 啟用了 ctx cache,我們就看看業(yè)務的 loader 此前有沒有執(zhí)行過,如果有,直接返回 ctx 中緩存的結果。如果從未執(zhí)行過,調(diào)用此前的 cacheItem.doOnce 來執(zhí)行。

// LoadFromCtxCache 從ctx中嘗試獲取key的緩存結果
// 如果不存在,調(diào)用loader;如果沒有開啟緩存,直接調(diào)用loader
func LoadFromCtxCache(ctx context.Context, key string, loader loadFunc) (interface{}, error) {
	var cacheItem *cacheItem
	v := ctx.Value(callCacheKey)
	if v == nil {
		cacheItem = nil
	} else {
		cacheItem = v.(*callCache).getOrCreateCacheItem(key)
	}
	// cache not enabled
	if cacheItem == nil {
		return loader(ctx)
	}
	// now that all routines hold references to the same cacheItem
	cacheItem.doOnce(ctx, loader)
	return cacheItem.ret, cacheItem.err
}

使用方法

  • 使用 WithCallCache 針對當前的 ctx 啟用 loader cache;
  • 改造數(shù)據(jù)加載邏輯,抽出來 loader,外層用 LoadFromCtxCache 來調(diào)用,以達到上游無感。

假設我們的 loader 是 myloader,接受一個 string,返回 int 和 error,下面看一下示例:

使用起來其實非常簡單,只需要大家封裝一下自己的數(shù)據(jù)加載邏輯即可。

源碼倉庫:go-ctxcache,感興趣的同學可以試一下,整體代碼量很小,實用性很強。

以上就是 context.Context 的 Golang loader 緩存請求放大問題解決的詳細內(nèi)容,更多關于Golang loader 緩存的資料請關注腳本之家其它相關文章!

相關文章

  • golang框架gin的日志處理和zap lumberjack日志使用方式

    golang框架gin的日志處理和zap lumberjack日志使用方式

    這篇文章主要介紹了golang框架gin的日志處理和zap lumberjack日志使用方式,具有很好的參考價值,希望對大家有所幫助,如有錯誤或未考慮完全的地方,望不吝賜教
    2024-01-01
  • Golang利用Template模板動態(tài)生成文本

    Golang利用Template模板動態(tài)生成文本

    Go語言中的Go?Template是一種用于生成文本輸出的簡單而強大的模板引擎,它提供了一種靈活的方式來生成各種格式的文本,下面我們就來看看具體如何使用Template實現(xiàn)動態(tài)文本生成吧
    2023-09-09
  • 詳解golang中Context超時控制與原理

    詳解golang中Context超時控制與原理

    Context本身的含義是上下文,我們可以理解為它內(nèi)部攜帶了超時信息、退出信號,以及其他一些上下文相關的值,本文給大家詳細介紹了golang中Context超時控制與原理,文中有相關的代碼示例供大家參考,需要的朋友可以參考下
    2024-01-01
  • 深入探討Go語言中的map是否是并發(fā)安全以及解決方法

    深入探討Go語言中的map是否是并發(fā)安全以及解決方法

    這篇文章主要來和大家探討?Go?語言中的?map?是否是并發(fā)安全的,并提供三種方案來解決并發(fā)問題,文中的示例代碼講解詳細,需要的可以參考一下
    2023-05-05
  • 聊聊Golang性能分析工具pprof的使用

    聊聊Golang性能分析工具pprof的使用

    對于線上穩(wěn)定運行的服務來說,?可能會遇到?cpu、mem?利用率升高的問題,那我們就需要使用?pprof?工具來進行性能分析,所以本文就來和大家講講pprof的具體使用吧
    2023-05-05
  • 淺析Go語言中的逃逸分析

    淺析Go語言中的逃逸分析

    逃逸分析算是go語言的特色之一,所以這篇文章小編就來和大家聊聊為什么不應該過度關注go語言的逃逸分析,感興趣的小伙伴可以跟隨小編一起了解一下
    2024-10-10
  • Go gRPC環(huán)境安裝教程示例詳解

    Go gRPC環(huán)境安裝教程示例詳解

    這篇文章主要為大家介紹了Go gRPC環(huán)境安裝的教程示例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪
    2022-06-06
  • 如何使用go實現(xiàn)創(chuàng)建WebSocket服務器

    如何使用go實現(xiàn)創(chuàng)建WebSocket服務器

    文章介紹了如何使用Go語言和gorilla/websocket庫創(chuàng)建一個簡單的WebSocket服務器,并實現(xiàn)商品信息的實時廣播,感興趣的朋友一起看看吧
    2024-11-11
  • Golang優(yōu)雅關閉channel的方法示例

    Golang優(yōu)雅關閉channel的方法示例

    Goroutine和channel是Go在“并發(fā)”方面兩個核心feature,下面這篇文章主要給大家介紹了關于Golang如何優(yōu)雅關閉channel的相關資料,文中通過示例代碼介紹的非常詳細,需要的朋友可以參考解決,下面來一起看看吧。
    2017-11-11
  • Gin golang web開發(fā)模型綁定實現(xiàn)過程解析

    Gin golang web開發(fā)模型綁定實現(xiàn)過程解析

    這篇文章主要介紹了Gin golang web開發(fā)模型綁定實現(xiàn)過程解析,文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友可以參考下
    2020-10-10

最新評論