欧美bbbwbbbw肥妇,免费乱码人妻系列日韩,一级黄片

基于context.Context的Golang?loader緩存請(qǐng)求放大問題解決

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

請(qǐng)求放大的問題

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

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

為什么需要考慮這個(gè)問題?

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

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

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

所以,我們需要嚴(yán)肅地看待這件事。目標(biāo)其實(shí)很明確:

  • 只拿需要的數(shù)據(jù);
  • 不重復(fù)拿同一份數(shù)據(jù)(如果數(shù)據(jù)可能會(huì)變,可以考慮放大,這不是絕對(duì)的);
  • 處理好強(qiáng)弱依賴,不因?yàn)橐粋€(gè)明明可以接受,降級(jí)的失敗請(qǐng)求,導(dǎo)致整個(gè)處理流程中斷。

那我們?cè)趺床拍鼙WC一個(gè)請(qǐng)求處理過(guò)程中,不去重復(fù)請(qǐng)求下游呢?我只是其中一環(huán),怎么知道此前流程里是不是已經(jīng)拿過(guò)數(shù)據(jù)了呢?就算知道,人家都放到了自己業(yè)務(wù)的結(jié)構(gòu)體里,我怎么用?

中間件能解決么?

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

接口中間件里我就把余額拿到,隨后作為一個(gè)公共的結(jié)構(gòu)體,一路透?jìng)?。類似這樣:

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

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

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

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

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

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

基于 context.Context 的解決方案

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

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

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

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

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

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

loader 定義

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

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

存儲(chǔ)結(jié)構(gòu)

我們希望往 Context 里面放什么數(shù)據(jù),這一點(diǎn)很關(guān)鍵。鑒于我們希望支持多個(gè)業(yè)務(wù)場(chǎng)景,勢(shì)必會(huì)需要一個(gè) map 結(jié)構(gòu),key 對(duì)應(yīng)場(chǎng)景,value 是緩存的值。

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

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

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

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

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

這里 cacheItem 里面放什么,很關(guān)鍵!

  • 是不是直接就一個(gè) interface{} 就可以了?

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

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

  • 錯(cuò)誤如何感知?

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

綜上兩點(diǎn),一個(gè)可能的結(jié)構(gòu)如下:

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

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

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

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

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

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

我們從 Context 直接獲取的結(jié)構(gòu)是 callCache,那么當(dāng)某個(gè)場(chǎng)景的 key 首次請(qǐng)求的時(shí)候,勢(shì)必需要對(duì) cacheItem 進(jìn)行初始化。

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

  • 既然用了 RWMutex,我們希望把讀寫粒度拆開,所以一上來(lái)應(yīng)該判斷讀鎖,如果有值,直接返回;
  • 如果在讀鎖里沒獲取到,說(shuō)明需要初始化,開始加寫鎖;
  • 在寫鎖中,完成初始化,寫入 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ā)者怎么用這個(gè) lib。

WithCallCache

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

此外,既然要往 Context 里面放,一定需要一個(gè)自己的 key,這里我們采用空結(jié)構(gòu)體,用來(lái)與其他類型區(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ā)者傳進(jìn)來(lái):1.業(yè)務(wù)場(chǎng)景;2.業(yè)務(wù)對(duì)應(yīng)的 loader。

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

// LoadFromCtxCache 從ctx中嘗試獲取key的緩存結(jié)果
// 如果不存在,調(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 針對(duì)當(dāng)前的 ctx 啟用 loader cache;
  • 改造數(shù)據(jù)加載邏輯,抽出來(lái) loader,外層用 LoadFromCtxCache 來(lái)調(diào)用,以達(dá)到上游無(wú)感。

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

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

源碼倉(cāng)庫(kù):go-ctxcache,感興趣的同學(xué)可以試一下,整體代碼量很小,實(shí)用性很強(qiáng)。

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

相關(guān)文章

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

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

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

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

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

    詳解golang中Context超時(shí)控制與原理

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

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

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

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

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

    淺析Go語(yǔ)言中的逃逸分析

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

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

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

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

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

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

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

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

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

最新評(píng)論