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

Golang キャッシュ

プログラミング

2020-09-29 01:53:39

概要

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での並行処理やロックに入門する良い機会だった。

参考

About the author

Image

bmf san @bmf_san
A web developer in Japan.