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)