Implement an In Memory Cache in Go
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
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
andfalse
- If the key is not expired, then return the
value
andtrue
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)
}