Golang in-memory kv store lib - go-cache

·

3 min read

go-cache is a in-memory key value store that used to store cache data in our services.

How to use it to store/retrieve cache data.

import (
    "time"

    "github.com/patrickmn/go-cache"
)

var (
    // create kvstore with default expiration: 5 mins, and clean up interval: 10 mins
    kvstore = cache.New(5*time.Minute, 10*time.Minute)
)

func Set(key string, value interface{}, d time.Duration) {
    kvstore.Set(key, value, d)
}

func Get(key string) (value interface{}, ok bool) {
    return kvstore.Get(key)
}

// 1. first we init kvstore with 5min default expiration and 10min clean up interval
// 2. set a key-value pair by call Set method with a duration.
// 3. get a value by call Get method.

// New Cache
func New(defaultExpiration, cleanupInterval time.Duration) *Cache {
    items := make(map[string]Item)
    return newCacheWithJanitor(defaultExpiration, cleanupInterval, items)
}
..

// First create a items map which used for store the key-values pairs.
// Call newCacheWithJanitor to create the underlying cache strucuture.
func newCacheWithJanitor(de time.Duration, ci time.Duration, m map[string]Item) *Cache {
    c := newCache(de, m)
    C := &Cache{c}
    if ci > 0 {
        runJanitor(c, ci)
        runtime.SetFinalizer(C, stopJanitor)
    }
    return C
}

func newCache(de time.Duration, m map[string]Item) *cache {
    if de == 0 {
        de = -1
    }
    c := &cache{
        defaultExpiration: de,
        items:             m,
    }
    return c
}

func runJanitor(c *cache, ci time.Duration) {
    j := &janitor{
        Interval: ci,
        stop:     make(chan bool),
    }
    c.janitor = j
    go j.Run(c)
}

Create the cache structure with default expiration value and key-value map. Create janitor and start to run janitor in another goroutine.

janitor.Run

  • when the clean up interval triggered, calls DeleteExpired to delete the expired keys from cache.
  • when received stop signal from stop channel the ticker stops and Run exits.
func (j *janitor) Run(c *cache) {
    ticker := time.NewTicker(j.Interval)
    for {
        select {
        case <-ticker.C:
            c.DeleteExpired()
        case <-j.stop:
            ticker.Stop()
            return
        }
    }
}

Set

if d equals default expiration then, we use the default expiration value passed to the New, then calculate the final expiration time of the key.

lock the items key-value map to write the key: k and value: x.

func (c *cache) Set(k string, x interface{}, d time.Duration) {
    var e int64
    if d == DefaultExpiration {
        d = c.defaultExpiration
    }
    if d > 0 {
        e = time.Now().Add(d).UnixNano()
    }
    c.mu.Lock()
    c.items[k] = Item{
        Object:     x,
        Expiration: e,
    }
    c.mu.Unlock()
}

Get

  1. Lock the items map to reading the key.
  2. If not found the key, then just return nil value of the key, and false to indicate that the key not exists.
  3. If the value has expiration and already expired then also return nil, false
  4. Unlock the items map, and returns the value.
func (c *cache) Get(k string) (interface{}, bool) {
    c.mu.RLock()
    item, found := c.items[k]
    if !found {
        c.mu.RUnlock()
        return nil, false
    }
    if item.Expiration > 0 {
        if time.Now().UnixNano() > item.Expiration {
            c.mu.RUnlock()
            return nil, false
        }
    }
    c.mu.RUnlock()
    return item.Object, true
}

thoughts of go-cache

  • go-cache can not let us to limit the number of keys in the cache.
  • go-cache also can not limit the memory usage that it consumes.