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

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

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

一、使用場景

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

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

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

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

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

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

3、使用集群版Redis,當然是可以解決這個問題,就是成本有點點高咯,公司不差錢完全可以使用這個方案。

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

二、技術(shù)方案

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

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

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

三、如何更新本地緩存

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

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

有幾個方法:

1、在腳本系統(tǒng)中維護一個腳本,每隔一段時間就去讀取MySQL的數(shù)據(jù),然后更新到本地緩存。但這個得綜合評估下時間和MySQL的性能,因為要一直掃表。

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

3、使用Redis的發(fā)布訂閱功能,上游api有更新配置信息時就去發(fā)布信息,每個腳本服務都去訂閱該信息,一有消息就去更新自己機器上的本地緩存。但這也有個弊端,Redis的發(fā)布訂閱功能是沒有確認機制的,所以可能某個腳本服務沒收到信息導致沒更新本地緩存,然后就出現(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ū)分消息隊列,消息隊列是發(fā)布者發(fā)布,只有一個訂閱者能搶到。
func main() {
	rdb := redis.NewClient(&redis.Options{
		Addr:     "localhost:16379",
		Password: "123456",
	})

	i := 0
	for {
		// 模擬數(shù)據(jù)更新時發(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信息,有則更新本地緩存。其實就是延遲隊列。但這個就得上游的配置A增刪改都要寫入這個Redis,有時候增刪改的口子太多,其實實施起來也比較困難。

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

四、本地緩存常用類庫

go如何使用本地緩存呢?

1、可以自己實現(xiàn)一個本地緩存,一般可以使用LRU(最近最少使用)算法。下面是自己實現(xiàn)的一個本地緩存的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 // 過期時間
}

// 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()) {
			// 移動到鏈表頭部 (最近使用)
			c.lruList.MoveToFront(elem)
			return elem.Value.(entry).value, true
		}
		// 如果過期,刪除緩存項
		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)
}

// 清理過期項
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è)置過期時間為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()           // 進行清理
}

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

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

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

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

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

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

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

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

相關(guān)文章

  • golang頻率限制 rate詳解

    golang頻率限制 rate詳解

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

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

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

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

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

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

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

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

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

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

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

    Go項目配置管理神器之viper的介紹與使用詳解

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

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

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

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

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

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

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

最新評論