Golang校招簡歷項目-簡單的分布式緩存


前言

前段時間,校招投了golang崗位,但是沒什么好的項目往簡歷上寫,於是參考了許多網上資料,做了一個簡單的分布式緩存項目。
現在閑下來了,打算整理下。

github項目地址:https://github.com/Jun10ng/Gache
里面還有我整理的一些面試問題,給顆星吧。


typora-root-url: ./


Golang校招面試項目-類redis分布式緩存

實現一個分布式緩存,功能有:LRU淘汰策略,http調用,並發緩存,一致性哈希,分布式節點,防止緩存擊穿

實現LRU淘汰策略

LRU的數據結構大致如下,上層是一個map,key是數據對象的key值,而value值則是指向 下層雙向鏈表的節點,在雙向鏈表中,每個節點存儲的元素是完整的數據對象,包含key值和value。

  • get:存在->將元素所在節點提到最前面,不存在->返回失敗
  • add:存在->更新,不存在->增加;將元素所在節點提到最前面,判斷是否大於maxSize
  • removeOldest:刪除鏈表最后方的節點

代碼實現

具體代碼實現看:https://github.com/Jun10ng/Gache/tree/master/lru

定義了三個數據結構

Value是golang中的接口類型,可以理解為java中的Object類,是一個能“兜底”所有數據結構的數據類型。

entry是一個雙向鏈表存儲的數據結構

Cache則是lru核心數據結構,包含一個哈希表和一個雙向鏈表

type Value interface {

	//返回占用的內存大小
	Len() int
}

type entry struct {
	key string
	value Value
}

type Cache struct {
	//允許使用的最大內存
	maxBytes int64

	//當前已使用的內存
	nbytes int64


	ll *list.List

	cache map[string] *list.Element

	//某條記錄被移除時的回調函數,可以是nil
	OnEvicted func(key string, value Value)

}

這里說一下OnEvicted成員,這是一個函數對象,他的作用是,在緩存中沒有需要的數據對象時,我們需要去原始數據源獲取,(redis中沒有,就需要去數據庫中獲取),但是數據源不唯一,有時候是數據庫,有時候是磁盤,有時候是表格,他們的獲取方式都不相同,所以OnEvicted成員傳入的函數,就是自定義的獲取方法。

實現單機並發

具體代碼實現:https://github.com/Jun10ng/Gache/blob/master/cache.go

上文實現的LRU數據結構並不支持並發,需要加鎖來實現並發,所以使用sync.Mutex,在LRU數據結構上封裝,使之實現並發功能。

type cache struct {
	mu sync.Mutex
	lru *lru.Cache
	cacheBytes int64
}

cache並沒有new方法,因為采用的是延遲初始化 在add方法中,判斷c.lru是否為nil,如果等於nil再創建 這種方法稱為延遲初始化,一個對象的延遲初始化意味着該對象的 創建將會延遲至第一次使用該對象時。 這個方法在redis中很常見,因為能一定程度上提高性能

func (c *cache) add(key string, value ByteView){
	c.mu.Lock()
	defer c.mu.Unlock()
	if c.lru == nil{
		c.lru = lru.New(c.cacheBytes,nil)
	}
	c.lru.Add(key,value)
}

主體結構

具體代碼實現:https://github.com/Jun10ng/Gache/blob/master/gache.go

本質上是再進行一次封裝

難道一台機器就只有一個緩存表嗎?你打開redis的可視化工具,能看到redis還有16個池呢,所以我們要實現多個緩存表。怎么做?再加一層。試想一下:

//groups 實例集合表
groups = make(map[string]*Group)

我們要實現的數據結構大致是這樣的,是一個存儲並發cache的表,這是本項目的核心結構

//這里的group是實例
type Group struct {
	name string
	getter Getter
	mainCache cache
}

http服務調用

具體代碼實現:https://github.com/Jun10ng/Gache/blob/master/http.go

當請求URL具有前綴/_Gache/時,則認為該請求為緩存調用。

約定的請求URL為:http://XXX.com/_Gache/<groupname>/<key>

groupname字段為主體結構中groups中的某個元素的name值,由此調用。key字段為元素中的元素的key值,所以最后邏輯為

groups[groupname][key]

一致性哈希

一致性哈希抽象的解釋就是一個很大的環,但是在實現的時候,我們總不可能聲明一個有個成千鏈表節點的環吧,何況其中大多節點還是閑置節點,沒有實際的作用,所以我們需要在邏輯上去聲明哈希環。

代碼實現:https://github.com/Jun10ng/Gache/blob/master/consistent/consistentHash.go

數據結構

(真實節點就是指機器,虛擬節點相反)

type Map struct {
	hash Hash
	virMpl int
	keys []int
	hashMap map[int]string
}
  • hash是函數變量
  • virMpl是虛擬節點的倍數
  • keys是存放節點哈希值的有序數組
  • hashMap中存放的是虛擬節點和真實節點的對映,之所以是[int]string類型,是因為key是虛擬節點的哈希值,value是真實節點

添加真實節點

代碼注釋寫的很詳細了,就不多說了。

缺點是,當有一個真實節點添加進來的時候,所有值都要重新計算一遍。這在並發情況下,會造成一定擁塞。因為在重新計算期間,不能進行正確的訪問操作。

歡迎提供解決思路。

func (m* Map) Add(keys ...string){
	for _,realNodeKey:=range keys{
		for i:=0;i<m.virMpl;i++{
			/*
				keys中的每個真實節點都對映着virMpl個虛擬節點
				每個虛擬節點的key(即virNodeKey)為 i+realNodekey
				(即一個“不定數”,這里用i值,加上真實節點key
			*/
			virNodeKey := []byte(strconv.Itoa(i)+realNodeKey)
			/*
				對虛擬節點做哈希
			*/
			virNodeHash:= int(m.hash(virNodeKey))
			/*
				添加進哈希環,所以虛擬節點也存在於哈希環中
			*/
			m.keys = append(m.keys,virNodeHash)
			/*
				虛擬節點的hash對映某個真實節點的key
			*/
			m.hashMap[virNodeHash] = realNodeKey
		}
	}
	sort.Ints(m.keys)
}

訪問真實節點

也就是get函數

分為三個步驟

  • 計算出虛擬節點的哈希值virNodeHash
  • keys數組中找到大於等於virNodeHash的值,返回其下標index,則對應的節點為keys[index]
  • 通過下標在hashMap中找到keys[index]的真實節點

自己試着寫下get函數,會對整個邏輯更清晰。

分布式節點設計

這一章涉及的東西有點多,在代碼中給出了詳細的注釋,

主要是下面幾個文件:

https://github.com/Jun10ng/Gache/blob/master/peer.go

定義了兩個抽象接口,用於遠程節點的獲取

https://github.com/Jun10ng/Gache/blob/master/http.go

實現了peer.go中的兩個接口,並定義了新的結構體httpGetter用於獲取遠程節點緩存數據

https://github.com/Jun10ng/Gache/blob/master/gache.go

集成了一致性哈希表,使用http訪問各個節點

主要參考資料:https://geektutu.com/post/geecache.html


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM