Overview
This article is a translation of Golangでインメモリなキャッシュを実装する.
Golang's in-memory cache library looks good, but it's lightweight and simple enough, so I implemented it myself.
Implementation
Requirements
- Can hold multiple data.
- You can keep time-limited data in memory. It should be destroyed from memory when the deadline is reached.
- Be aware of data lock in consideration of simultaneous reference and update to cache.
Initial design
cf. Github.com - bmf-san/go-snippets/architecture_design/cache/cache.go
I implemented it as if I first came up with it.
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)) }
I thought that sync.Map
, which does not have to worry about lock processing, is convenient, but I rejected it because it does not meet the requirements in terms of data structure and functionality.
Release version
cf. Github.com - bmf-san/go-snippets/architecture_design/cache/cache.go
An implementation version that meets the requirements.
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) }
I wanted to use sync.Map
because it is convenient, but if I store cache data in sync.Map
, it is difficult to check and delete the cache data without specifying the cache key, so map
is used to retain the cache data. I decided to adopt.
Expiration check is done at intervals using ticker
.
In the above, the interval is every second.
In this implementation, the cache can be accessed until the cache expiration time + 1 second has elapsed, so the actual cache expiration date is the time specified by expires plus the interval.
Impressions
It was a good time to get started with concurrency and locking in Golang.
Top comments (0)