SoFunction
Updated on 2025-03-05

Study on the example of Golang implementing EasyCache cache library

introduction

You will not suffer any losses after learning, and you will not be deceived after learning. You must have basic skills when you enter the factory to make nails. You will definitely feel very happy after reading it. The core code is300Many lines, few codes, but no discounts on functions

What did you learn from this project?

  • Golang basic syntax

  • Cache data structure

  • Lock usage (concurrency security & sharding to reduce lock granularity)

  • LRU (cache elimination algorithm)

  • Key expiration deletion policy (timed deletion)

  • Writing test cases

Code Principle

New function

Responsible for creating*EasyCacheObject, the underlying layer of the object containsThe purpose of shards is to reduce lock conflicts

func New(conf Config) (*EasyCache, error) {

	if !() {
		returnnil, ("shards number must be power of two")
	}

	if  <= 0 {
		 = defaultCap
	}
	// init cache object
	cache := &EasyCache{
		shards:    make([]*cacheShard, ),
		conf:      conf,
		hash:      ,
		shardMask: uint64( - 1), // mask
		close:     make(chanstruct{}),
	}

	var onRemove OnRemoveCallback
	if  != nil {
		onRemove = 
	} else {
		onRemove = 
	}

	// init shard
	for i := 0; i < ; i++ {
		[i] = newCacheShard(conf, i, onRemove, )
	}
	return cache, nil
}

newCacheShard function

Used to initialize actual storagek/vData structure*cacheShard(that is, a single shard). The storage at the bottom layer of shard uses two maps and one list:

  • itemsResponsible for preserving allk/v(End or expired)

  • expireItemsResponsible for keeping expiredk/v, the purpose is to reduce the amount of data scan key`

  • listUsed asLRURecord the least recent usekeyorder. Read this article for LRU code implementationLeetcode LRU problem solution, helps to understand the details of LRU in this project.

func newCacheShard(conf Config, id int, onRemove OnRemoveCallback, close chan struct{}) *cacheShard {
	shard := &cacheShard{
		items:           make(map[string]*),
		expireItems:     make(map[string]*),
		cap:             ,
		list:            (),
		logger:          newLogger(),
		cleanupInterval: defaultInternal,
		cleanupTicker:   (defaultInternal),
		addChan:         make(chanstring),
		isVerbose:       ,
		id:              id,
		onRemove:        onRemove,
		close:           close,
	}
	// goroutine clean expired key
	go ()
	return shard
}

expireCleanup

Responsible for regularly deleting expired keys in this shard: The key to code understanding lies in differentkeyThere will be different expiration times, such as key=a expiration time 3s, key=b expiration time 5s.

  • The timer execution interval should not be too long. For example, 10s, a/b has expired and will not be cleaned yet, it is too timely

  • The execution interval of the timer timer cannot be too short, for example, 1s, the execution frequency is too high, a/b has not expired, and it is idle.

  • The interval will definitely change dynamically. At the beginning, it is a 3s interval. After execution, clean up a. At this time, b still has (5-3) = 2s survival time, so the interval is set to 2s. After the execution is completed, there is no data, and the interval is set to a large value.smallestInternal = defaultInternalIn sleep

Let’s think about another situation here. According to the above explanation, set the interval between 3 seconds at the beginning, and you can clean up a when it expires. If the user sets the key=c expiration time 1s at this time, then if the timer executes in 3s, it becomes too long. So we need to send a signal:, reset the interval

/*
 1. When the timer expires, perform expired cleaning.
 2. When the newly added key has an expiration time, the execution is triggered by addChan
 */
func (cs *cacheShard) expireCleanup() {
	for {
		select {
		case &lt;-:
		case &lt;-: // Trigger immediately		case &lt;-: // stop goroutine
			if  {
				("[shard %d] flush..", )
			}
			() // free
			return
		}
		()
		// Record the minimum interval of the next timer (purpose: the key expires, delete it as soon as possible)		smallestInternal := 0 * 
		now := ()
		()
		for key, ele := range  { // traverse expired keys			item := .(*cacheItem)
			if () == 0 { // No expiration time				("warning wrong data\n")
				continue
			}
			if (()) &gt;= () { // Expired				// del
				delete(, key)
				delete(, key)
				(ele)
				(key, (), Expired)
				if  {
					("[shard %d]: expire del key &lt;%s&gt;  createdOn:%v,  lifeSpan:%d ms \n", , key, (), ().Milliseconds())
				}
			} else {
				d := () - (())
				if smallestInternal == 0 || d &lt; smallestInternal {
					smallestInternal = d
				}
			}
		}
		if smallestInternal == 0 {
			smallestInternal = defaultInternal
		}
		 = smallestInternal
		()
		()
	}
}

set function understanding

The key is that users can repeatedly set the same key:

(key, 0, 5*) // expire 5s
(key, 0, 0*) // expire 0s

The first time it is set to expire 5s, and it is immediately modified to expire 0s, so it is necessary to determine whether the key has existed before.

  • If there is a duplication & there is an expiration time, it needs to be expiredexpireItemsExcluded

  • If there is no new one, just add it directly (prerequisite: there is still remaining capacity)

Basic rules of LRU

  • The latest data is placed in the list Front

  • If the maximum capacity is exceeded, delete the element from the Back of the list

func (cs *cacheShard) set(key string, value interface{}, lifeSpan ) error {

	()
	defer ()

	oldEle, ok := [key]
	if ok { // old item
		oldItem := .(*cacheItem)
		oldLifeSpan := ()

		// modify
		 = newCacheItem(key, value, lifeSpan)
		(oldEle)

		if oldLifeSpan &amp;gt; 0 &amp;amp;&amp;amp; lifeSpan == 0 { // The original one has an expiration time, the new one has no expiration time			delete(, key)
		}

		if oldLifeSpan == 0 &amp;amp;&amp;amp; lifeSpan &amp;gt; 0 { // The original no expiration time, the current expiration time			[key] = oldEle
			if lifeSpan &amp;lt;  {
				gofunc() {
					 &amp;lt;- key
				}()
			}
		}

	} else { // new item

		iflen() &amp;gt;= int() { // lru: No space
			delVal := (())
			item := delVal.(*cacheItem)
			delete(, ())
			if () &amp;gt; 0 {
				delete(, ())
			}
			(key, (), NoSpace)

			if  {
				("[shard %d] no space del key &amp;lt;%s&amp;gt;\n", , ())
			}
		}
		// add
		ele := (newCacheItem(key, value, lifeSpan))
		[key] = ele
		if lifeSpan &amp;gt; 0 {
			[key] = ele
			if lifeSpan &amp;lt;  {
				gofunc() {
					 &amp;lt;- key
				}()
			}
		}
	}

	if  {
		if lifeSpan == 0 {
			("[shard %d]: set persist key &amp;lt;%s&amp;gt;\n", , key)
		} else {
			("[shard %d]: set expired key &amp;lt;%s&amp;gt;", , key)
		}
	}
	returnnil
}

The above is the detailed content of the Golang implementation of EasyCache cache library example. For more information about Golang EasyCache cache library, please follow my other related articles!