一、groupcache介紹: http://www.csdn.net/article/2013-07-30/2816399-groupcache-readme-go git
二、groupcache經常使用框架:github
通常經常使用以上的框架去使用groupcache,此框架以及框架示例代碼可經過https://github.com/capotej/groupcache-db-experiment 下載, 框架示例核心源碼解讀:算法
func main() { var port = flag.String("port", "8001", "groupcache port") flag.Parse() peers := groupcache.NewHTTPPool("http://localhost:" + *port) client := new(client.Client) var stringcache = groupcache.NewGroup("SlowDBCache", 64<<20, groupcache.GetterFunc( func(ctx groupcache.Context, key string, dest groupcache.Sink) error { result := client.Get(key) fmt.Printf("asking for %s from dbserver\n", key) dest.SetBytes([]byte(result)) return nil })) peers.Set("http://localhost:8001", "http://localhost:8002", "http://localhost:8003") frontendServer := NewServer(stringcache) i, err := strconv.Atoi(*port) if err != nil { // handle error fmt.Println(err) os.Exit(2) } var frontEndport = ":" + strconv.Itoa(i+1000) go frontendServer.Start(frontEndport) fmt.Println(stringcache) fmt.Println("cachegroup slave starting on " + *port) fmt.Println("frontend starting on " + frontEndport) http.ListenAndServe("127.0.0.1:"+*port, http.HandlerFunc(peers.ServeHTTP)) }
理解以上這段代碼須要首先理解groupcache中的peer如何與HttpPool產生關聯,關鍵代碼段:數據庫
func NewHTTPPoolOpts(self string, o *HTTPPoolOptions) *HTTPPool { if httpPoolMade { panic("groupcache: NewHTTPPool must be called only once") } httpPoolMade = true opts := HTTPPoolOptions{} if o != nil { opts = *o } if opts.BasePath == "" { opts.BasePath = defaultBasePath } if opts.Replicas == 0 { opts.Replicas = defaultReplicas } p := &HTTPPool{ basePath: opts.BasePath, self: self, peers: consistenthash.New(opts.Replicas, opts.HashFn), httpGetters: make(map[string]*httpGetter), } RegisterPeerPicker(func() PeerPicker { return p }) return p }
經過RegisterPeerPicker將獲取httppool的對象返回函數註冊到全局的portPicker,這樣在調用Group的Get接口時,經過調用initPeers接口返回HTTPPool對象,HTTPPool與groupcache的關聯就是經過portPicker函數變量;後端
三、groupcache源碼解讀緩存
A、在使用以上框架的時候或許你會困惑,經過key分片,而後去遠端獲取數據時,在遠端仍然是經過調用HTTPPool的ServeHttp來進行處理,咱們先來看下該接口代碼實現:數據結構
func (p *HTTPPool) ServeHTTP(w http.ResponseWriter, r *http.Request) { // Parse request. if !strings.HasPrefix(r.URL.Path, p.basePath) { panic("HTTPPool serving unexpected path: " + r.URL.Path) } parts := strings.SplitN(r.URL.Path[len(p.basePath):], "/", 2) if len(parts) != 2 { http.Error(w, "bad request", http.StatusBadRequest) return } groupName := parts[0] key := parts[1] // Fetch the value for this group/key. group := GetGroup(groupName) if group == nil { http.Error(w, "no such group: "+groupName, http.StatusNotFound) return } var ctx Context if p.Context != nil { ctx = p.Context(r) } group.Stats.ServerRequests.Add(1) var value []byte err := group.Get(ctx, key, AllocatingByteSliceSink(&value)) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } // Write the value to the response body as a proto message. body, err := proto.Marshal(&pb.GetResponse{Value: value}) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } w.Header().Set("Content-Type", "application/x-protobuf") w.Write(body) }
首先經過GetGroup獲取本地的group對象指針,而後group.Get(ctx, key, AllocatingByteSliceSink(&value))獲取數據,而在Frontend中也是經過調用Get接口獲取數據,這樣會不會造成死循環 ? 爲解答這一問題,首先咱們來看下groupcache中的Get接口:app
func (g *Group) Get(ctx Context, key string, dest Sink) error { g.peersOnce.Do(g.initPeers) g.Stats.Gets.Add(1) if dest == nil { return errors.New("groupcache: nil dest Sink") } value, cacheHit := g.lookupCache(key) if cacheHit { fmt.Printf("key %s cache hit!\n", key) g.Stats.CacheHits.Add(1) return setSinkView(dest, value) } // Optimization to avoid double unmarshalling or copying: keep // track of whether the dest was already populated. One caller // (if local) will set this; the losers will not. The common // case will likely be one caller. destPopulated := false value, destPopulated, err := g.load(ctx, key, dest) if err != nil { return err } if destPopulated { return nil } return setSinkView(dest, value) }
大概流程:先執行initPeers獲取遠端peer,查本地緩存是否有數據,若是命中,返回數據,不然load數據,看load實現:框架
func (g *Group) load(ctx Context, key string, dest Sink) (value ByteView, destPopulated bool, err error) { g.Stats.Loads.Add(1) viewi, err := g.loadGroup.Do(key, func() (interface{}, error) { g.Stats.LoadsDeduped.Add(1) var value ByteView var err error if peer, ok := g.peers.PickPeer(key); ok { value, err = g.getFromPeer(ctx, peer, key) if err == nil { g.Stats.PeerLoads.Add(1) return value, nil } g.Stats.PeerErrors.Add(1) // TODO(bradfitz): log the peer's error? keep // log of the past few for /groupcachez? It's // probably boring (normal task movement), so not // worth logging I imagine. } value, err = g.getLocally(ctx, key, dest) if err != nil { g.Stats.LocalLoadErrs.Add(1) return nil, err } g.Stats.LocalLoads.Add(1) destPopulated = true // only one caller of load gets this return value g.populateCache(key, value, &g.mainCache) return value, nil }) if err == nil { value = viewi.(ByteView) } return }
大概流程:根據key選擇一個固定的遠端peer,若是獲取成功,那麼從遠端獲取數據,不然getLocally直接從後端(數據庫或者其餘數據服務)獲取數據;讀到這裏仍然沒法解答這一疑惑,繼續看PickPeer接口:frontend
func (p *HTTPPool) PickPeer(key string) (ProtoGetter, bool) { p.mu.Lock() defer p.mu.Unlock() if p.peers.IsEmpty() { return nil, false } if peer := p.peers.Get(key); peer != p.self { return p.httpGetters[peer], true } return nil, false }
根據Key獲取一個Peer,若是獲取的peer是本身,那麼認爲失敗,因此會選擇從後端數據服務獲取獲取,並緩存在本地的maincache中,讀到這裏,困惑就消除了,一個key會選取固定的peer,因此若是已經定位到某個peer獲取數據,peer再次調用Get接口時,若是lookupCache失敗,那麼就會調用getLocally嘗試從數據服務獲取,而不會循環從另外的peer去獲取,造成死循環效應;
B、LRU緩存算法爲經常使用算法,通常採用list和hash數據結構結合實現,在此再也不講解;
C、singleflight.go是爲了保證當沒有命中本地緩存是,同一個key在同一時刻只有一個在去remote peer或後端數據服務獲取數據;
總結:
groupcache精小而又強大,直接集成在本身的服務內部,推及使用!