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