前言
前段時間,校招投了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訪問各個節點