Skip to content

Cache System

Implementation of a concurrent-safe cache memory system using Go

Interface Definition

go
package cache

import "time"

// Cache is an interface that defines the methods for an in-memory cache system.
type Cache interface {
	// SetMaxMemory sets the maximum allowed memory size for the cache.
	// size: a string representing the maximum memory size, e.g., "10MB".
	// Returns true if the size was set successfully, false otherwise.
	SetMaxMemory(size string) bool

	// Set adds a key-value pair to the cache with an expiration duration.
	// key: the key under which the value will be stored.
	// value: the value to store in the cache.
	// expire: the duration after which the key-value pair will expire.
	// Returns true if the value was set successfully, false otherwise (e.g., if it exceeds max memory).
	Set(key string, value any, expire time.Duration) bool

	// Get retrieves a value from the cache by its key.
	// key: the key to look up in the cache.
	// Returns the value associated with the key and a boolean indicating whether the key was found.
	Get(key string) (any, bool)

	// Delete removes a key-value pair from the cache.
	// key: the key to remove from the cache.
	// Returns true if the key was deleted successfully, false otherwise.
	Delete(key string) bool

	// Exists checks if a key is present in the cache.
	// key: the key to check for existence in the cache.
	// Returns true if the key exists, false otherwise.
	Exists(key string) bool

	// Flush clears all entries in the cache.
	// Returns true if the cache was cleared successfully, false otherwise.
	Flush() bool

	// KeyNum returns the number of keys currently in the cache.
	// Returns the number of keys as an int64.
	KeyNum() int64
}

Structure Implementation

Init

go
// MemoryCache is a struct that represents an in-memory cache with a fixed maximum size.
type MemoryCache struct {
	maxMemorySize     int64             // Maximum allowed memory size for the cache
	maxMemoryString   string            // Human-readable string representation of the maximum memory size
	currentMemorySize int64             // Current memory usage of the cache
	cacheMap          map[string]*Value // The underlying map to store cache values
	locker            sync.RWMutex      // Read-write mutex for safe concurrent access
	clearTime         time.Duration     // Interval for clearing expired cache entries
}

// Value represents a cached value with an expiration time and size.
type Value struct {
	value  any       // The actual value stored in the cache
	expire time.Time // Expiration time of the cached value
	size   int64     // Size of the cached value in bytes
}

// NewMemoryCache initializes a new MemoryCache with default settings.
func NewMemoryCache() *MemoryCache {
	m := &MemoryCache{
		cacheMap:  make(map[string]*Value, 100), // Initialize the cache map with a capacity of 100
		clearTime: time.Second * 1,              // Set the default clear interval to 1 second
	}
	go m.clearExpireCache(m.clearTime) // Start a goroutine to clear expired cache entries periodically
	return m
}

Basic Operations

For map assignment, there are two cases: deletion and addition. Recalculate the current occupied size for each case. All change operations are based on the following two methods:

go
// deleteKey removes a key from the cache and adjusts the current memory size.
func (m *MemoryCache) deleteKey(key string) bool {
	value, ok := m.cacheMap[key]
	if value != nil && ok {
		delete(m.cacheMap, key)
		m.currentMemorySize -= value.size
		return true
	}
	return false
}

// addKey adds a new key-value pair to the cache and updates the current memory size.
func (m *MemoryCache) addKey(key string, value *Value) {
	m.cacheMap[key] = value
	m.currentMemorySize += value.size
}

SetMaxMemory

Set the maximum memory: The ParseSize function returns the default memory of 100MB if it fails to parse.

go
// SetMaxMemory sets the maximum allowed memory size for the cache.
func (m *MemoryCache) SetMaxMemory(size string) bool {
	var err error
	m.maxMemorySize, m.maxMemoryString, err = ParseSize(size)
	if err != nil {
		return false
	}
	return true
}

Set

go
// Set adds a key-value pair to the cache with an expiration duration.
func (m *MemoryCache) Set(key string, value any, expire time.Duration) bool {
	m.locker.Lock()
	defer m.locker.Unlock()
	v := &Value{
		value:  value,
		expire: time.Now().Add(expire),
		size:   int64(SizeOfVariable(value)),
	}
	if v.size+m.currentMemorySize > m.maxMemorySize {
		log.Printf("超出内存限制, 当前对象使用内存 %d bytes, 最大内存 %d bytes", v.size, m.maxMemorySize)
		return false
	}
	m.deleteKey(key)
	m.addKey(key, v)
	return true
}

Get

Check for expiration: Poll using the given time and release memory.

go
// Get retrieves a value from the cache by its key.
func (m *MemoryCache) Get(key string) (any, bool) {
	m.locker.Lock()
	defer m.locker.Unlock()
	value, ok := m.cacheMap[key]
	if !ok {
		return nil, false
	}
	if value.expire.Before(time.Now()) {
		m.deleteKey(key)
		return nil, false
	}
	return value, true
}

Delete

go
// Delete removes a key-value pair from the cache.
func (m *MemoryCache) Delete(key string) bool {
	m.locker.Lock()
	defer m.locker.Unlock()
	return m.deleteKey(key)
}

Exists

go
// Exists checks if a key is present in the cache.
func (m *MemoryCache) Exists(key string) bool {
	m.locker.Lock()
	defer m.locker.Unlock()
	_, ok := m.cacheMap[key]
	return ok
}

Flush

go
// Flush clears all entries in the cache.
func (m *MemoryCache) Flush() bool {
	m.locker.Lock()
	defer m.locker.Unlock()
	m.cacheMap = make(map[string]*Value, 100)
	m.currentMemorySize = 0
	return true
}

Get Key Nums

go
// KeyNum returns the number of keys currently in the cache.
func (m *MemoryCache) KeyNum() int64 {
	m.locker.Lock()
	defer m.locker.Unlock()
	return int64(len(m.cacheMap))
}

ClearExpireCache

Poll using the given time and release memory.

go
// clearExpireCache periodically removes expired entries from the cache.
func (m *MemoryCache) clearExpireCache(dr time.Duration) {
	ticker := time.NewTicker(dr)
	defer ticker.Stop()
	for {
		select {
		case <-ticker.C:
			for key, value := range m.cacheMap {
				if value.expire.Before(time.Now()) {
					m.locker.Lock()
					m.deleteKey(key)
					m.locker.Unlock()
				}
			}
		
		}
	}
}

Utility Functions

Constants

go
const (
	B = 1 << (iota * 10)
	KB
	MB
	GB
	TB
)

parseSize

This function converts the input size from a string to a byte value.

go
func ParseSize(size string) (int64, string, error) {
	var n, u string
	for _, value := range size {
		if unicode.IsDigit(value) {
			n += string(value)
		} else if unicode.IsLetter(value) {
			u += string(value)
		}
	}
	u = strings.ToUpper(u)
	dn, err := strconv.ParseInt(n, 10, 64)
	if err != nil {
		log.Println("The unit is wrong, and the default setting is 100 megabytes")
		return 100, "100MB", err
	}
	var bs int64 = 0
	switch u {
	case "B":
		bs = dn
	case "KB":
		bs = dn * KB
	case "MB":
		bs = dn * MB
	case "GB":
		bs = dn * GB
	case "TB":
		bs = dn * TB
	default:
		log.Println("The unit is wrong, and the default setting is 100 megabytes")
		return 100, "100MB", err
	}

	return bs, n + u, nil
}

calculateSize

go
// SizeOfVariable Returns the memory size of a variable (in bytes)
func SizeOfVariable(v any) uintptr {
	val := reflect.ValueOf(v)
	return calculateSize(val)
}

// calculateSize Recursively calculate the memory size of a variable
func calculateSize(val reflect.Value) uintptr {
	switch val.Kind() {
	case reflect.String:
		return uintptr(val.Len())
	case reflect.Slice:
		var size uintptr
		for i := 0; i < val.Len(); i++ {
			size += calculateSize(val.Index(i))
		}
		return size
	case reflect.Array:
		var size uintptr
		for i := 0; i < val.Len(); i++ {
			size += calculateSize(val.Index(i))
		}
		return size
	case reflect.Map:
		var size uintptr
		for _, key := range val.MapKeys() {
			size += calculateSize(key)
			size += calculateSize(val.MapIndex(key))
		}
		return size
	case reflect.Ptr, reflect.Interface:
		if val.IsNil() {
			return 0
		}
		return calculateSize(val.Elem())
	case reflect.Struct:
		var size uintptr
		for i := 0; i < val.NumField(); i++ {
			size += calculateSize(val.Field(i))
		}
		return size
	default:
		return uintptr(unsafe.Sizeof(val.Interface()))
	}
}

Meet your first blog