Golangでインメモリなキャッシュを実装する

Golangで期限付きキャッシュを実装し、sync.Mapを使った同時参照安全性確保とメモリ破棄の仕組みを設計・検証。

Read in: en
Golangでインメモリなキャッシュを実装する

概要

Golangのインメモリキャッシュのライブラリは良さそうなものが存在するが、軽量でシンプルなもので十分だったので自前で実装してみた。

実装

要件

初期設計

github.com - bmf-san/go-snippets/architecture_design/cache/cache.goに置いてあるが、転載。

最初に思いついた感じで実装したもの。

package main

import (
	"fmt"
	"log"
	"sync"
	"time"
)

// Cache is a struct for caching.
type Cache struct {
	value   sync.Map
	expires int64
}

// Expired determines if it has expired.
func (c *Cache) Expired(time int64) bool {
	if c.expires == 0 {
		return false
	}
	return time > c.expires
}

// Get gets a value from a cache. Returns an empty string if the value does not exist or has expired.
func (c *Cache) Get(key string) string {
	if c.Expired(time.Now().UnixNano()) {
		log.Printf("%s has expired", key)
		return ""
	}
	v, ok := c.value.Load(key)
	var s string
	if ok {
		s, ok = v.(string)
		if !ok {
			log.Printf("%s does not exists", key)
			return ""
		}
	}
	return s
}

// Put puts a value to a cache. If a key and value exists, overwrite it.
func (c *Cache) Put(key string, value string, expired int64) {
	c.value.Store(key, value)
	c.expires = expired
}

var cache = &Cache{}

func main() {
	fk := "first-key"
	sk := "second-key"

	cache.Put(fk, "first-value", time.Now().Add(2*time.Second).UnixNano())
	s := cache.Get(fk)
	fmt.Println(cache.Get(fk))

	time.Sleep(5 * time.Second)

	// fk should have expired
	s = cache.Get(fk)
	if len(s) == 0 {
		cache.Put(sk, "second-value", time.Now().Add(100*time.Second).UnixNano())
	}
	fmt.Println(cache.Get(sk))
}

ロックの処理を気にしなくてよいsync.Mapが便利で良いなぁと思っていたのだが、データ構造や機能的に要件を満たせていないので却下。

リリース版

github.com - bmf-san/go-snippets/architecture_design/cache/cache.goに置いてあるが、転載。

要件を満たす実装をしたバージョン。

package main

import (
	"fmt"
	"log"
	"net/http"
	"sync"
	"time"
)

// item is the data to be cached.
type item struct {
	value   string
	expires int64
}

// Cache is a struct for caching.
type Cache struct {
	items map[string]*item
	mu    sync.Mutex
}

func New() *Cache {
	c := &Cache{items: make(map[string]*item)}
	go func() {
		t := time.NewTicker(time.Second)
		defer t.Stop()
		for {
			select {
			case <-t.C:
				c.mu.Lock()
				for k, v := range c.items {
					if v.Expired(time.Now().UnixNano()) {
						log.Printf("%v has expires at %d", c.items, time.Now().UnixNano())
						delete(c.items, k)
					}
				}
				c.mu.Unlock()
			}
		}
	}()
	return c
}

// Expired determines if it has expires.
func (i *item) Expired(time int64) bool {
	if i.expires == 0 {
		return true
	}
	return time > i.expires
}

// Get gets a value from a cache.
func (c *Cache) Get(key string) string {
	c.mu.Lock()
	var s string
	if v, ok := c.items[key]; ok {
		s = v.value
	}
	c.mu.Unlock()
	return s
}

// Put puts a value to a cache. If a key and value exists, overwrite it.
func (c *Cache) Put(key string, value string, expires int64) {
	c.mu.Lock()
	if _, ok := c.items[key]; !ok {
		c.items[key] = &item{
			value:   value,
			expires: expires,
		}
	}
	c.mu.Unlock()
}

func main() {
	http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
		fk := "first-key"
		sk := "second-key"

		cache := New()

		cache.Put(fk, "first-value", time.Now().Add(2*time.Second).UnixNano())
		fmt.Println(cache.Get(fk))

		time.Sleep(10 * time.Second)

		if len(cache.Get(fk)) == 0 {
			cache.Put(sk, "second-value", time.Now().Add(100*time.Second).UnixNano())
		}
		fmt.Println(cache.Get(sk))
	})
	http.ListenAndServe(":8080", nil)
}

sync.Mapが便利なので使いたかったのだが、sync.Mapにキャッシュデータを保持させるとキャッシュキー指定なしにキャッシュデータの期限切れチェック・削除が難しいため、キャッシュデータの保持にはmapを採用することにした。

期限切れチェックはtickerを使ってインターバルを置いてチェックするようにしている。 上記では1秒毎のインターバルとなっている。 上記の実装ではキャッシュ期限+1秒経過するまではキャッシュにアクセスすることができてしまうので、実際のキャッシュ期限はexpiresで指定した時間+インターバルになっている。

所感

Golangでの並行処理やロックに入門する良い機会だった。

参考

Tags: Golang キャッシュ
Share: 𝕏 Post Facebook Hatena
✏️ View source / Discuss on GitHub
☕ サポート

このブログを応援していただける方は、以下からサポートをお願いします。いただいたサポートはブログ運営・技術研鑽に活用します。


関連記事