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)