Skip to content

Latest commit

 

History

24 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

expirable-cache

Build Status Coverage Status godoc

Package cache implements expirable cache.

  • Support LRC, LRU and TTL-based eviction.
  • Package is thread-safe and doesn't spawn any goroutines.
  • On every Set() call, cache deletes single oldest entry in case it's expired.
  • In case MaxSize is set, cache deletes the oldest entry disregarding its expiration date to maintain the size, either using LRC or LRU eviction.
  • In case of default TTL (10 years) and default MaxSize (0, unlimited) the cache will be truly unlimited and will never delete entries from itself automatically.

Latest version is v3, it requires Go 1.23 or later; v2 and v1 require Go 1.20 or later. Only v3 is actively developed.

Important: only reliable way of not having expired entries stuck in a cache is to run cache.DeleteExpired periodically using time.Ticker, advisable period is 1/2 of TTL.

This cache is heavily inspired by hashicorp/golang-lru simplelru implementation. v3 implements simplelru.LRUCache interface, so if you use a subset of functions, so you can switch from github.com/hashicorp/golang-lru/v2/simplelru or github.com/hashicorp/golang-lru/v2/expirable without any changes in your code except for cache creation. Key differences are:

  • Support LRC (Least Recently Created) in addition to LRU and TTL-based eviction. In LRC mode the eviction order is defined by the time of the last write: setting an existing key moves it to the front, while reads leave the order untouched, which is what LRU mode adds
  • Supports per-key TTL setting
  • Doesn't spawn any goroutines, whereas hashicorp/golang-lru/v2/expirable spawns goroutine which is never killed (as of now)
  • Provides stats about hits and misses, added and evicted entries

Usage example

package main

import (
	"fmt"
	"time"

	cache "github.com/go-pkgz/expirable-cache/v3"
)

func main() {
	// make cache with short TTL and 3 max keys
	c := cache.NewCache[string, string]().WithMaxKeys(3).WithTTL(time.Millisecond * 100)

	// set value under key1.
	// with 0 ttl (last parameter) will use cache-wide setting instead (100ms).
	c.Set("key1", "val1", 0)

	// get value under key1
	r, ok := c.Get("key1")
	fmt.Printf("value before expiration is found: %v, value: %v\n", ok, r)

	time.Sleep(time.Millisecond * 110)

	// get value under key1 after key expiration.
	// expired entry is not removed until something touches it, so the stored value
	// is still returned, with ok set to false
	r, ok = c.Get("key1")
	fmt.Printf("value after expiration is found: %v, value: %v\n", ok, r)

	// set value under key2, would evict key1 because it is already expired.
	// ttl (last parameter) overrides cache-wide ttl.
	c.Set("key2", "val2", time.Minute*5)

	fmt.Printf("%+v\n", c)

	// Output:
	// value before expiration is found: true, value: val1
	// value after expiration is found: false, value: val1
	// Size: 1, Stats: {Hits:1 Misses:1 Added:2 Evicted:1} (50.0%)
}

Performance Comparison

For detailed benchmarks comparing different versions and cache implementations, see the benchmarks directory.

Based on all the benchmarks across four different caching libraries:

  1. go-pkgz/expirable-cache remains the best overall option:

    • Excellent performance across all operations
    • Lowest memory usage and allocations
    • Type safety through generics
    • Clean API with method chaining
    • Simple implementation
  2. dgraph-io/ristretto is a strong contender for specific use cases:

    • Great performance for read-heavy workloads
    • Sophisticated memory management for very large caches
    • Built-in metrics and statistics
    • Designed for high-concurrency environments
    • Writes are asynchronous, so they are cheap to issue but expensive to complete, see the methodology note in the benchmarks
  3. patrickmn/go-cache is still the fastest for reads but lacks modern features, and leaks goroutines

  4. jellydator/ttlcache lags behind in speed, two to three times slower than the leaders, although its memory usage is now on par with them.

Version Improvements

v2 and v3 use Go generics and achieve significant performance improvements over v1:

  • v2 is approximately 28-42% faster than v1 for basic operations
  • v3 maintains the performance gains of v2 while being compatible with the Hashicorp simplelru interface
  • Recent optimizations have improved performance across all versions

Performance Comparison

Operation v1 v2 v3 Improvement v1→v3
Random LRU (no expire) 188.3 ns/op 127.5 ns/op 132.3 ns/op ~30% faster
Frequency LRU (no expire) 180.3 ns/op 127.4 ns/op 128.1 ns/op ~29% faster
Random LRU (with expire) 191.9 ns/op 129.7 ns/op 130.7 ns/op ~32% faster
Frequency LRU (with expire) 181.3 ns/op 126.7 ns/op 131.2 ns/op ~28% faster

Cross-Library Comparison

Recent benchmarks comparing expirable-cache with other popular Go caching libraries:

Operation go-pkgz/expirable-cache patrickmn/go-cache jellydator/ttlcache dgraph-io/ristretto
Set 65.59 ns/op 79.87 ns/op 256.7 ns/op 778.9 ns/op
Get 80.98 ns/op 67.82 ns/op 191.0 ns/op 78.75 ns/op
Set+Get 65.93 ns/op 69.63 ns/op 198.9 ns/op 528.7 ns/op
Real-world scenario 82.28 ns/op 70.87 ns/op 189.5 ns/op 105.6 ns/op
Memory allocations Lowest Low Low Highest
v1 benchmark results
~/expirable-cache ❯ go test -bench=.
goos: darwin
goarch: arm64
pkg: github.com/go-pkgz/expirable-cache
BenchmarkLRU_Rand_NoExpire-8     	 4494738	       272.4 ns/op
--- BENCH: BenchmarkLRU_Rand_NoExpire-8
    cache_test.go:46: hit: 0 miss: 1 ratio: 0.000000
    cache_test.go:46: hit: 1 miss: 99 ratio: 0.010000
    cache_test.go:46: hit: 1352 miss: 8648 ratio: 0.135200
    cache_test.go:46: hit: 248678 miss: 751322 ratio: 0.248678
    cache_test.go:46: hit: 1121791 miss: 3372947 ratio: 0.249579
BenchmarkLRU_Freq_NoExpire-8     	 4612648	       261.6 ns/op
--- BENCH: BenchmarkLRU_Freq_NoExpire-8
    cache_test.go:74: hit: 1 miss: 0 ratio: 1.000000
    cache_test.go:74: hit: 100 miss: 0 ratio: 1.000000
    cache_test.go:74: hit: 9825 miss: 175 ratio: 0.982500
    cache_test.go:74: hit: 312345 miss: 687655 ratio: 0.312345
    cache_test.go:74: hit: 1414620 miss: 3198028 ratio: 0.306683
BenchmarkLRU_Rand_WithExpire-8   	 4109704	       286.5 ns/op
--- BENCH: BenchmarkLRU_Rand_WithExpire-8
    cache_test.go:99: hit: 0 miss: 1 ratio: 0.000000
    cache_test.go:99: hit: 0 miss: 100 ratio: 0.000000
    cache_test.go:99: hit: 1304 miss: 8696 ratio: 0.130400
    cache_test.go:99: hit: 248310 miss: 751690 ratio: 0.248310
    cache_test.go:99: hit: 1027317 miss: 3082387 ratio: 0.249973
BenchmarkLRU_Freq_WithExpire-8   	 4341217	       279.6 ns/op
--- BENCH: BenchmarkLRU_Freq_WithExpire-8
    cache_test.go:127: hit: 1 miss: 0 ratio: 1.000000
    cache_test.go:127: hit: 100 miss: 0 ratio: 1.000000
    cache_test.go:127: hit: 9868 miss: 132 ratio: 0.986800
    cache_test.go:127: hit: 38221 miss: 961779 ratio: 0.038221
    cache_test.go:127: hit: 37296 miss: 4303921 ratio: 0.008591
PASS
ok  	github.com/go-pkgz/expirable-cache	18.307s
v3 benchmark results
~/Desktop/expirable-cache/v3 master !2 ❯ go test -bench=.
goos: darwin
goarch: arm64
pkg: github.com/go-pkgz/expirable-cache/v3
BenchmarkLRU_Rand_NoExpire-8     	 7556680	       158.1 ns/op
--- BENCH: BenchmarkLRU_Rand_NoExpire-8
    cache_test.go:47: hit: 0 miss: 1 ratio: 0.000000
    cache_test.go:47: hit: 0 miss: 100 ratio: 0.000000
    cache_test.go:47: hit: 1409 miss: 8591 ratio: 0.140900
    cache_test.go:47: hit: 249063 miss: 750937 ratio: 0.249063
    cache_test.go:47: hit: 1887563 miss: 5669117 ratio: 0.249787
BenchmarkLRU_Freq_NoExpire-8     	 7876738	       150.9 ns/op
--- BENCH: BenchmarkLRU_Freq_NoExpire-8
    cache_test.go:75: hit: 1 miss: 0 ratio: 1.000000
    cache_test.go:75: hit: 100 miss: 0 ratio: 1.000000
    cache_test.go:75: hit: 9850 miss: 150 ratio: 0.985000
    cache_test.go:75: hit: 310888 miss: 689112 ratio: 0.310888
    cache_test.go:75: hit: 2413312 miss: 5463426 ratio: 0.306385
BenchmarkLRU_Rand_WithExpire-8   	 6822362	       175.3 ns/op
--- BENCH: BenchmarkLRU_Rand_WithExpire-8
    cache_test.go:100: hit: 0 miss: 1 ratio: 0.000000
    cache_test.go:100: hit: 0 miss: 100 ratio: 0.000000
    cache_test.go:100: hit: 1326 miss: 8674 ratio: 0.132600
    cache_test.go:100: hit: 248508 miss: 751492 ratio: 0.248508
    cache_test.go:100: hit: 1704172 miss: 5118190 ratio: 0.249792
BenchmarkLRU_Freq_WithExpire-8   	 7098261	       168.1 ns/op
--- BENCH: BenchmarkLRU_Freq_WithExpire-8
    cache_test.go:128: hit: 1 miss: 0 ratio: 1.000000
    cache_test.go:128: hit: 100 miss: 0 ratio: 1.000000
    cache_test.go:128: hit: 9842 miss: 158 ratio: 0.984200
    cache_test.go:128: hit: 90167 miss: 909833 ratio: 0.090167
    cache_test.go:128: hit: 90421 miss: 7007840 ratio: 0.012738
PASS
ok  	github.com/go-pkgz/expirable-cache/v3	24.315s
Details

For detailed benchmarks and methodology, see the benchmarks directory.

Releases

Sponsor this project

Packages

Used by

Contributors

Languages