Implement an In Memory Cache in Go

·

2 min read

what is in memory cache

In memory cache is an way that the application temp store the data to computer main memory, for quickly retrieval.

So basically, we will implement an in memory cache that let the client can get and set values by key, and also can delete the keys when they expired.

The Architecture

inmemcache_v1.png

the cache struct.

  • bucketCount: how many bucket we will use for our cache.
  • defaultKeyCount: what's the init cap for each bucket.
  • defaultCleanInterval: the interval used to clean up the expired keys.
const (
    bucketCount          = 256
    defaultKeyCount      = 8
    defaultCleanInterval = 10 * time.Second
)

type Value struct {
    Val      interface{}
    ExpireAt *time.Time // zero time value means, this key never expire
}

type kvstore struct {
    mu sync.Mutex
    m  map[string]*Value
}
type cache struct {
    data          [bucketCount]*kvstore
    cleanInterval time.Duration
}

Init Cache

func NewCache(initKeyCount uint32, cleanInterval time.Duration) *cache {
    if initKeyCount <= 0 {
        initKeyCount = defaultKeyCount
    }
    c := &cache{
        cleanInterval: cleanInterval,
    }

    if c.cleanInterval <= 0 {
        c.cleanInterval = defaultCleanInterval
    }

    for i := 0; i < bucketCount; i++ {
        c.data[i] = &kvstore{m: make(map[string]*Value, initKeyCount)}
    }

    go c.checkAndRemoveExpireKeys()
    return c
}

Get

  • First calculate the bucket index by the hash of the key.
  • call bucket.Get to query the value of the key.
  • Check if the key is never expire, then just return it.
  • If the key is expired, then return nil and false
  • If the key is not expired, then return the value and true
func (c *cache) Get(key string) (interface{}, bool) {
    i := xxhash.Sum64String(key) % bucketCount
    bucket := c.data[i]
    v, ok := bucket.Get(key)
    if !ok {
        return nil, false
    }

    if v.ExpireAt.IsZero() {
        return v.Val, true
    }

    now := time.Now()
    // this value expired
    if v.ExpireAt.Unix()-now.Unix() <= 0 {
        return nil, false
    }

    return v.Val, true
}

Set

  • Find the bucket by the hash of the key.
  • Set the expireAt is empty time.
  • Call bucket.Set(key, val) to set the value.
func (c *cache) Set(key string, value interface{}) {
    c.setKeyWithExpire(key, value, &time.Time{})
}

func (c *cache) setKeyWithExpire(key string, value interface{}, expireAt *time.Time) {
    i := xxhash.Sum64String(key) % bucketCount
    bucket := c.data[i]

    v := &Value{Val: value, ExpireAt: expireAt}
    bucket.Set(key, v)
}

SetWithTTL

Similar with set operation, but with an actually expiredAt value.

func (c *cache) SetWithTTL(key string, value interface{}, ttl time.Duration) {
    expireAt := time.Now().Add(ttl)
    c.setKeyWithExpire(key, value, &expireAt)
}