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

golang高并發(fā)之本地緩存詳解

 更新時(shí)間:2024年10月28日 08:55:13   作者:snail_lie  
這篇文章主要為大家詳細(xì)介紹了golang高并發(fā)中本地緩存的相關(guān)知識(shí),文中的示例代碼講解詳細(xì),感興趣的小伙伴可以跟隨小編一起學(xué)習(xí)一下

一、使用場景

試想一個(gè)場景,有一個(gè)配置服務(wù)系統(tǒng),里面存儲(chǔ)著各種各樣的配置,比如直播間的直播信息、點(diǎn)贊、簽到、紅包、帶貨等等。這些配置信息有兩個(gè)特點(diǎn):

并發(fā)量可能會(huì)特別特別大,試想一下,一個(gè)幾十萬人的直播間,可能在直播開始前幾秒鐘,用戶就瞬間涌入進(jìn)來了,那么這時(shí)候我們的系統(tǒng)就得加載這些配置信息。此時(shí)請求量就如同洪峰一般,一下子就沖擊進(jìn)入我們的系統(tǒng)。

這些配置通常都是只需要讀取,在B端(管理后臺(tái))設(shè)置好的,一般直播開始后,修改的頻率很低。

那么面對上述的業(yè)務(wù)場景,假設(shè)我們的目標(biāo)是扛住3wQPS,你們會(huì)選用什么技術(shù)架構(gòu)和方案呢?

1、直接查數(shù)據(jù)庫,例如MySQL、Doris之類的關(guān)系型數(shù)據(jù)庫。很明顯這肯定扛不住,一般關(guān)系型數(shù)據(jù)庫能讓扛個(gè)幾千就基本上到頭了。

2、使用單機(jī)版Redis。理論上是可以的,騰訊云(下圖)和一些Redis官方的數(shù)據(jù),都說理論上高配置版本的單機(jī)Redis能抗住10W+的QPS??墒抢碚摦吘故抢碚摚瑢?shí)際上工作中,我使用Redis做過許多壓測,都表明單機(jī)Redis上了兩萬多之后就性能會(huì)出現(xiàn)瓶頸,壓測就壓不上去。(當(dāng)然,或許是我司的Redis還沒升到頂配?)

3、使用集群版Redis,當(dāng)然是可以解決這個(gè)問題,就是成本有點(diǎn)點(diǎn)高咯,公司不差錢完全可以使用這個(gè)方案。

4、本地緩存,就是本文的重點(diǎn),完美地解決這個(gè)問題。所謂本地緩存就是將這些所需要獲取的數(shù)據(jù)存儲(chǔ)在服務(wù)器的內(nèi)存中。服務(wù)器讀取本地緩存的速度理論上來說沒有上限,看服務(wù)器物理機(jī)的配置。但其下限就遠(yuǎn)比MySQL和單機(jī)Redis之類的高好幾倍了,一臺(tái)2核4G的Linux服務(wù)器,估計(jì)也至少10W+QPS起步。我曾經(jīng)在本地的Windows系統(tǒng)做過壓測(四核八線程,16G),就達(dá)到過100W+的QPS。換到同等配置的Linux系統(tǒng)上,那就更不用說了。

二、技術(shù)方案

既然選用了本地緩存這個(gè)策略,那么我們怎么設(shè)計(jì)這個(gè)本地緩存的技術(shù)方案呢?

1、如上圖所示,我們客戶端獲取數(shù)據(jù)首先會(huì)讀取本地緩存,如果本地緩存沒有數(shù)據(jù)就會(huì)讀取Redis數(shù)據(jù),如果Redis沒有就會(huì)讀取DB數(shù)據(jù)。

2、需要注意的是,本地緩存和DB之間一般還會(huì)加入Redis這一層緩存。這是因?yàn)楸镜鼐彺嬖O(shè)置好后就無法再更新了(除非重啟服務(wù)器),而Redis緩存我們是可以在DB有更改后,隨時(shí)更新。這個(gè)也很好理解,因?yàn)镽edis是有單獨(dú)的Redis服務(wù)器,而本地緩存就只能在那臺(tái)機(jī)器上更新和設(shè)置,但實(shí)際項(xiàng)目中,設(shè)置本地緩存的DB數(shù)據(jù)源的機(jī)器和使用本地緩存的機(jī)器大概率都不在同一個(gè)系統(tǒng)中。所以我們本地緩存的時(shí)間都設(shè)置得很短,大部分都是秒級(jí)的,一般不會(huì)超過1分鐘,比如1秒、2秒... 。而Redis這個(gè)緩存時(shí)長明顯可以設(shè)置長一些,比如半小時(shí)、1小時(shí)...。

三、如何更新本地緩存

上面講了,本地緩存最不好的地方就是更新問題,因?yàn)楹芸赡茉O(shè)置本地緩存的DB數(shù)據(jù)源的系統(tǒng)和使用本地緩存的系統(tǒng)不是同一個(gè),無法在DB數(shù)據(jù)更新的時(shí)候就同步更新本地緩存。但是實(shí)際使用的時(shí)候很可能就需要這種場景,就是在更新數(shù)據(jù)源的時(shí)候去更新本地緩存。舉個(gè)例子:

我們設(shè)置配置A的DB數(shù)據(jù)源的系統(tǒng)是一個(gè)API系統(tǒng),但現(xiàn)在有一個(gè)腳本系統(tǒng),需要根據(jù)某個(gè)配置A,去處理C端的一些行為數(shù)據(jù),判斷是否滿足該配置A,然后進(jìn)行對應(yīng)的業(yè)務(wù)處理。好了,現(xiàn)在C端的行為數(shù)據(jù)量是非常龐大的,可以說是海量數(shù)據(jù),平均每秒鐘有五十萬的數(shù)據(jù)通過kafka推送過來。此時(shí)我們就必須得用本地緩存存儲(chǔ)配置A的信息了,才能抗的住這個(gè)流量洪峰。但是這是一個(gè)腳本系統(tǒng)啊,我們更新配置A的DB信息是在對應(yīng)的API系統(tǒng)中的。那怎么辦呢?

有幾個(gè)方法:

1、在腳本系統(tǒng)中維護(hù)一個(gè)腳本,每隔一段時(shí)間就去讀取MySQL的數(shù)據(jù),然后更新到本地緩存。但這個(gè)得綜合評(píng)估下時(shí)間和MySQL的性能,因?yàn)橐恢睊弑怼?/p>

2、拉取MySQL的binlog日志,每當(dāng)數(shù)據(jù)有變更時(shí),kafka推送數(shù)據(jù)到下游。腳本監(jiān)聽kafka數(shù)據(jù),當(dāng)收到kafka數(shù)據(jù)是就更新配置A的本地緩存。但這個(gè)也得注意,因?yàn)槟_本系統(tǒng)一般會(huì)同時(shí)起很多個(gè)服務(wù),所以得注意有多少個(gè)服務(wù)就得設(shè)置多少個(gè)消費(fèi)者組,因?yàn)橐WC腳本系統(tǒng)的每個(gè)服務(wù)都消費(fèi)到kafka對應(yīng)的DB更新數(shù)據(jù),進(jìn)而更新各自機(jī)器上的本地緩存。

3、使用Redis的發(fā)布訂閱功能,上游api有更新配置信息時(shí)就去發(fā)布信息,每個(gè)腳本服務(wù)都去訂閱該信息,一有消息就去更新自己機(jī)器上的本地緩存。但這也有個(gè)弊端,Redis的發(fā)布訂閱功能是沒有確認(rèn)機(jī)制的,所以可能某個(gè)腳本服務(wù)沒收到信息導(dǎo)致沒更新本地緩存,然后就出現(xiàn)bug了。demo如下:

(1)發(fā)布者:

package main

import (
	"context"
	"fmt"
	"github.com/go-redis/redis/v8"
	"time"
)

var ctx = context.Background()

// 發(fā)布訂閱功能
// 發(fā)布者發(fā)布,所有訂閱者都能接收到發(fā)布的消息。注意區(qū)分消息隊(duì)列,消息隊(duì)列是發(fā)布者發(fā)布,只有一個(gè)訂閱者能搶到。
func main() {
	rdb := redis.NewClient(&redis.Options{
		Addr:     "localhost:16379",
		Password: "123456",
	})

	i := 0
	for {
		// 模擬數(shù)據(jù)更新時(shí)發(fā)布消息
		rdb.Publish(ctx, "money_updates", "New money value updated "+fmt.Sprintf("%d", i))
		fmt.Println("Message published " + fmt.Sprintf("%d", i))
		time.Sleep(5 * time.Second)
		i++
	}

}

(2)訂閱者:

package main

import (
	"context"
	"fmt"
	"github.com/go-redis/redis/v8"
)

var ctx = context.Background()

func main() {
	rdb := redis.NewClient(&redis.Options{
		Addr:     "localhost:16379",
		Password: "123456",
	})

	pubsub := rdb.Subscribe(ctx, "money_updates")
	defer pubsub.Close()

	// 等待消息
	for {
		msg, err := pubsub.ReceiveMessage(ctx)
		if err != nil {
			fmt.Println("Error receiving message:", err)
			return
		}
		fmt.Println("Received message:", msg.Payload)
	}
}

4、跟方法1類似,只是可以把修改的配置A信息推送到Redis中,然后腳本去掃描Redis信息,有則更新本地緩存。其實(shí)就是延遲隊(duì)列。但這個(gè)就得上游的配置A增刪改都要寫入這個(gè)Redis,有時(shí)候增刪改的口子太多,其實(shí)實(shí)施起來也比較困難。

如上所述,基本上更新本地緩存沒有一個(gè)很合適、很高效的方法,只能選取其中一個(gè)比較符合自己業(yè)務(wù)場景的方法。

四、本地緩存常用類庫

go如何使用本地緩存呢?

1、可以自己實(shí)現(xiàn)一個(gè)本地緩存,一般可以使用LRU(最近最少使用)算法。下面是自己實(shí)現(xiàn)的一個(gè)本地緩存的demo。

package main

import (
	"container/list"
	"fmt"
	"sync"
	"time"
)

type Cache struct {
	capacity int
	cache    map[int]*list.Element
	lruList  *list.List
	mu       sync.Mutex // 確保線程安全
}

type entry struct {
	key        int
	value      int
	expiration time.Time // 過期時(shí)間
}

// NewCache 創(chuàng)建新的緩存
func NewCache(capacity int) *Cache {
	return &Cache{
		capacity: capacity,
		cache:    make(map[int]*list.Element),
		lruList:  list.New(),
	}
}

// Get 從緩存中獲取值
func (c *Cache) Get(key int) (int, bool) {
	c.mu.Lock()
	defer c.mu.Unlock()

	if elem, found := c.cache[key]; found {
		// 檢查是否過期
		if elem.Value.(entry).expiration.After(time.Now()) {
			// 移動(dòng)到鏈表頭部 (最近使用)
			c.lruList.MoveToFront(elem)
			return elem.Value.(entry).value, true
		}
		// 如果過期,刪除緩存項(xiàng)
		c.removeElement(elem)
	}
	return 0, false
}

// Put 將值放入緩存
func (c *Cache) Put(key int, value int, ttl time.Duration) {
	c.mu.Lock()
	defer c.mu.Unlock()

	if elem, found := c.cache[key]; found {
		// 更新現(xiàn)有值
		elem.Value = entry{key, value, time.Now().Add(ttl)}
		c.lruList.MoveToFront(elem)
	} else {
		// 添加新條目
		if c.lruList.Len() == c.capacity {
			// 刪除最舊的條目
			oldest := c.lruList.Back()
			if oldest != nil {
				c.removeElement(oldest)
			}
		}
		newElem := c.lruList.PushFront(entry{key, value, time.Now().Add(ttl)})
		c.cache[key] = newElem
	}
}

// removeElement 從緩存中刪除元素
func (c *Cache) removeElement(elem *list.Element) {
	c.lruList.Remove(elem)
	delete(c.cache, elem.Value.(entry).key)
}

// 清理過期項(xiàng)
func (c *Cache) CleanUp() {
	c.mu.Lock()
	defer c.mu.Unlock()

	for e := c.lruList.Back(); e != nil; {
		next := e.Prev()
		if e.Value.(entry).expiration.Before(time.Now()) {
			c.removeElement(e)
		}
		e = next
	}
}

func main() {
	cache := NewCache(2)
	cache.Put(1, 1, 5*time.Second) // 設(shè)置過期時(shí)間為5秒
	cache.Put(2, 2, 5*time.Second)

	fmt.Println(cache.Get(1)) // 輸出: 1 true
	time.Sleep(6 * time.Second)

	// 過期后的訪問
	fmt.Println(cache.Get(1)) // 輸出: 0 false
	cache.CleanUp()           // 進(jìn)行清理
}

該代碼中使用LRU算法,通過將最新的緩存移動(dòng)到鏈表頭部(最近使用)來實(shí)現(xiàn)這個(gè)算法。但也有一些問題,CleanUp 方法需要手動(dòng)調(diào)用去清理過期緩存,并沒有定期自動(dòng)清理的機(jī)制。這就意味著使用者可能需要頻繁調(diào)用 CleanUp,否則過期項(xiàng)可能會(huì)在緩存中停留較長時(shí)間。代碼中也加了鎖,可能還會(huì)存在并發(fā)訪問時(shí)的數(shù)據(jù)一致性和性能問題等等。

所以,這種自己實(shí)現(xiàn)的demo不建議放在生產(chǎn)環(huán)境中使用,可能會(huì)存在一些小問題。比如,之前我部門寫的一個(gè)本地緩存類庫,就存在一個(gè)大的bug:本地緩存的內(nèi)存空間不能釋放,導(dǎo)致內(nèi)存一直蹭蹭地往上漲。隔好幾天內(nèi)存就飆到90%,然后我們臨時(shí)處理方法是:隔好幾天就去重啟一次腳本...

2、所以呢,我們還是建議去使用開源的類庫,至少有許多前輩幫我們踩過坑了,這里推薦幾個(gè)star數(shù)比較高的:

(1)go-cache:一個(gè)簡單的內(nèi)存緩存庫,支持過期和自動(dòng)清理,適合簡單的緩存key-value需求。(本人項(xiàng)目中使用比較多,方便簡單,推薦)

(2)bigcache:高性能的內(nèi)存緩存庫,適用于大量數(shù)據(jù)的緩存,其設(shè)計(jì)旨在減少垃圾回收的壓力。

(3)groupcache:Google 開發(fā)的一個(gè)緩存庫,支持分布式緩存和單機(jī)緩存。適用于需要高并發(fā)和高可用性的場景。

以上,就是個(gè)人使用本地緩存的一些經(jīng)驗(yàn)了。不得不說,這玩意用著是真香,物美價(jià)廉,能扛能打。唯一美中不足的就是本地緩存不太好實(shí)時(shí)去更新,當(dāng)然這個(gè)上面也給出了幾個(gè)解決方案。

到此這篇關(guān)于golang高并發(fā)之本地緩存詳解的文章就介紹到這了,更多相關(guān)go本地緩存內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!

相關(guān)文章

  • golang頻率限制 rate詳解

    golang頻率限制 rate詳解

    這篇文章主要介紹了golang頻率限制 rate詳解,具有很好的參考價(jià)值,希望對大家有所幫助。一起跟隨小編過來看看吧
    2020-12-12
  • go實(shí)現(xiàn)反轉(zhuǎn)鏈表

    go實(shí)現(xiàn)反轉(zhuǎn)鏈表

    這篇文章主要介紹了go實(shí)現(xiàn)反轉(zhuǎn)鏈表的操作,具有很好的參考價(jià)值,希望對大家有所幫助。一起跟隨小編過來看看吧
    2021-04-04
  • Golang函數(shù)這些神操作你知道哪些

    Golang函數(shù)這些神操作你知道哪些

    這篇文章主要為大家介紹了一些Golang中函數(shù)的神操作,不知道你都知道哪些呢?文中的示例代碼講解詳細(xì),具有一定的學(xué)習(xí)價(jià)值,需要的可以參考一下
    2023-02-02
  • golang 如何獲取pem格式RSA公私鑰長度

    golang 如何獲取pem格式RSA公私鑰長度

    這篇文章主要介紹了golang 如何獲取pem格式RSA公私鑰長度操作,具有很好的參考價(jià)值,希望對大家有所幫助。一起跟隨小編過來看看吧
    2020-12-12
  • Golang: 內(nèi)建容器的用法

    Golang: 內(nèi)建容器的用法

    這篇文章主要介紹了Golang: 內(nèi)建容器的用法,具有很好的參考價(jià)值,希望對大家有所幫助。一起跟隨小編過來看看吧
    2021-05-05
  • Go接口構(gòu)建可擴(kuò)展Go應(yīng)用示例詳解

    Go接口構(gòu)建可擴(kuò)展Go應(yīng)用示例詳解

    本文深入探討了Go語言中接口的概念和實(shí)際應(yīng)用場景。從基礎(chǔ)知識(shí)如接口的定義和實(shí)現(xiàn),到更復(fù)雜的實(shí)戰(zhàn)應(yīng)用如解耦與抽象、多態(tài)、錯(cuò)誤處理、插件架構(gòu)以及資源管理,文章通過豐富的代碼示例和詳細(xì)的解釋,展示了Go接口在軟件開發(fā)中的強(qiáng)大功能和靈活性
    2023-10-10
  • Go項(xiàng)目配置管理神器之viper的介紹與使用詳解

    Go項(xiàng)目配置管理神器之viper的介紹與使用詳解

    viper是一個(gè)完整的?Go應(yīng)用程序的配置解決方案,它被設(shè)計(jì)為在應(yīng)用程序中工作,并能處理所有類型的配置需求和格式,下面這篇文章主要給大家介紹了關(guān)于Go項(xiàng)目配置管理神器之viper的介紹與使用,需要的朋友可以參考下
    2023-02-02
  • Golang使用WebSocket通信的實(shí)現(xiàn)

    Golang使用WebSocket通信的實(shí)現(xiàn)

    這篇文章主要介紹了Golang使用WebSocket通信的實(shí)現(xiàn),文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧
    2021-02-02
  • golang中map增刪改查的示例代碼

    golang中map增刪改查的示例代碼

    在Go語言中,map是一種內(nèi)置的數(shù)據(jù)結(jié)構(gòu),用于存儲(chǔ)鍵值對,本文主要介紹了golang中map增刪改查的示例代碼,具有一定的參考價(jià)值,感興趣的可以了解一下
    2023-11-11
  • Go語言繼承功能使用結(jié)構(gòu)體實(shí)現(xiàn)代碼重用

    Go語言繼承功能使用結(jié)構(gòu)體實(shí)現(xiàn)代碼重用

    今天我來給大家介紹一下在?Go?語言中如何實(shí)現(xiàn)類似于繼承的功能,讓我們的代碼更加簡潔和可重用,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪
    2024-01-01

最新評(píng)論