├── go.work ├── assets └── logo.png ├── .gitignore ├── Makefile ├── .editorconfig ├── go.work.sum ├── go.mod ├── .github └── workflows │ └── go.yml ├── types.go ├── .golangci.yaml ├── internal ├── utils │ ├── helper.go │ ├── hash.go │ ├── random.go │ └── helper_test.go └── containers │ ├── map.go │ └── map_test.go ├── LICENSE ├── benchmark ├── go.mod ├── go.sum └── benchmark_test.go ├── heap_test.go ├── go.sum ├── options_test.go ├── heap.go ├── deque_test.go ├── options.go ├── README_CN.md ├── deque.go ├── README.md ├── cache.go └── cache_test.go /go.work: -------------------------------------------------------------------------------- 1 | go 1.19 2 | 3 | use ( 4 | . 5 | ./benchmark 6 | ) 7 | -------------------------------------------------------------------------------- /assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lxzan/memorycache/HEAD/assets/logo.png -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | .vscode/ 3 | vendor/ 4 | cmd/ 5 | bin/ 6 | examples/ 7 | .DS_Store 8 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | test: 2 | go test -count 1 -timeout 30s -run ^Test ./... 3 | go test -count 1 -timeout 30s -run ^Test github.com/lxzan/memorycache/benchmark 4 | 5 | bench: 6 | go test -benchmem -run=^$$ -bench . github.com/lxzan/memorycache/benchmark 7 | 8 | cover: 9 | go test -coverprofile=./bin/cover.out --cover ./... -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig is awesome: https://EditorConfig.org 2 | 3 | # top-most EditorConfig file 4 | root = true 5 | 6 | [*] 7 | end_of_line = lf 8 | insert_final_newline = true 9 | 10 | [*.go] 11 | indent_style = tab 12 | indent_size = 4 13 | 14 | [Makefile] 15 | indent_style = tab 16 | 17 | [*.{yml,yaml}] 18 | indent_style = space 19 | indent_size = 2 -------------------------------------------------------------------------------- /go.work.sum: -------------------------------------------------------------------------------- 1 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= 2 | github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= 3 | github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= 4 | github.com/zeebo/assert v1.3.0/go.mod h1:Pq9JiuJQpG8JLJdtkwrJESF0Foym2/D9XMU5ciN/wJ0= 5 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 6 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/lxzan/memorycache 2 | 3 | go 1.19 4 | 5 | require ( 6 | github.com/dolthub/maphash v0.1.0 7 | github.com/dolthub/swiss v0.2.1 8 | github.com/lxzan/dao v1.1.6 9 | github.com/stretchr/testify v1.8.4 10 | ) 11 | 12 | require ( 13 | github.com/davecgh/go-spew v1.1.1 // indirect 14 | github.com/kr/pretty v0.1.0 // indirect 15 | github.com/pmezard/go-difflib v1.0.0 // indirect 16 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 // indirect 17 | gopkg.in/yaml.v3 v3.0.1 // indirect 18 | ) 19 | -------------------------------------------------------------------------------- /.github/workflows/go.yml: -------------------------------------------------------------------------------- 1 | # This workflow will build a golang project 2 | # For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-go 3 | 4 | name: Go Test 5 | 6 | on: 7 | push: 8 | branches: [ "main" ] 9 | pull_request: 10 | branches: [ "main" ] 11 | 12 | jobs: 13 | 14 | build: 15 | runs-on: ubuntu-latest 16 | steps: 17 | - uses: actions/checkout@v3 18 | 19 | - name: Set up Go 20 | uses: actions/setup-go@v3 21 | with: 22 | go-version: 1.19 23 | 24 | - name: Test 25 | run: make test 26 | 27 | - name: Bench 28 | run: make bench 29 | -------------------------------------------------------------------------------- /types.go: -------------------------------------------------------------------------------- 1 | package memorycache 2 | 3 | // Reason 回调函数触发原因 4 | type Reason uint8 5 | 6 | const ( 7 | ReasonExpired = Reason(0) // 过期 8 | ReasonEvicted = Reason(1) // 被驱逐 9 | ReasonDeleted = Reason(2) // 被删除 10 | ) 11 | 12 | type CallbackFunc[T any] func(element T, reason Reason) 13 | 14 | type Element[K comparable, V any] struct { 15 | // 地址 16 | prev, addr, next pointer 17 | 18 | // 索引 19 | index int 20 | 21 | // 哈希 22 | hashcode uint64 23 | 24 | // 回调函数 25 | cb CallbackFunc[*Element[K, V]] 26 | 27 | // 键 28 | Key K 29 | 30 | // 值 31 | Value V 32 | 33 | // 过期时间, 毫秒 34 | ExpireAt int64 35 | } 36 | 37 | func (c *Element[K, V]) expired(now int64) bool { 38 | return now > c.ExpireAt 39 | } 40 | -------------------------------------------------------------------------------- /.golangci.yaml: -------------------------------------------------------------------------------- 1 | linters: 2 | enable-all: true 3 | # Disable specific linter 4 | # https://golangci-lint.run/usage/linters/#disabled-by-default 5 | disable: 6 | - testpackage 7 | - nosnakecase 8 | - nlreturn 9 | - gomnd 10 | - forcetypeassert 11 | - wsl 12 | - whitespace 13 | - prealloc 14 | - ineffassign 15 | - lll 16 | - funlen 17 | - scopelint 18 | - dupl 19 | - gofumpt 20 | - gofmt 21 | - godot 22 | - gci 23 | - goimports 24 | - gocognit 25 | - ifshort 26 | - gochecknoinits 27 | - predeclared 28 | - containedctx 29 | # Enable presets. 30 | # https://golangci-lint.run/usage/linters 31 | # Run only fast linters from enabled linters set (first run won't be fast) 32 | # Default: false 33 | fast: true 34 | -------------------------------------------------------------------------------- /internal/utils/helper.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | type Integer interface { 4 | int | int64 | int32 | uint | uint64 | uint32 5 | } 6 | 7 | func ToBinaryNumber[T Integer](n T) T { 8 | var x T = 1 9 | for x < n { 10 | x *= 2 11 | } 12 | return x 13 | } 14 | 15 | func Uniq[T comparable](arr []T) []T { 16 | var m = make(map[T]struct{}, len(arr)) 17 | var list = make([]T, 0, len(arr)) 18 | for _, item := range arr { 19 | m[item] = struct{}{} 20 | } 21 | for k, _ := range m { 22 | list = append(list, k) 23 | } 24 | return list 25 | } 26 | 27 | func SelectValue[T any](ok bool, a, b T) T { 28 | if ok { 29 | return a 30 | } 31 | return b 32 | } 33 | 34 | func IsSameSlice[T comparable](a, b []T) bool { 35 | if len(a) != len(b) { 36 | return false 37 | } 38 | for i, v := range a { 39 | if v != b[i] { 40 | return false 41 | } 42 | } 43 | return true 44 | } 45 | -------------------------------------------------------------------------------- /internal/containers/map.go: -------------------------------------------------------------------------------- 1 | package containers 2 | 3 | import "github.com/dolthub/swiss" 4 | 5 | type Map[K comparable, V any] interface { 6 | Count() int 7 | Get(K) (V, bool) 8 | Put(k K, v V) 9 | Delete(key K) bool 10 | Iter(f func(K, V) bool) 11 | } 12 | 13 | type HashMap[K comparable, V any] map[K]V 14 | 15 | func (c HashMap[K, V]) Count() int { 16 | return len(c) 17 | } 18 | 19 | func (c HashMap[K, V]) Put(k K, v V) { 20 | c[k] = v 21 | } 22 | 23 | func (c HashMap[K, V]) Get(k K) (V, bool) { 24 | v, ok := c[k] 25 | return v, ok 26 | } 27 | 28 | func (c HashMap[K, V]) Delete(k K) bool { 29 | delete(c, k) 30 | return true 31 | } 32 | 33 | func (c HashMap[K, V]) Iter(f func(K, V) bool) { 34 | for k, v := range c { 35 | if !f(k, v) { 36 | return 37 | } 38 | } 39 | } 40 | 41 | func NewMap[K comparable, V any](capacity int, swissTable bool) Map[K, V] { 42 | if swissTable { 43 | return swiss.NewMap[K, V](uint32(capacity)) 44 | } 45 | return make(HashMap[K, V], capacity) 46 | } 47 | -------------------------------------------------------------------------------- /internal/utils/hash.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | const ( 4 | prime64 = 1099511628211 5 | offset64 = 14695981039346656037 6 | 7 | prime32 = 16777619 8 | offset32 = 2166136261 9 | ) 10 | 11 | func Fnv64(s string) uint64 { 12 | var hash uint64 = offset64 13 | for _, c := range s { 14 | hash *= prime64 15 | hash ^= uint64(c) 16 | } 17 | return hash 18 | } 19 | 20 | func Fnv32(s string) uint32 { 21 | var hash uint32 = offset32 22 | for _, c := range s { 23 | hash *= prime32 24 | hash ^= uint32(c) 25 | } 26 | return hash 27 | } 28 | 29 | type Hasher[K comparable] interface { 30 | Hash(K) uint64 31 | } 32 | 33 | // Fnv32Hasher 用于测试哈希冲突的数据集 34 | // [O4XOUsgCQqkVCvLQ wYLAGPVADrDTi7VT] 35 | // [e7p5kjn8U6SDvI5B wbMm2kjYjwkBeqzc] 36 | // [SfZaE3dDLWcYxT6G x12qmBRf3TVb0oZA] 37 | // [d3n5BOTvkYif9o5T x4vw8ToKcrwQ8aYc] 38 | // [eR8LklziA5C9XsSl xXcUr3WtBNJQomaK] 39 | // [E9rj2ySsqr7DZUuU xkWJQCMpAWrIczTY] 40 | // [kAsRa2rcRAXvEgiB xtLwyg9fYRclMpsW] 41 | // [xOWb2UMFqAEML9d5 xxVWZzckpn6LMhW7] 42 | type Fnv32Hasher struct{} 43 | 44 | func (c *Fnv32Hasher) Hash(key string) uint64 { 45 | return uint64(Fnv32(key)) 46 | } 47 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 CodeBeast 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /benchmark/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/lxzan/memorycache/benchmark 2 | 3 | go 1.19 4 | 5 | require ( 6 | github.com/Yiling-J/theine-go v0.3.1 7 | github.com/dgraph-io/ristretto v0.1.1 8 | github.com/hashicorp/golang-lru/v2 v2.0.7 9 | github.com/lxzan/memorycache v1.0.0 10 | github.com/stretchr/testify v1.8.4 11 | ) 12 | 13 | require ( 14 | github.com/cespare/xxhash/v2 v2.1.2 // indirect 15 | github.com/davecgh/go-spew v1.1.1 // indirect 16 | github.com/dolthub/maphash v0.1.0 // indirect 17 | github.com/dolthub/swiss v0.2.1 // indirect 18 | github.com/dustin/go-humanize v1.0.0 // indirect 19 | github.com/gammazero/deque v0.2.1 // indirect 20 | github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b // indirect 21 | github.com/klauspost/cpuid/v2 v2.2.6 // indirect 22 | github.com/kr/text v0.2.0 // indirect 23 | github.com/lxzan/dao v1.1.2 // indirect 24 | github.com/ncw/directio v1.0.5 // indirect 25 | github.com/pkg/errors v0.9.1 // indirect 26 | github.com/pmezard/go-difflib v1.0.0 // indirect 27 | github.com/zeebo/xxh3 v1.0.2 // indirect 28 | golang.org/x/sys v0.15.0 // indirect 29 | gopkg.in/yaml.v3 v3.0.1 // indirect 30 | ) 31 | 32 | replace github.com/lxzan/memorycache v1.0.0 => ../ 33 | -------------------------------------------------------------------------------- /internal/utils/random.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "math/rand" 5 | "sync" 6 | "time" 7 | ) 8 | 9 | type RandomString struct { 10 | mu sync.Mutex 11 | r *rand.Rand 12 | layout string 13 | } 14 | 15 | var ( 16 | AlphabetNumeric = &RandomString{ 17 | layout: "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ", 18 | r: rand.New(rand.NewSource(time.Now().UnixNano())), 19 | mu: sync.Mutex{}, 20 | } 21 | 22 | Numeric = &RandomString{ 23 | layout: "0123456789", 24 | r: rand.New(rand.NewSource(time.Now().UnixNano())), 25 | mu: sync.Mutex{}, 26 | } 27 | ) 28 | 29 | func (c *RandomString) Generate(n int) []byte { 30 | c.mu.Lock() 31 | var b = make([]byte, n, n) 32 | var length = len(c.layout) 33 | for i := 0; i < n; i++ { 34 | var idx = c.r.Intn(length) 35 | b[i] = c.layout[idx] 36 | } 37 | c.mu.Unlock() 38 | return b 39 | } 40 | 41 | func (c *RandomString) Intn(n int) int { 42 | c.mu.Lock() 43 | x := c.r.Intn(n) 44 | c.mu.Unlock() 45 | return x 46 | } 47 | 48 | func (c *RandomString) Uint32() uint32 { 49 | c.mu.Lock() 50 | x := c.r.Uint32() 51 | c.mu.Unlock() 52 | return x 53 | } 54 | 55 | func (c *RandomString) Uint64() uint64 { 56 | c.mu.Lock() 57 | x := c.r.Uint64() 58 | c.mu.Unlock() 59 | return x 60 | } 61 | -------------------------------------------------------------------------------- /internal/containers/map_test.go: -------------------------------------------------------------------------------- 1 | package containers 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/dolthub/swiss" 7 | "github.com/stretchr/testify/assert" 8 | ) 9 | 10 | func TestHashMap(t *testing.T) { 11 | var m = HashMap[string, any]{} 12 | assert.Equal(t, m.Count(), 0) 13 | m.Put("a", 1) 14 | assert.Equal(t, m.Count(), 1) 15 | 16 | { 17 | v, ok := m.Get("a") 18 | assert.True(t, ok) 19 | assert.Equal(t, v, 1) 20 | } 21 | 22 | { 23 | m.Delete("a") 24 | _, ok := m.Get("a") 25 | assert.False(t, ok) 26 | } 27 | } 28 | 29 | func TestHashMap_Iter(t *testing.T) { 30 | var m = HashMap[string, any]{} 31 | m.Put("a", 1) 32 | m.Put("b", 2) 33 | m.Put("c", 3) 34 | 35 | t.Run("", func(t *testing.T) { 36 | var keys []string 37 | m.Iter(func(s string, a any) bool { 38 | keys = append(keys, s) 39 | return true 40 | }) 41 | assert.ElementsMatch(t, keys, []string{"a", "b", "c"}) 42 | }) 43 | 44 | t.Run("", func(t *testing.T) { 45 | var keys []string 46 | m.Iter(func(s string, a any) bool { 47 | keys = append(keys, s) 48 | return len(keys) < 2 49 | }) 50 | assert.Equal(t, len(keys), 2) 51 | }) 52 | } 53 | 54 | func TestNewMap(t *testing.T) { 55 | t.Run("", func(t *testing.T) { 56 | m := NewMap[string, any](1, true) 57 | _, ok := m.(*swiss.Map[string, any]) 58 | assert.True(t, ok) 59 | }) 60 | 61 | t.Run("", func(t *testing.T) { 62 | m := NewMap[string, any](1, false) 63 | _, ok := m.(HashMap[string, any]) 64 | assert.True(t, ok) 65 | }) 66 | } 67 | -------------------------------------------------------------------------------- /heap_test.go: -------------------------------------------------------------------------------- 1 | package memorycache 2 | 3 | import ( 4 | "math/rand" 5 | "sort" 6 | "testing" 7 | 8 | "github.com/lxzan/memorycache/internal/utils" 9 | "github.com/stretchr/testify/assert" 10 | ) 11 | 12 | func isSorted[K comparable, V any](h *heap[K, V]) bool { 13 | var list0 []int 14 | var list1 []int 15 | for h.Len() > 0 { 16 | addr := h.Pop() 17 | v := h.List.Get(addr) 18 | list0 = append(list0, int(v.ExpireAt)) 19 | list1 = append(list1, int(v.ExpireAt)) 20 | } 21 | sort.Ints(list1) 22 | return utils.IsSameSlice(list0, list1) 23 | } 24 | 25 | func TestHeap_Sort(t *testing.T) { 26 | var as = assert.New(t) 27 | var q = newDeque[string, int](0) 28 | var h = newHeap[string, int](q, 0) 29 | for i := 0; i < 1000; i++ { 30 | num := rand.Int63n(1000) 31 | ele := q.PushBack() 32 | ele.ExpireAt = num 33 | h.Push(ele) 34 | } 35 | 36 | as.LessOrEqual(h.Front().ExpireAt, h.List.Get(h.Data[1]).ExpireAt) 37 | as.LessOrEqual(h.Front().ExpireAt, h.List.Get(h.Data[2]).ExpireAt) 38 | as.LessOrEqual(h.Front().ExpireAt, h.List.Get(h.Data[3]).ExpireAt) 39 | as.LessOrEqual(h.Front().ExpireAt, h.List.Get(h.Data[4]).ExpireAt) 40 | as.True(isSorted(h)) 41 | as.Zero(h.Pop()) 42 | } 43 | 44 | func TestHeap_Delete(t *testing.T) { 45 | var as = assert.New(t) 46 | var q = newDeque[string, int](0) 47 | var h = newHeap[string, int](q, 0) 48 | var push = func(exp int64) { 49 | ele := q.PushBack() 50 | ele.ExpireAt = exp 51 | h.Push(ele) 52 | } 53 | push(1) 54 | push(2) 55 | push(3) 56 | push(4) 57 | push(5) 58 | push(6) 59 | push(7) 60 | push(8) 61 | push(9) 62 | push(10) 63 | h.Delete(3) 64 | h.Delete(5) 65 | 66 | var list []int64 67 | for _, item := range h.Data { 68 | ele := h.List.Get(item) 69 | list = append(list, ele.ExpireAt) 70 | } 71 | as.ElementsMatch(list, []int64{1, 2, 3, 8, 5, 9, 7, 10}) 72 | } 73 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 2 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 3 | github.com/dolthub/maphash v0.1.0 h1:bsQ7JsF4FkkWyrP3oCnFJgrCUAFbFf3kOl4L/QxPDyQ= 4 | github.com/dolthub/maphash v0.1.0/go.mod h1:gkg4Ch4CdCDu5h6PMriVLawB7koZ+5ijb9puGMV50a4= 5 | github.com/dolthub/swiss v0.2.1 h1:gs2osYs5SJkAaH5/ggVJqXQxRXtWshF6uE0lgR/Y3Gw= 6 | github.com/dolthub/swiss v0.2.1/go.mod h1:8AhKZZ1HK7g18j7v7k6c5cYIGEZJcPn0ARsai8cUrh0= 7 | github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= 8 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= 9 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 10 | github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= 11 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 12 | github.com/lxzan/dao v1.1.6 h1:tYhLpuooCZA2knmORoGE3pdGwckQjSEetNrq/mZCRgA= 13 | github.com/lxzan/dao v1.1.6/go.mod h1:5ChTIo7RSZ4upqRo16eicJ3XdJWhGwgMIsyuGLMUofM= 14 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 15 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 16 | github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= 17 | github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= 18 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 19 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= 20 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 21 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 22 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 23 | -------------------------------------------------------------------------------- /internal/utils/helper_test.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "hash/fnv" 5 | "hash/maphash" 6 | "testing" 7 | 8 | "github.com/stretchr/testify/assert" 9 | ) 10 | 11 | func BenchmarkHash_Fnv64(b *testing.B) { 12 | b.Run("16", func(b *testing.B) { 13 | key := string(AlphabetNumeric.Generate(16)) 14 | for i := 0; i < b.N; i++ { 15 | Fnv64(key) 16 | } 17 | }) 18 | 19 | b.Run("32", func(b *testing.B) { 20 | key := string(AlphabetNumeric.Generate(32)) 21 | for i := 0; i < b.N; i++ { 22 | Fnv64(key) 23 | } 24 | }) 25 | } 26 | 27 | func BenchmarkHash_MapHash(b *testing.B) { 28 | seed := maphash.MakeSeed() 29 | 30 | b.Run("16", func(b *testing.B) { 31 | key := string(AlphabetNumeric.Generate(16)) 32 | for i := 0; i < b.N; i++ { 33 | maphash.String(seed, key) 34 | } 35 | }) 36 | 37 | b.Run("32", func(b *testing.B) { 38 | key := string(AlphabetNumeric.Generate(32)) 39 | for i := 0; i < b.N; i++ { 40 | maphash.String(seed, key) 41 | } 42 | }) 43 | } 44 | 45 | func TestNewFnv64(t *testing.T) { 46 | for i := 0; i < 10; i++ { 47 | key := AlphabetNumeric.Generate(16) 48 | h := fnv.New64() 49 | h.Write(key) 50 | assert.Equal(t, h.Sum64(), Fnv64(string(key))) 51 | } 52 | } 53 | 54 | func TestToBinaryNumber(t *testing.T) { 55 | assert.Equal(t, 8, ToBinaryNumber(7)) 56 | assert.Equal(t, 1, ToBinaryNumber(0)) 57 | assert.Equal(t, 128, ToBinaryNumber(120)) 58 | assert.Equal(t, 1024, ToBinaryNumber(1024)) 59 | } 60 | 61 | func TestUniq(t *testing.T) { 62 | assert.ElementsMatch(t, Uniq([]int{1, 3, 5, 7, 7, 9}), []int{1, 3, 5, 7, 9}) 63 | assert.ElementsMatch(t, Uniq([]string{"ming", "ming", "shi"}), []string{"ming", "shi"}) 64 | } 65 | 66 | func TestRandomString(t *testing.T) { 67 | assert.Less(t, Numeric.Intn(10), 10) 68 | Numeric.Uint32() 69 | Numeric.Uint64() 70 | } 71 | 72 | func TestSelectValue(t *testing.T) { 73 | assert.Equal(t, SelectValue(true, 1, 2), 1) 74 | assert.Equal(t, SelectValue(false, 1, 2), 2) 75 | } 76 | 77 | func TestIsSameSlice(t *testing.T) { 78 | assert.True(t, IsSameSlice( 79 | []int{1, 2, 3}, 80 | []int{1, 2, 3}, 81 | )) 82 | 83 | assert.False(t, IsSameSlice( 84 | []int{1, 2, 3}, 85 | []int{1, 2}, 86 | )) 87 | 88 | assert.False(t, IsSameSlice( 89 | []int{1, 2, 3}, 90 | []int{1, 2, 4}, 91 | )) 92 | } 93 | -------------------------------------------------------------------------------- /options_test.go: -------------------------------------------------------------------------------- 1 | package memorycache 2 | 3 | import ( 4 | "testing" 5 | "time" 6 | 7 | "github.com/dolthub/swiss" 8 | "github.com/lxzan/memorycache/internal/containers" 9 | "github.com/stretchr/testify/assert" 10 | ) 11 | 12 | func TestWithBucketNum(t *testing.T) { 13 | var as = assert.New(t) 14 | { 15 | var mc = New[string, any](WithBucketNum(3)) 16 | as.Equal(mc.conf.BucketNum, 4) 17 | } 18 | { 19 | var mc = New[string, any](WithBucketNum(0)) 20 | as.Equal(mc.conf.BucketNum, defaultBucketNum) 21 | } 22 | } 23 | 24 | func TestWithInterval(t *testing.T) { 25 | var as = assert.New(t) 26 | { 27 | var mc = New[string, any]() 28 | as.Equal(mc.conf.MinInterval, defaultMinInterval) 29 | as.Equal(mc.conf.MaxInterval, defaultMaxInterval) 30 | } 31 | { 32 | var mc = New[string, any](WithInterval(time.Second, 2*time.Second)) 33 | as.Equal(mc.conf.MinInterval, time.Second) 34 | as.Equal(mc.conf.MaxInterval, 2*time.Second) 35 | } 36 | } 37 | 38 | func TestWithCapacity(t *testing.T) { 39 | var as = assert.New(t) 40 | { 41 | var mc = New[string, any](WithBucketSize(0, 0)) 42 | as.Equal(mc.conf.BucketSize, defaultBucketSize) 43 | as.Equal(mc.conf.BucketCap, defaultBucketCap) 44 | } 45 | { 46 | var mc = New[string, any](WithBucketSize(100, 1000)) 47 | as.Equal(mc.conf.BucketSize, 100) 48 | as.Equal(mc.conf.BucketCap, 1000) 49 | } 50 | } 51 | 52 | func TestWithDeleteLimits(t *testing.T) { 53 | var as = assert.New(t) 54 | { 55 | var mc = New[string, any](WithDeleteLimits(0)) 56 | as.Equal(mc.conf.DeleteLimits, defaultDeleteLimits) 57 | } 58 | { 59 | var mc = New[string, any](WithDeleteLimits(10)) 60 | as.Equal(mc.conf.DeleteLimits, 10) 61 | } 62 | } 63 | 64 | func TestWithTimeCache(t *testing.T) { 65 | var as = assert.New(t) 66 | { 67 | var mc = New[string, any]() 68 | as.Equal(mc.conf.CachedTime, true) 69 | } 70 | { 71 | var mc = New[string, any](WithCachedTime(false)) 72 | as.Equal(mc.conf.CachedTime, false) 73 | } 74 | } 75 | 76 | func TestWithSwissTable(t *testing.T) { 77 | t.Run("", func(t *testing.T) { 78 | var mc = New[string, int]( 79 | WithSwissTable(true), 80 | ) 81 | _, ok := mc.storage[0].Map.(*swiss.Map[uint64, pointer]) 82 | assert.True(t, ok) 83 | assert.True(t, mc.conf.SwissTable) 84 | }) 85 | 86 | t.Run("", func(t *testing.T) { 87 | var mc = New[string, int]() 88 | _, ok := mc.storage[0].Map.(containers.Map[uint64, pointer]) 89 | assert.True(t, ok) 90 | assert.False(t, mc.conf.SwissTable) 91 | }) 92 | } 93 | -------------------------------------------------------------------------------- /heap.go: -------------------------------------------------------------------------------- 1 | package memorycache 2 | 3 | import "github.com/lxzan/dao/algo" 4 | 5 | // newHeap 新建一个堆 6 | // Create a new heap 7 | func newHeap[K comparable, V any](q *deque[K, V], cap int) *heap[K, V] { 8 | return &heap[K, V]{List: q, Data: make([]pointer, 0, cap)} 9 | } 10 | 11 | type heap[K comparable, V any] struct { 12 | List *deque[K, V] 13 | Data []pointer 14 | } 15 | 16 | func (c *heap[K, V]) Less(i, j int) bool { 17 | p0 := c.Data[i] 18 | p1 := c.Data[j] 19 | return c.List.Get(p0).ExpireAt < c.List.Get(p1).ExpireAt 20 | } 21 | 22 | func (c *heap[K, V]) UpdateTTL(ele *Element[K, V], exp int64) { 23 | var down = exp > ele.ExpireAt 24 | ele.ExpireAt = exp 25 | if down { 26 | c.Down(ele.index, c.Len()) 27 | } else { 28 | c.Up(ele.index) 29 | } 30 | } 31 | 32 | func (c *heap[K, V]) Len() int { 33 | return len(c.Data) 34 | } 35 | 36 | func (c *heap[K, V]) Swap(i, j int) { 37 | x := c.List.Get(c.Data[i]) 38 | y := c.List.Get(c.Data[j]) 39 | x.index, y.index = y.index, x.index 40 | c.Data[i], c.Data[j] = c.Data[j], c.Data[i] 41 | } 42 | 43 | func (c *heap[K, V]) Push(ele *Element[K, V]) { 44 | ele.index = c.Len() 45 | c.Data = append(c.Data, ele.addr) 46 | c.Up(c.Len() - 1) 47 | } 48 | 49 | func (c *heap[K, V]) Up(i int) { 50 | var j = (i - 1) >> 2 51 | if i >= 1 && c.Less(i, j) { 52 | c.Swap(i, j) 53 | c.Up(j) 54 | } 55 | } 56 | 57 | func (c *heap[K, V]) Pop() (ele pointer) { 58 | var n = c.Len() 59 | switch n { 60 | case 0: 61 | case 1: 62 | ele = c.Data[0] 63 | c.Data = c.Data[:0] 64 | default: 65 | ele = c.Data[0] 66 | c.Swap(0, n-1) 67 | c.Data = c.Data[:n-1] 68 | c.Down(0, n-1) 69 | } 70 | return 71 | } 72 | 73 | func (c *heap[K, V]) Delete(i int) { 74 | if i == 0 { 75 | c.Pop() 76 | return 77 | } 78 | 79 | var n = c.Len() 80 | var down = c.Less(i, n-1) 81 | c.Swap(i, n-1) 82 | c.Data = c.Data[:n-1] 83 | if i < n-1 { 84 | if down { 85 | c.Down(i, n-1) 86 | } else { 87 | c.Up(i) 88 | } 89 | } 90 | } 91 | 92 | func (c *heap[K, V]) Down(i, n int) { 93 | var base = i << 2 94 | var index = base + 1 95 | if index >= n { 96 | return 97 | } 98 | 99 | var end = algo.Min(base+4, n-1) 100 | for j := base + 2; j <= end; j++ { 101 | if c.Less(j, index) { 102 | index = j 103 | } 104 | } 105 | 106 | if c.Less(index, i) { 107 | c.Swap(i, index) 108 | c.Down(index, n) 109 | } 110 | } 111 | 112 | // Front 访问堆顶元素 113 | // Accessing the top Element of the heap 114 | func (c *heap[K, V]) Front() *Element[K, V] { 115 | return c.List.Get(c.Data[0]) 116 | } 117 | -------------------------------------------------------------------------------- /deque_test.go: -------------------------------------------------------------------------------- 1 | package memorycache 2 | 3 | import ( 4 | "container/list" 5 | "math/rand" 6 | "testing" 7 | 8 | "github.com/lxzan/memorycache/internal/utils" 9 | 10 | "github.com/stretchr/testify/assert" 11 | ) 12 | 13 | func validate(q *deque[int, int]) bool { 14 | var sum = 0 15 | for i := q.Get(q.head); i != nil; i = q.Get(i.next) { 16 | sum++ 17 | next := q.Get(i.next) 18 | if next == nil { 19 | continue 20 | } 21 | if i.next != next.addr { 22 | return false 23 | } 24 | if next.prev != i.addr { 25 | return false 26 | } 27 | } 28 | 29 | if q.Len() != sum { 30 | return false 31 | } 32 | 33 | if head := q.Front(); head != nil { 34 | if head.prev != 0 { 35 | return false 36 | } 37 | } 38 | 39 | if tail := q.Back(); tail != nil { 40 | if tail.next != 0 { 41 | return false 42 | } 43 | } 44 | 45 | if q.Len() == 1 && q.Front().Value != q.Back().Value { 46 | return false 47 | } 48 | 49 | return true 50 | } 51 | 52 | func TestQueue_Random(t *testing.T) { 53 | var count = 10000 54 | var q = newDeque[int, int](0) 55 | var linkedlist = list.New() 56 | for i := 0; i < count; i++ { 57 | var flag = rand.Intn(7) 58 | var val = rand.Int() 59 | switch flag { 60 | case 0, 1, 2, 3: 61 | ele := q.PushBack() 62 | ele.Value = val 63 | linkedlist.PushBack(val) 64 | case 4: 65 | if q.Len() > 0 { 66 | q.PopFront() 67 | linkedlist.Remove(linkedlist.Front()) 68 | } 69 | case 5: 70 | var n = rand.Intn(10) 71 | var index = 0 72 | for iter := q.Front(); iter != nil; iter = q.Get(iter.next) { 73 | index++ 74 | if index >= n { 75 | q.MoveToBack(iter.addr) 76 | break 77 | } 78 | } 79 | 80 | index = 0 81 | for iter := linkedlist.Front(); iter != nil; iter = iter.Next() { 82 | index++ 83 | if index >= n { 84 | linkedlist.MoveToBack(iter) 85 | break 86 | } 87 | } 88 | 89 | case 6: 90 | var n = rand.Intn(10) 91 | var index = 0 92 | for iter := q.Front(); iter != nil; iter = q.Get(iter.next) { 93 | index++ 94 | if index >= n { 95 | q.Remove(iter.addr) 96 | break 97 | } 98 | } 99 | 100 | index = 0 101 | for iter := linkedlist.Front(); iter != nil; iter = iter.Next() { 102 | index++ 103 | if index >= n { 104 | linkedlist.Remove(iter) 105 | break 106 | } 107 | } 108 | default: 109 | 110 | } 111 | } 112 | 113 | assert.True(t, validate(q)) 114 | for i := linkedlist.Front(); i != nil; i = i.Next() { 115 | var ele = q.PopFront() 116 | assert.Equal(t, i.Value, ele.Value) 117 | } 118 | } 119 | 120 | func TestDeque_Range(t *testing.T) { 121 | var q = newDeque[string, int](8) 122 | var push = func(values ...int) { 123 | for _, v := range values { 124 | q.PushBack().Value = v 125 | } 126 | } 127 | push(1, 3, 5, 7, 9) 128 | 129 | { 130 | var arr []int 131 | q.Range(func(ele *Element[string, int]) bool { 132 | arr = append(arr, ele.Value) 133 | return true 134 | }) 135 | assert.True(t, utils.IsSameSlice(arr, []int{1, 3, 5, 7, 9})) 136 | } 137 | 138 | { 139 | var arr []int 140 | q.Range(func(ele *Element[string, int]) bool { 141 | arr = append(arr, ele.Value) 142 | return len(arr) < 3 143 | }) 144 | assert.True(t, utils.IsSameSlice(arr, []int{1, 3, 5})) 145 | } 146 | } 147 | -------------------------------------------------------------------------------- /options.go: -------------------------------------------------------------------------------- 1 | package memorycache 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/lxzan/memorycache/internal/utils" 7 | ) 8 | 9 | const ( 10 | defaultBucketNum = 16 11 | defaultMinInterval = 5 * time.Second 12 | defaultMaxInterval = 30 * time.Second 13 | defaultDeleteLimits = 1000 14 | defaultBucketSize = 1000 15 | defaultBucketCap = 100000 16 | ) 17 | 18 | type Option func(c *config) 19 | 20 | // WithBucketNum 设置存储桶数量 21 | // Setting the number of storage buckets 22 | func WithBucketNum(num int) Option { 23 | return func(c *config) { 24 | c.BucketNum = num 25 | } 26 | } 27 | 28 | // WithDeleteLimits 设置每次TTL检查最大删除key数量. (单个存储桶) 29 | // Set the maximum number of keys to be deleted per TTL check (single bucket) 30 | func WithDeleteLimits(num int) Option { 31 | return func(c *config) { 32 | c.DeleteLimits = num 33 | } 34 | } 35 | 36 | // WithBucketSize 设置初始化大小和最大容量. 超过最大容量会被清除. (单个存储桶) 37 | // Set the initial size and maximum capacity. Exceeding the maximum capacity will be erased. (Single bucket) 38 | func WithBucketSize(size, cap int) Option { 39 | return func(c *config) { 40 | c.BucketSize = size 41 | c.BucketCap = cap 42 | } 43 | } 44 | 45 | // WithInterval 设置TTL检查周期 46 | // 设置过期时间检查周期. 如果过期元素较少, 取最大值, 反之取最小值. 47 | // Setting the TTL check period 48 | // If the number of expired elements is small, take the maximum value, otherwise take the minimum value. 49 | func WithInterval(min, max time.Duration) Option { 50 | return func(c *config) { 51 | c.MinInterval = min 52 | c.MaxInterval = max 53 | } 54 | } 55 | 56 | // WithCachedTime 是否开启时间缓存 57 | // Whether to turn on time caching 58 | func WithCachedTime(enabled bool) Option { 59 | return func(c *config) { 60 | c.CachedTime = enabled 61 | } 62 | } 63 | 64 | // WithSwissTable 使用swiss table替代runtime map 65 | // Using swiss table instead of runtime map 66 | func WithSwissTable(enabled bool) Option { 67 | return func(c *config) { 68 | c.SwissTable = enabled 69 | } 70 | } 71 | 72 | func withInitialize() Option { 73 | return func(c *config) { 74 | if c.BucketNum <= 0 { 75 | c.BucketNum = defaultBucketNum 76 | } 77 | c.BucketNum = utils.ToBinaryNumber(c.BucketNum) 78 | 79 | if c.MinInterval <= 0 { 80 | c.MinInterval = defaultMinInterval 81 | } 82 | 83 | if c.MaxInterval <= 0 { 84 | c.MaxInterval = defaultMaxInterval 85 | } 86 | 87 | if c.DeleteLimits <= 0 { 88 | c.DeleteLimits = defaultDeleteLimits 89 | } 90 | 91 | if c.BucketSize <= 0 { 92 | c.BucketSize = defaultBucketSize 93 | } 94 | 95 | if c.BucketCap <= 0 { 96 | c.BucketCap = defaultBucketCap 97 | } 98 | } 99 | } 100 | 101 | type config struct { 102 | // 检查周期, 默认30s, 最小检查周期为5s 103 | // Check period, default 30s, minimum 5s. 104 | MinInterval, MaxInterval time.Duration 105 | 106 | // 存储桶的初始化大小和最大容量, 默认为1000和100000 107 | // Initialized bucket size and maximum capacity, defaults to 1000 and 100000. 108 | BucketSize, BucketCap int 109 | 110 | // 存储桶数量, 默认为16 111 | // Number of buckets, default is 16 112 | BucketNum int 113 | 114 | // 每次检查至多删除key的数量(单个存储桶) 115 | // The number of keys to be deleted per check (for a single bucket). 116 | DeleteLimits int 117 | 118 | // 是否开启时间缓存, 默认为true 119 | // Whether to enable time caching, true by default. 120 | CachedTime bool 121 | 122 | // 是否使用swiss table, 默认为false 123 | // Whether to use swiss table, false by default. 124 | SwissTable bool 125 | } 126 | -------------------------------------------------------------------------------- /README_CN.md: -------------------------------------------------------------------------------- 1 |
2 |

MemoryCache

3 | logo 4 |
To the time to life, rather than to life in time.
5 |
6 | 7 | [![Build Status][1]][2] [![codecov][3]][4] 8 | 9 | [1]: https://github.com/lxzan/memorycache/workflows/Go%20Test/badge.svg?branch=main 10 | 11 | [2]: https://github.com/lxzan/memorycache/actions?query=branch%3Amain 12 | 13 | [3]: https://codecov.io/gh/lxzan/memorycache/graph/badge.svg?token=OHD6918OPT 14 | 15 | [4]: https://codecov.io/gh/lxzan/memorycache 16 | 17 | ### 简介: 18 | 19 | 极简的内存键值(KV)存储系统,其核心由哈希表(HashMap) 和最小四叉堆(Minimal Quad Heap) 构成. 20 | 21 | **缓存淘汰策略:** 22 | 23 | - Set 方法清理溢出的键值对 24 | - 周期清理过期的键值对 25 | 26 | ### 原则: 27 | 28 | - 存储数据限制:受最大容量限制 29 | - 过期时间:支持 30 | - 缓存驱逐策略:LRU 31 | - 持久化:无 32 | - 锁定机制:分片和互斥锁 33 | - GC 优化:无指针技术实现的哈希表, 最小堆和链表(不包括用户KV) 34 | 35 | ### 优势: 36 | 37 | - 简单易用 38 | - 高性能 39 | - 内存占用低 40 | - 使用四叉堆维护过期时间, 有效降低树高度, 提高插入性能 41 | 42 | ### 方法: 43 | 44 | - [x] **Set** : 设置键值对及其过期时间。如果键已存在,将更新其值和过期时间。 45 | - [x] **SetWithCallback** : 与 Set 类似,但可指定回调函数。 46 | - [x] **Get** : 根据键获取值。如果键不存在,第二个返回值为 false。 47 | - [x] **GetWithTTL** : 根据键获取值,如果键不存在,第二个返回值为 false。在返回值时,该方法将刷新过期时间。 48 | - [x] **Delete** : 根据键删除键值对。 49 | - [x] **GetOrCreate** : 根据键获取值。如果键不存在,将创建该值。 50 | - [x] **GetOrCreateWithCallback** : 根据键获取值。如果键不存在,将创建该值,并可调用回调函数。 51 | 52 | ### 使用 53 | 54 | ```go 55 | package main 56 | 57 | import ( 58 | "fmt" 59 | "time" 60 | 61 | "github.com/lxzan/memorycache" 62 | ) 63 | 64 | func main() { 65 | mc := memorycache.New[string, any]( 66 | // 设置存储桶数量, y=pow(2,x) 67 | memorycache.WithBucketNum(128), 68 | 69 | // 设置单个存储桶的初始化容量和最大容量 70 | memorycache.WithBucketSize(1000, 10000), 71 | 72 | // 设置过期时间检查周期. 如果过期元素较少, 取最大值, 反之取最小值. 73 | memorycache.WithInterval(5*time.Second, 30*time.Second), 74 | ) 75 | 76 | mc.SetWithCallback("xxx", 1, time.Second, func(element *memorycache.Element[string, any], reason memorycache.Reason) { 77 | fmt.Printf("callback: key=%s, reason=%v\n", element.Key, reason) 78 | }) 79 | 80 | val, exist := mc.Get("xxx") 81 | fmt.Printf("val=%v, exist=%v\n", val, exist) 82 | 83 | time.Sleep(2 * time.Second) 84 | 85 | val, exist = mc.Get("xxx") 86 | fmt.Printf("val=%v, exist=%v\n", val, exist) 87 | } 88 | 89 | ``` 90 | 91 | ### 基准测试 92 | 93 | - 1,000,000 元素 94 | 95 | ``` 96 | goos: linux 97 | goarch: amd64 98 | pkg: github.com/lxzan/memorycache/benchmark 99 | cpu: AMD Ryzen 5 PRO 4650G with Radeon Graphics 100 | BenchmarkMemoryCache_Set-8 16107153 74.85 ns/op 15 B/op 0 allocs/op 101 | BenchmarkMemoryCache_Get-8 28859542 42.34 ns/op 0 B/op 0 allocs/op 102 | BenchmarkMemoryCache_SetAndGet-8 27317874 63.02 ns/op 0 B/op 0 allocs/op 103 | BenchmarkRistretto_Set-8 13343023 272.6 ns/op 120 B/op 2 allocs/op 104 | BenchmarkRistretto_Get-8 19799044 55.06 ns/op 17 B/op 1 allocs/op 105 | BenchmarkRistretto_SetAndGet-8 11212923 119.6 ns/op 30 B/op 1 allocs/op 106 | BenchmarkTheine_Set-8 3775975 322.5 ns/op 30 B/op 0 allocs/op 107 | BenchmarkTheine_Get-8 21579301 54.94 ns/op 0 B/op 0 allocs/op 108 | BenchmarkTheine_SetAndGet-8 6265330 224.6 ns/op 0 B/op 0 allocs/op 109 | PASS 110 | ok github.com/lxzan/memorycache/benchmark 53.498s 111 | ``` 112 | -------------------------------------------------------------------------------- /deque.go: -------------------------------------------------------------------------------- 1 | package memorycache 2 | 3 | import ( 4 | "github.com/lxzan/dao/stack" 5 | ) 6 | 7 | const null = 0 8 | 9 | type ( 10 | pointer uint32 11 | 12 | // Deque 可以不使用New函数, 声明为值类型自动初始化 13 | deque[K comparable, V any] struct { 14 | head, tail pointer // 头尾指针 15 | length int // 长度 16 | stack stack.Stack[pointer] // 回收站 17 | elements []Element[K, V] // 元素列表 18 | template Element[K, V] // 空值模板 19 | } 20 | ) 21 | 22 | func (c pointer) IsNil() bool { 23 | return c == null 24 | } 25 | 26 | func newDeque[K comparable, V any](capacity int) *deque[K, V] { 27 | return &deque[K, V]{elements: make([]Element[K, V], 1, 1+capacity)} 28 | } 29 | 30 | func (c *deque[K, V]) Get(addr pointer) *Element[K, V] { 31 | if addr > 0 { 32 | return &(c.elements[addr]) 33 | } 34 | return nil 35 | } 36 | 37 | // getElement 追加元素一定要先调用此方法, 因为追加可能会造成扩容, 地址发生变化!!! 38 | func (c *deque[K, V]) getElement() *Element[K, V] { 39 | if c.stack.Len() > 0 { 40 | addr := c.stack.Pop() 41 | v := c.Get(addr) 42 | v.addr = addr 43 | return v 44 | } 45 | 46 | addr := pointer(len(c.elements)) 47 | c.elements = append(c.elements, c.template) 48 | v := c.Get(addr) 49 | v.addr = addr 50 | return v 51 | } 52 | 53 | func (c *deque[K, V]) putElement(ele *Element[K, V]) { 54 | c.stack.Push(ele.addr) 55 | *ele = c.template 56 | } 57 | 58 | func (c *deque[K, V]) autoReset() { 59 | c.head, c.tail, c.length = null, null, 0 60 | c.stack = c.stack[:0] 61 | c.elements = c.elements[:1] 62 | } 63 | 64 | func (c *deque[K, V]) Len() int { 65 | return c.length 66 | } 67 | 68 | func (c *deque[K, V]) Front() *Element[K, V] { 69 | return c.Get(c.head) 70 | } 71 | 72 | func (c *deque[K, V]) Back() *Element[K, V] { 73 | return c.Get(c.tail) 74 | } 75 | 76 | func (c *deque[K, V]) PushBack() *Element[K, V] { 77 | ele := c.getElement() 78 | c.doPushBack(ele) 79 | return ele 80 | } 81 | 82 | func (c *deque[K, V]) doPushBack(ele *Element[K, V]) { 83 | c.length++ 84 | 85 | if c.tail.IsNil() { 86 | c.head, c.tail = ele.addr, ele.addr 87 | return 88 | } 89 | 90 | tail := c.Get(c.tail) 91 | tail.next = ele.addr 92 | ele.prev = tail.addr 93 | c.tail = ele.addr 94 | } 95 | 96 | func (c *deque[K, V]) PopFront() (value Element[K, V]) { 97 | if ele := c.Front(); ele != nil { 98 | value = *ele 99 | c.doRemove(ele) 100 | c.putElement(ele) 101 | if c.length == 0 { 102 | c.autoReset() 103 | } 104 | } 105 | return value 106 | } 107 | 108 | func (c *deque[K, V]) MoveToBack(addr pointer) { 109 | if ele := c.Get(addr); ele != nil { 110 | c.doRemove(ele) 111 | ele.prev, ele.next = null, null 112 | c.doPushBack(ele) 113 | } 114 | } 115 | 116 | func (c *deque[K, V]) Remove(addr pointer) { 117 | if ele := c.Get(addr); ele != nil { 118 | c.doRemove(ele) 119 | c.putElement(ele) 120 | if c.length == 0 { 121 | c.autoReset() 122 | } 123 | } 124 | } 125 | 126 | func (c *deque[K, V]) doRemove(ele *Element[K, V]) { 127 | var prev, next *Element[K, V] = nil, nil 128 | var state = 0 129 | if !ele.prev.IsNil() { 130 | prev = c.Get(ele.prev) 131 | state += 1 132 | } 133 | if !ele.next.IsNil() { 134 | next = c.Get(ele.next) 135 | state += 2 136 | } 137 | 138 | c.length-- 139 | switch state { 140 | case 3: 141 | prev.next = next.addr 142 | next.prev = prev.addr 143 | case 2: 144 | next.prev = null 145 | c.head = next.addr 146 | case 1: 147 | prev.next = null 148 | c.tail = prev.addr 149 | default: 150 | c.head = null 151 | c.tail = null 152 | } 153 | } 154 | 155 | func (c *deque[K, V]) Range(f func(ele *Element[K, V]) bool) { 156 | for i := c.Get(c.head); i != nil; i = c.Get(i.next) { 157 | if !f(i) { 158 | break 159 | } 160 | } 161 | } 162 | -------------------------------------------------------------------------------- /benchmark/go.sum: -------------------------------------------------------------------------------- 1 | github.com/Yiling-J/theine-go v0.3.1 h1:pNrTp2s/ytpqY7JdzjL9GrzeJOqjvHHqumRiphUAiFU= 2 | github.com/Yiling-J/theine-go v0.3.1/go.mod h1:9HtlXa6gjwnqdhqW0R/0BDHxGF4CNmZdVBiv6BdISOw= 3 | github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= 4 | github.com/cespare/xxhash/v2 v2.1.2 h1:YRXhKfTDauu4ajMg1TPgFO5jnlC2HCbmLXMcTG5cbYE= 5 | github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= 6 | github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= 7 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 8 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 9 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 10 | github.com/dgraph-io/ristretto v0.1.1 h1:6CWw5tJNgpegArSHpNHJKldNeq03FQCwYvfMVWajOK8= 11 | github.com/dgraph-io/ristretto v0.1.1/go.mod h1:S1GPSBCYCIhmVNfcth17y2zZtQT6wzkzgwUve0VDWWA= 12 | github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2 h1:tdlZCpZ/P9DhczCTSixgIKmwPv6+wP5DGjqLYw5SUiA= 13 | github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw= 14 | github.com/dolthub/maphash v0.1.0 h1:bsQ7JsF4FkkWyrP3oCnFJgrCUAFbFf3kOl4L/QxPDyQ= 15 | github.com/dolthub/maphash v0.1.0/go.mod h1:gkg4Ch4CdCDu5h6PMriVLawB7koZ+5ijb9puGMV50a4= 16 | github.com/dolthub/swiss v0.2.1 h1:gs2osYs5SJkAaH5/ggVJqXQxRXtWshF6uE0lgR/Y3Gw= 17 | github.com/dolthub/swiss v0.2.1/go.mod h1:8AhKZZ1HK7g18j7v7k6c5cYIGEZJcPn0ARsai8cUrh0= 18 | github.com/dustin/go-humanize v1.0.0 h1:VSnTsYCnlFHaM2/igO1h6X3HA71jcobQuxemgkq4zYo= 19 | github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= 20 | github.com/gammazero/deque v0.2.1 h1:qSdsbG6pgp6nL7A0+K/B7s12mcCY/5l5SIUpMOl+dC0= 21 | github.com/gammazero/deque v0.2.1/go.mod h1:LFroj8x4cMYCukHJDbxFCkT+r9AndaJnFMuZDV34tuU= 22 | github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b h1:VKtxabqXZkF25pY9ekfRL6a582T4P37/31XEstQ5p58= 23 | github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= 24 | github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k= 25 | github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM= 26 | github.com/klauspost/cpuid/v2 v2.2.6 h1:ndNyv040zDGIDh8thGkXYjnFtiN02M1PVVF+JE/48xc= 27 | github.com/klauspost/cpuid/v2 v2.2.6/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws= 28 | github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= 29 | github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= 30 | github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 31 | github.com/lxzan/dao v1.1.2 h1:ATYm6yBE117iQetDTmCZeHB4mBFjO43NbsEt7wf/JBo= 32 | github.com/lxzan/dao v1.1.2/go.mod h1:5ChTIo7RSZ4upqRo16eicJ3XdJWhGwgMIsyuGLMUofM= 33 | github.com/ncw/directio v1.0.5 h1:JSUBhdjEvVaJvOoyPAbcW0fnd0tvRXD76wEfZ1KcQz4= 34 | github.com/ncw/directio v1.0.5/go.mod h1:rX/pKEYkOXBGOggmcyJeJGloCkleSvphPx2eV3t6ROk= 35 | github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= 36 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 37 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 38 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 39 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 40 | github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= 41 | github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= 42 | github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= 43 | github.com/zeebo/assert v1.3.0 h1:g7C04CbJuIDKNPFHmsk4hwZDO5O+kntRxzaUoNXj+IQ= 44 | github.com/zeebo/xxh3 v1.0.2 h1:xZmwmqxHZA8AI603jOQ0tMqmBr9lPeFwGg6d+xy9DC0= 45 | github.com/zeebo/xxh3 v1.0.2/go.mod h1:5NWz9Sef7zIDm2JHfFlcQvNekmcEl9ekUZQQKCYaDcA= 46 | golang.org/x/sys v0.0.0-20221010170243-090e33056c14/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 47 | golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 48 | golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc= 49 | golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 50 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 51 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= 52 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 53 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 54 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 55 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 |

MemoryCache

3 | logo 4 |
To the time to life, rather than to life in time.
5 |
6 | 7 | 8 | [中文](README_CN.md) 9 | 10 | [![Build Status][1]][2] [![codecov][3]][4] 11 | 12 | [1]: https://github.com/lxzan/memorycache/workflows/Go%20Test/badge.svg?branch=main 13 | 14 | [2]: https://github.com/lxzan/memorycache/actions?query=branch%3Amain 15 | 16 | [3]: https://codecov.io/gh/lxzan/memorycache/graph/badge.svg?token=OHD6918OPT 17 | 18 | [4]: https://codecov.io/gh/lxzan/memorycache 19 | 20 | ### Description 21 | 22 | Minimalist in-memory KV storage, powered by `HashMap` and `Minimal Quad Heap`. 23 | 24 | **Cache Elimination Policy:** 25 | 26 | - Set method cleans up overflowed keys 27 | - Active cycle cleans up expired keys 28 | 29 | ### Principle 30 | 31 | - Storage Data Limit: Limited by maximum capacity 32 | - Expiration Time: Supported 33 | - Cache Eviction Policy: LRU 34 | - Persistent: None 35 | - Locking Mechanism: Slicing + Mutual Exclusion Locking 36 | - HashMap, Heap and LinkedList (excluding user KVs) implemented in pointerless technology 37 | 38 | ### Advantage 39 | 40 | - Simple and easy to use 41 | - High performance 42 | - Low memory usage 43 | - Use quadruple heap to maintain the expiration time, effectively reduce the height of the tree, and improve the 44 | insertion performance 45 | 46 | ### Methods 47 | 48 | - [x] **Set** : Set key-value pair with expiring time. If the key already exists, the value will be updated. Also the 49 | expiration time will be updated. 50 | - [x] **SetWithCallback** : Set key-value pair with expiring time and callback function. If the key already exists, 51 | the value will be updated. Also the expiration time will be updated. 52 | - [x] **Get** : Get value by key. If the key does not exist, the second return value will be false. 53 | - [x] **GetWithTTL** : Get value by key. If the key does not exist, the second return value will be false. When return 54 | value, method will refresh the expiration time. 55 | - [x] **Delete** : Delete key-value pair by key. 56 | - [x] **GetOrCreate** : Get value by key. If the key does not exist, the value will be created. 57 | - [x] **GetOrCreateWithCallback** : Get value by key. If the key does not exist, the value will be created. Also the 58 | callback function will be called. 59 | 60 | ### Example 61 | 62 | ```go 63 | package main 64 | 65 | import ( 66 | "fmt" 67 | "github.com/lxzan/memorycache" 68 | "time" 69 | ) 70 | 71 | func main() { 72 | mc := memorycache.New[string, any]( 73 | // Set the number of storage buckets, y=pow(2,x) 74 | memorycache.WithBucketNum(128), 75 | 76 | // Set bucket size, initial size and maximum capacity (single bucket) 77 | memorycache.WithBucketSize(1000, 10000), 78 | 79 | // Set the expiration time check period. 80 | // If the number of expired elements is small, take the maximum value, otherwise take the minimum value. 81 | memorycache.WithInterval(5*time.Second, 30*time.Second), 82 | ) 83 | 84 | mc.SetWithCallback("xxx", 1, time.Second, func(element *memorycache.Element[string, any], reason memorycache.Reason) { 85 | fmt.Printf("callback: key=%s, reason=%v\n", element.Key, reason) 86 | }) 87 | 88 | val, exist := mc.Get("xxx") 89 | fmt.Printf("val=%v, exist=%v\n", val, exist) 90 | 91 | time.Sleep(2 * time.Second) 92 | 93 | val, exist = mc.Get("xxx") 94 | fmt.Printf("val=%v, exist=%v\n", val, exist) 95 | } 96 | 97 | ``` 98 | 99 | ### Benchmark 100 | 101 | - 1,000,000 elements 102 | 103 | ``` 104 | goos: linux 105 | goarch: amd64 106 | pkg: github.com/lxzan/memorycache/benchmark 107 | cpu: AMD Ryzen 5 PRO 4650G with Radeon Graphics 108 | BenchmarkMemoryCache_Set-8 16107153 74.85 ns/op 15 B/op 0 allocs/op 109 | BenchmarkMemoryCache_Get-8 28859542 42.34 ns/op 0 B/op 0 allocs/op 110 | BenchmarkMemoryCache_SetAndGet-8 27317874 63.02 ns/op 0 B/op 0 allocs/op 111 | BenchmarkRistretto_Set-8 13343023 272.6 ns/op 120 B/op 2 allocs/op 112 | BenchmarkRistretto_Get-8 19799044 55.06 ns/op 17 B/op 1 allocs/op 113 | BenchmarkRistretto_SetAndGet-8 11212923 119.6 ns/op 30 B/op 1 allocs/op 114 | BenchmarkTheine_Set-8 3775975 322.5 ns/op 30 B/op 0 allocs/op 115 | BenchmarkTheine_Get-8 21579301 54.94 ns/op 0 B/op 0 allocs/op 116 | BenchmarkTheine_SetAndGet-8 6265330 224.6 ns/op 0 B/op 0 allocs/op 117 | PASS 118 | ok github.com/lxzan/memorycache/benchmark 53.498s 119 | ``` 120 | -------------------------------------------------------------------------------- /benchmark/benchmark_test.go: -------------------------------------------------------------------------------- 1 | package benchmark 2 | 3 | import ( 4 | "testing" 5 | "time" 6 | 7 | "github.com/Yiling-J/theine-go" 8 | "github.com/dgraph-io/ristretto" 9 | lru "github.com/hashicorp/golang-lru/v2" 10 | "github.com/lxzan/memorycache" 11 | "github.com/lxzan/memorycache/internal/utils" 12 | "github.com/stretchr/testify/assert" 13 | ) 14 | 15 | const ( 16 | sharding = 128 17 | capacity = 10000 18 | benchcount = 1 << 20 19 | ) 20 | 21 | var ( 22 | benchkeys = make([]string, 0, benchcount) 23 | 24 | options = []memorycache.Option{ 25 | memorycache.WithBucketNum(sharding), 26 | memorycache.WithBucketSize(capacity/10, capacity), 27 | memorycache.WithSwissTable(false), 28 | } 29 | ) 30 | 31 | func init() { 32 | for i := 0; i < benchcount; i++ { 33 | benchkeys = append(benchkeys, string(utils.AlphabetNumeric.Generate(16))) 34 | } 35 | } 36 | 37 | func getIndex(i int) int { 38 | return i & (len(benchkeys) - 1) 39 | } 40 | 41 | func BenchmarkMemoryCache_Set(b *testing.B) { 42 | var mc = memorycache.New[string, int](options...) 43 | b.RunParallel(func(pb *testing.PB) { 44 | var i = 0 45 | for pb.Next() { 46 | index := getIndex(i) 47 | i++ 48 | mc.Set(benchkeys[index], 1, time.Hour) 49 | } 50 | }) 51 | } 52 | 53 | func BenchmarkMemoryCache_Get(b *testing.B) { 54 | var mc = memorycache.New[string, int](options...) 55 | for i := 0; i < benchcount; i++ { 56 | mc.Set(benchkeys[i%benchcount], 1, time.Hour) 57 | } 58 | 59 | b.ResetTimer() 60 | b.RunParallel(func(pb *testing.PB) { 61 | var i = 0 62 | for pb.Next() { 63 | index := getIndex(i) 64 | i++ 65 | mc.Get(benchkeys[index]) 66 | } 67 | }) 68 | } 69 | 70 | func BenchmarkMemoryCache_SetAndGet(b *testing.B) { 71 | var mc = memorycache.New[string, int](options...) 72 | for i := 0; i < benchcount; i++ { 73 | mc.Set(benchkeys[i%benchcount], 1, time.Hour) 74 | } 75 | 76 | b.ResetTimer() 77 | b.RunParallel(func(pb *testing.PB) { 78 | var i = 0 79 | for pb.Next() { 80 | index := getIndex(i) 81 | i++ 82 | if index&7 == 0 { 83 | mc.Set(benchkeys[index], 1, time.Hour) 84 | } else { 85 | mc.Get(benchkeys[index]) 86 | } 87 | } 88 | }) 89 | } 90 | 91 | func BenchmarkRistretto_Set(b *testing.B) { 92 | var mc, _ = ristretto.NewCache(&ristretto.Config{ 93 | NumCounters: capacity * sharding * 10, // number of keys to track frequency of (10M). 94 | MaxCost: 1 << 30, // maximum cost of cache (1GB). 95 | BufferItems: 64, // number of keys per Get buffer. 96 | }) 97 | b.RunParallel(func(pb *testing.PB) { 98 | var i = 0 99 | for pb.Next() { 100 | index := getIndex(i) 101 | i++ 102 | mc.SetWithTTL(benchkeys[index], 1, 1, time.Hour) 103 | } 104 | }) 105 | } 106 | 107 | func BenchmarkRistretto_Get(b *testing.B) { 108 | var mc, _ = ristretto.NewCache(&ristretto.Config{ 109 | NumCounters: capacity * sharding * 10, // number of keys to track frequency of (10M). 110 | MaxCost: 1 << 30, // maximum cost of cache (1GB). 111 | BufferItems: 64, // number of keys per Get buffer. 112 | }) 113 | for i := 0; i < benchcount; i++ { 114 | mc.SetWithTTL(benchkeys[i%benchcount], 1, 1, time.Hour) 115 | } 116 | 117 | b.ResetTimer() 118 | b.RunParallel(func(pb *testing.PB) { 119 | var i = 0 120 | for pb.Next() { 121 | index := getIndex(i) 122 | i++ 123 | mc.Get(benchkeys[index]) 124 | } 125 | }) 126 | } 127 | 128 | func BenchmarkRistretto_SetAndGet(b *testing.B) { 129 | var mc, _ = ristretto.NewCache(&ristretto.Config{ 130 | NumCounters: capacity * sharding * 10, // number of keys to track frequency of (10M). 131 | MaxCost: 1 << 30, // maximum cost of cache (1GB). 132 | BufferItems: 64, // number of keys per Get buffer. 133 | }) 134 | for i := 0; i < benchcount; i++ { 135 | mc.SetWithTTL(benchkeys[i%benchcount], 1, 1, time.Hour) 136 | } 137 | 138 | b.ResetTimer() 139 | b.RunParallel(func(pb *testing.PB) { 140 | var i = 0 141 | for pb.Next() { 142 | index := getIndex(i) 143 | i++ 144 | if index&7 == 0 { 145 | mc.SetWithTTL(benchkeys[index], 1, 1, time.Hour) 146 | } else { 147 | mc.Get(benchkeys[index]) 148 | } 149 | } 150 | }) 151 | } 152 | 153 | func BenchmarkTheine_Set(b *testing.B) { 154 | mc, _ := theine.NewBuilder[string, int](sharding * capacity).Build() 155 | b.RunParallel(func(pb *testing.PB) { 156 | i := 0 157 | for pb.Next() { 158 | index := getIndex(i) 159 | i++ 160 | mc.SetWithTTL(benchkeys[index], 1, 1, time.Hour) 161 | } 162 | }) 163 | } 164 | 165 | func BenchmarkTheine_Get(b *testing.B) { 166 | mc, _ := theine.NewBuilder[string, int](sharding * capacity).Build() 167 | for i := 0; i < benchcount; i++ { 168 | mc.SetWithTTL(benchkeys[i%benchcount], 1, 1, time.Hour) 169 | } 170 | 171 | b.ResetTimer() 172 | b.RunParallel(func(pb *testing.PB) { 173 | i := 0 174 | for pb.Next() { 175 | index := getIndex(i) 176 | i++ 177 | mc.Get(benchkeys[index]) 178 | } 179 | }) 180 | } 181 | 182 | func BenchmarkTheine_SetAndGet(b *testing.B) { 183 | mc, _ := theine.NewBuilder[string, int](sharding * capacity).Build() 184 | for i := 0; i < benchcount; i++ { 185 | mc.SetWithTTL(benchkeys[i%benchcount], 1, 1, time.Hour) 186 | } 187 | 188 | b.ResetTimer() 189 | b.RunParallel(func(pb *testing.PB) { 190 | i := 0 191 | for pb.Next() { 192 | index := getIndex(i) 193 | i++ 194 | if index&7 == 0 { 195 | mc.SetWithTTL(benchkeys[index], 1, 1, time.Hour) 196 | } else { 197 | mc.Get(benchkeys[index]) 198 | } 199 | } 200 | }) 201 | } 202 | 203 | // 测试LRU算法实现的正确性 204 | func TestLRU_Impl(t *testing.T) { 205 | var f = func() { 206 | var count = 10000 207 | var capacity = 5000 208 | var mc = memorycache.New[string, int]( 209 | memorycache.WithBucketNum(1), 210 | memorycache.WithBucketSize(capacity, capacity), 211 | ) 212 | var cache, _ = lru.New[string, int](capacity) 213 | for i := 0; i < count; i++ { 214 | key := string(utils.AlphabetNumeric.Generate(16)) 215 | val := utils.AlphabetNumeric.Intn(capacity) 216 | mc.Set(key, val, time.Hour) 217 | cache.Add(key, val) 218 | } 219 | 220 | keys := cache.Keys() 221 | assert.Equal(t, mc.Len(), capacity) 222 | assert.Equal(t, mc.Len(), cache.Len()) 223 | assert.Equal(t, mc.Len(), len(keys)) 224 | 225 | for _, key := range keys { 226 | v1, ok1 := mc.Get(key) 227 | v2, _ := cache.Peek(key) 228 | assert.True(t, ok1) 229 | assert.Equal(t, v1, v2) 230 | } 231 | } 232 | 233 | for i := 0; i < 10; i++ { 234 | f() 235 | } 236 | } 237 | -------------------------------------------------------------------------------- /cache.go: -------------------------------------------------------------------------------- 1 | package memorycache 2 | 3 | import ( 4 | "context" 5 | "math" 6 | "sync" 7 | "sync/atomic" 8 | "time" 9 | 10 | "github.com/dolthub/maphash" 11 | "github.com/lxzan/memorycache/internal/containers" 12 | "github.com/lxzan/memorycache/internal/utils" 13 | ) 14 | 15 | type MemoryCache[K comparable, V any] struct { 16 | conf *config 17 | storage []*bucket[K, V] 18 | hasher utils.Hasher[K] 19 | timestamp atomic.Int64 20 | ctx context.Context 21 | cancel context.CancelFunc 22 | wg sync.WaitGroup 23 | once sync.Once 24 | callback CallbackFunc[*Element[K, V]] 25 | } 26 | 27 | // New 创建缓存数据库实例 28 | // Creating a Cached Database Instance 29 | func New[K comparable, V any](options ...Option) *MemoryCache[K, V] { 30 | var conf = &config{CachedTime: true} 31 | options = append(options, withInitialize()) 32 | for _, fn := range options { 33 | fn(conf) 34 | } 35 | 36 | mc := &MemoryCache[K, V]{ 37 | conf: conf, 38 | storage: make([]*bucket[K, V], conf.BucketNum), 39 | hasher: maphash.NewHasher[K](), 40 | wg: sync.WaitGroup{}, 41 | once: sync.Once{}, 42 | } 43 | mc.callback = func(entry *Element[K, V], reason Reason) {} 44 | mc.ctx, mc.cancel = context.WithCancel(context.Background()) 45 | mc.timestamp.Store(time.Now().UnixMilli()) 46 | 47 | for i, _ := range mc.storage { 48 | b := (&bucket[K, V]{conf: conf}).init() 49 | mc.storage[i] = b 50 | } 51 | 52 | go func() { 53 | var d0 = conf.MaxInterval 54 | var ticker = time.NewTicker(d0) 55 | defer ticker.Stop() 56 | 57 | for { 58 | select { 59 | case <-mc.ctx.Done(): 60 | mc.wg.Done() 61 | return 62 | case now := <-ticker.C: 63 | var sum = 0 64 | for _, b := range mc.storage { 65 | sum += b.Check(now.UnixMilli(), conf.DeleteLimits) 66 | } 67 | 68 | // 删除数量超过阈值, 缩小时间间隔 69 | if sum > 0 { 70 | d1 := utils.SelectValue(sum > conf.BucketNum*conf.DeleteLimits*7/10, conf.MinInterval, conf.MaxInterval) 71 | if d1 != d0 { 72 | d0 = d1 73 | ticker.Reset(d0) 74 | } 75 | } 76 | } 77 | } 78 | }() 79 | 80 | // 每秒更新一次时间戳 81 | go func() { 82 | var ticker = time.NewTicker(time.Second) 83 | defer ticker.Stop() 84 | 85 | for { 86 | select { 87 | case <-mc.ctx.Done(): 88 | mc.wg.Done() 89 | return 90 | case now := <-ticker.C: 91 | mc.timestamp.Store(now.UnixMilli()) 92 | } 93 | } 94 | }() 95 | 96 | return mc 97 | } 98 | 99 | // Clear 清空缓存 100 | // clear caches 101 | func (c *MemoryCache[K, V]) Clear() { 102 | for _, b := range c.storage { 103 | b.Lock() 104 | b.init() 105 | b.Unlock() 106 | } 107 | } 108 | 109 | func (c *MemoryCache[K, V]) Stop() { 110 | c.once.Do(func() { 111 | c.wg.Add(2) 112 | c.cancel() 113 | c.wg.Wait() 114 | }) 115 | } 116 | 117 | func (c *MemoryCache[K, V]) getTimestamp() int64 { 118 | if c.conf.CachedTime { 119 | return c.timestamp.Load() 120 | } 121 | return time.Now().UnixMilli() 122 | } 123 | 124 | // 获取过期时间, d<=0表示永不过期 125 | func (c *MemoryCache[K, V]) getExp(d time.Duration) int64 { 126 | if d <= 0 { 127 | return math.MaxInt64 128 | } 129 | return c.getTimestamp() + d.Milliseconds() 130 | } 131 | 132 | func (c *MemoryCache[K, V]) getBucket(key K) bucketWrapper[K, V] { 133 | var hashcode = c.hasher.Hash(key) 134 | var index = hashcode & uint64(c.conf.BucketNum-1) 135 | return bucketWrapper[K, V]{bucket: c.storage[index], hashcode: hashcode} 136 | } 137 | 138 | // 查找数据. 如果存在且超时, 删除并返回false 139 | // @ele 查找结果 140 | // @conflict 是否哈希冲突 141 | // @exist 是否存在 142 | func (c *MemoryCache[K, V]) fetch(b bucketWrapper[K, V], key K) (ele *Element[K, V], conflict, exist bool) { 143 | addr, ok := b.Map.Get(b.hashcode) 144 | if !ok { 145 | return nil, false, false 146 | } 147 | 148 | ele = b.List.Get(addr) 149 | if ele.expired(c.getTimestamp()) { 150 | b.Delete(ele, ReasonExpired) 151 | return nil, false, false 152 | } 153 | 154 | return ele, key != ele.Key, true 155 | } 156 | 157 | // Set 设置键值和过期时间. exp<=0表示永不过期. 158 | // Set the key value and expiration time. exp<=0 means never expire. 159 | func (c *MemoryCache[K, V]) Set(key K, value V, exp time.Duration) (exist bool) { 160 | return c.SetWithCallback(key, value, exp, c.callback) 161 | } 162 | 163 | // SetWithCallback 设置键值, 过期时间和回调函数. 容量溢出和过期都会触发回调. 164 | // Set the key value, expiration time and callback function. The callback is triggered by both capacity overflow and expiration. 165 | func (c *MemoryCache[K, V]) SetWithCallback(key K, value V, exp time.Duration, cb CallbackFunc[*Element[K, V]]) (exist bool) { 166 | var b = c.getBucket(key) 167 | b.Lock() 168 | defer b.Unlock() 169 | 170 | var expireAt = c.getExp(exp) 171 | ele, conflict, ok := c.fetch(b, key) 172 | if conflict { 173 | ok = false 174 | b.Delete(ele, ReasonEvicted) 175 | } 176 | if ok { 177 | ele.Value, ele.cb = value, cb 178 | b.UpdateTTL(ele, expireAt) 179 | return true 180 | } 181 | 182 | ele = b.GetElement() 183 | ele.Key, ele.Value, ele.ExpireAt, ele.hashcode, ele.cb = key, value, expireAt, b.hashcode, cb 184 | b.Insert(ele) 185 | return false 186 | } 187 | 188 | // Get 查询缓存 189 | // query cache 190 | func (c *MemoryCache[K, V]) Get(key K) (v V, exist bool) { 191 | var b = c.getBucket(key) 192 | b.Lock() 193 | defer b.Unlock() 194 | 195 | ele, conflict, ok := c.fetch(b, key) 196 | if !ok || conflict { 197 | return v, false 198 | } 199 | 200 | b.List.MoveToBack(ele.addr) 201 | return ele.Value, true 202 | } 203 | 204 | // GetWithTTL 获取. 如果存在, 刷新过期时间. 205 | // Get a value. If it exists, refreshes the expiration time. 206 | func (c *MemoryCache[K, V]) GetWithTTL(key K, exp time.Duration) (v V, exist bool) { 207 | var b = c.getBucket(key) 208 | b.Lock() 209 | defer b.Unlock() 210 | 211 | ele, conflict, ok := c.fetch(b, key) 212 | if !ok || conflict { 213 | return v, false 214 | } 215 | 216 | b.UpdateTTL(ele, c.getExp(exp)) 217 | return ele.Value, true 218 | } 219 | 220 | // GetOrCreate 如果存在, 刷新过期时间. 如果不存在, 创建一个新的. 221 | // Get or create a value. If it exists, refreshes the expiration time. If it does not exist, creates a new one. 222 | func (c *MemoryCache[K, V]) GetOrCreate(key K, value V, exp time.Duration) (v V, exist bool) { 223 | return c.GetOrCreateWithCallback(key, value, exp, c.callback) 224 | } 225 | 226 | // GetOrCreateWithCallback 如果存在, 刷新过期时间. 如果不存在, 创建一个新的. 227 | // Get or create a value with CallbackFunc. If it exists, refreshes the expiration time. If it does not exist, creates a new one. 228 | func (c *MemoryCache[K, V]) GetOrCreateWithCallback(key K, value V, exp time.Duration, cb CallbackFunc[*Element[K, V]]) (v V, exist bool) { 229 | var b = c.getBucket(key) 230 | b.Lock() 231 | defer b.Unlock() 232 | 233 | expireAt := c.getExp(exp) 234 | ele, conflict, ok := c.fetch(b, key) 235 | if conflict { 236 | ok = false 237 | b.Delete(ele, ReasonEvicted) 238 | } 239 | if ok { 240 | b.UpdateTTL(ele, expireAt) 241 | return ele.Value, true 242 | } 243 | 244 | ele = b.GetElement() 245 | ele.Key, ele.Value, ele.ExpireAt, ele.hashcode, ele.cb = key, value, expireAt, b.hashcode, cb 246 | b.Insert(ele) 247 | return value, false 248 | } 249 | 250 | // Delete 删除缓存 251 | // delete cache 252 | func (c *MemoryCache[K, V]) Delete(key K) (exist bool) { 253 | var b = c.getBucket(key) 254 | b.Lock() 255 | defer b.Unlock() 256 | 257 | ele, conflict, ok := c.fetch(b, key) 258 | if ok && !conflict { 259 | b.Delete(ele, ReasonDeleted) 260 | return true 261 | } 262 | 263 | return false 264 | } 265 | 266 | // Range 遍历缓存 267 | // 注意: 不要在回调函数里面操作 MemoryCache[K, V] 实例, 可能会造成死锁. 268 | // Traverse the cache. 269 | // Note: Do not manipulate MemoryCache[K, V] instances inside callback functions, as this may cause deadlocks. 270 | func (c *MemoryCache[K, V]) Range(f func(K, V) bool) { 271 | var now = time.Now().UnixMilli() 272 | for _, b := range c.storage { 273 | b.Lock() 274 | for _, ele := range b.List.elements { 275 | if ele.addr == null || ele.expired(now) { 276 | continue 277 | } 278 | if !f(ele.Key, ele.Value) { 279 | b.Unlock() 280 | return 281 | } 282 | } 283 | b.Unlock() 284 | } 285 | } 286 | 287 | // Len 快速获取当前缓存元素数量, 不做过期检查. 288 | // Quickly gets the current number of cached elements, without checking for expiration. 289 | func (c *MemoryCache[K, V]) Len() int { 290 | var num = 0 291 | for _, b := range c.storage { 292 | b.Lock() 293 | num += b.Heap.Len() 294 | b.Unlock() 295 | } 296 | return num 297 | } 298 | 299 | type ( 300 | bucket[K comparable, V any] struct { 301 | sync.Mutex 302 | conf *config 303 | Map containers.Map[uint64, pointer] 304 | Heap *heap[K, V] 305 | List *deque[K, V] 306 | } 307 | 308 | bucketWrapper[K comparable, V any] struct { 309 | *bucket[K, V] 310 | hashcode uint64 311 | } 312 | ) 313 | 314 | func (c *bucket[K, V]) init() *bucket[K, V] { 315 | c.Map = containers.NewMap[uint64, pointer](c.conf.BucketSize, c.conf.SwissTable) 316 | c.List = newDeque[K, V](c.conf.BucketSize) 317 | c.Heap = newHeap[K, V](c.List, c.conf.BucketSize) 318 | return c 319 | } 320 | 321 | // Check 过期时间检查 322 | func (c *bucket[K, V]) Check(now int64, num int) int { 323 | c.Lock() 324 | defer c.Unlock() 325 | 326 | var sum = 0 327 | for c.Heap.Len() > 0 && c.Heap.Front().expired(now) && sum < num { 328 | c.Delete(c.Heap.Front(), ReasonExpired) 329 | sum++ 330 | } 331 | return sum 332 | } 333 | 334 | func (c *bucket[K, V]) Delete(ele *Element[K, V], reason Reason) { 335 | c.Heap.Delete(ele.index) 336 | c.Map.Delete(ele.hashcode) 337 | ele.cb(ele, reason) 338 | c.List.Remove(ele.addr) // 必须最后删除List, 因为会清空*Element[K, V]数据 339 | } 340 | 341 | func (c *bucket[K, V]) UpdateTTL(ele *Element[K, V], expireAt int64) { 342 | c.Heap.UpdateTTL(ele, expireAt) 343 | c.List.MoveToBack(ele.addr) 344 | } 345 | 346 | func (c *bucket[K, V]) GetElement() *Element[K, V] { 347 | if c.List.Len() >= c.conf.BucketCap { 348 | c.Delete(c.List.Front(), ReasonEvicted) 349 | } 350 | return c.List.PushBack() 351 | } 352 | 353 | func (c *bucket[K, V]) Insert(ele *Element[K, V]) { 354 | c.Heap.Push(ele) 355 | c.Map.Put(ele.hashcode, ele.addr) 356 | } 357 | -------------------------------------------------------------------------------- /cache_test.go: -------------------------------------------------------------------------------- 1 | package memorycache 2 | 3 | import ( 4 | "math/rand" 5 | "sort" 6 | "strconv" 7 | "sync" 8 | "testing" 9 | "time" 10 | 11 | "github.com/lxzan/memorycache/internal/utils" 12 | "github.com/stretchr/testify/assert" 13 | ) 14 | 15 | func getKeys[K comparable, V any](db *MemoryCache[K, V]) []K { 16 | var keys []K 17 | db.Range(func(k K, v V) bool { 18 | keys = append(keys, k) 19 | return true 20 | }) 21 | return keys 22 | } 23 | 24 | func TestMemoryCache(t *testing.T) { 25 | var as = assert.New(t) 26 | 27 | t.Run("", func(t *testing.T) { 28 | var db = New[string, any]( 29 | WithInterval(10*time.Millisecond, 10*time.Millisecond), 30 | WithBucketNum(1), 31 | WithCachedTime(false), 32 | ) 33 | db.Set("a", 1, 100*time.Millisecond) 34 | db.Set("b", 1, 300*time.Millisecond) 35 | db.Set("c", 1, 500*time.Millisecond) 36 | db.Set("d", 1, 700*time.Millisecond) 37 | db.Set("e", 1, 900*time.Millisecond) 38 | db.Set("c", 1, time.Millisecond) 39 | 40 | time.Sleep(200 * time.Millisecond) 41 | var keys = getKeys(db) 42 | as.ElementsMatch(keys, []string{"b", "d", "e"}) 43 | }) 44 | 45 | t.Run("", func(t *testing.T) { 46 | var db = New[string, any]( 47 | WithInterval(10*time.Millisecond, 10*time.Millisecond), 48 | WithCachedTime(false), 49 | ) 50 | db.Set("a", 1, 100*time.Millisecond) 51 | db.Set("b", 1, 200*time.Millisecond) 52 | db.Set("c", 1, 500*time.Millisecond) 53 | db.Set("d", 1, 700*time.Millisecond) 54 | db.Set("e", 1, 2900*time.Millisecond) 55 | db.Set("a", 1, 400*time.Millisecond) 56 | 57 | time.Sleep(300 * time.Millisecond) 58 | var keys = getKeys(db) 59 | as.ElementsMatch(keys, []string{"a", "c", "d", "e"}) 60 | }) 61 | 62 | t.Run("", func(t *testing.T) { 63 | var db = New[string, any]( 64 | WithInterval(10*time.Millisecond, 10*time.Millisecond), 65 | WithCachedTime(false), 66 | ) 67 | db.Set("a", 1, 100*time.Millisecond) 68 | db.Set("b", 1, 200*time.Millisecond) 69 | db.Set("c", 1, 400*time.Millisecond) 70 | db.Set("d", 1, 700*time.Millisecond) 71 | db.Set("d", 1, 400*time.Millisecond) 72 | 73 | time.Sleep(500 * time.Millisecond) 74 | var keys = getKeys(db) 75 | as.Equal(0, len(keys)) 76 | }) 77 | 78 | t.Run("batch", func(t *testing.T) { 79 | var count = 1000 80 | var mc = New[string, any]( 81 | WithInterval(10*time.Millisecond, 10*time.Millisecond), 82 | WithBucketNum(1), 83 | WithCachedTime(false), 84 | ) 85 | var m1 = make(map[string]int) 86 | var m2 = make(map[string]int64) 87 | for i := 0; i < count; i++ { 88 | key := string(utils.AlphabetNumeric.Generate(16)) 89 | exp := time.Duration(rand.Intn(10)+1) * 100 * time.Millisecond 90 | mc.Set(key, i, exp) 91 | m1[key] = i 92 | m2[key] = mc.getExp(exp) 93 | } 94 | 95 | time.Sleep(500 * time.Millisecond) 96 | for k, v := range m1 { 97 | result, ok := mc.Get(k) 98 | if ts := time.Now().UnixMilli(); ts > m2[k] { 99 | if ts-m2[k] >= 10 { 100 | as.False(ok) 101 | } 102 | continue 103 | } 104 | 105 | as.True(ok) 106 | as.Equal(result.(int), v) 107 | } 108 | 109 | var wg = &sync.WaitGroup{} 110 | wg.Add(1) 111 | result, exist := mc.GetOrCreateWithCallback(string(utils.AlphabetNumeric.Generate(16)), "x", 500*time.Millisecond, func(ele *Element[string, any], reason Reason) { 112 | as.Equal(reason, ReasonExpired) 113 | as.Equal(ele.Value.(string), "x") 114 | wg.Done() 115 | }) 116 | as.False(exist) 117 | as.Equal(result.(string), "x") 118 | wg.Wait() 119 | }) 120 | 121 | t.Run("expire", func(t *testing.T) { 122 | var mc = New[string, any]( 123 | WithBucketNum(1), 124 | WithDeleteLimits(3), 125 | WithInterval(50*time.Millisecond, 100*time.Millisecond), 126 | WithCachedTime(false), 127 | ) 128 | mc.Set("a", 1, 150*time.Millisecond) 129 | mc.Set("b", 1, 150*time.Millisecond) 130 | mc.Set("c", 1, 150*time.Millisecond) 131 | time.Sleep(200 * time.Millisecond) 132 | }) 133 | } 134 | 135 | func TestMemoryCache_Set(t *testing.T) { 136 | t.Run("", func(t *testing.T) { 137 | var list []string 138 | var count = 10000 139 | var mc = New[string, any](WithInterval(100*time.Millisecond, 100*time.Millisecond)) 140 | mc.Clear() 141 | for i := 0; i < count; i++ { 142 | key := string(utils.AlphabetNumeric.Generate(8)) 143 | exp := rand.Intn(1000) 144 | if exp == 0 { 145 | list = append(list, key) 146 | } 147 | mc.Set(key, 1, time.Duration(exp)*time.Millisecond) 148 | } 149 | for i := 0; i < count; i++ { 150 | key := string(utils.AlphabetNumeric.Generate(8)) 151 | list = append(list, key) 152 | exp := rand.Intn(1000) + 3000 153 | mc.Set(key, 1, time.Duration(exp)*time.Millisecond) 154 | } 155 | time.Sleep(1100 * time.Millisecond) 156 | var keys = getKeys(mc) 157 | assert.ElementsMatch(t, utils.Uniq(list), keys) 158 | }) 159 | 160 | t.Run("evict", func(t *testing.T) { 161 | var mc = New[string, any]( 162 | WithBucketNum(1), 163 | WithBucketSize(0, 2), 164 | ) 165 | mc.Set("ming", 1, 3*time.Hour) 166 | mc.Set("hong", 1, 1*time.Hour) 167 | mc.Set("feng", 1, 2*time.Hour) 168 | var keys = getKeys(mc) 169 | assert.ElementsMatch(t, keys, []string{"hong", "feng"}) 170 | }) 171 | 172 | t.Run("update ttl", func(t *testing.T) { 173 | var mc = New[string, any](WithBucketNum(1)) 174 | var count = 1000 175 | for i := 0; i < 10*count; i++ { 176 | key := strconv.Itoa(utils.Numeric.Intn(count)) 177 | exp := time.Duration(utils.Numeric.Intn(count)+10) * time.Second 178 | mc.Set(key, 1, exp) 179 | } 180 | 181 | var list1 []int 182 | var list2 []int 183 | for _, b := range mc.storage { 184 | b.Lock() 185 | for _, item := range b.Heap.Data { 186 | ele := b.List.Get(item) 187 | list1 = append(list1, int(ele.ExpireAt)) 188 | } 189 | b.Unlock() 190 | } 191 | sort.Ints(list1) 192 | 193 | for _, b := range mc.storage { 194 | b.Lock() 195 | for b.Heap.Len() > 0 { 196 | ele := b.List.Get(b.Heap.Pop()) 197 | list2 = append(list2, int(ele.ExpireAt)) 198 | } 199 | b.Unlock() 200 | } 201 | 202 | assert.Equal(t, len(list1), len(list2)) 203 | for i, v := range list2 { 204 | assert.Equal(t, list1[i], v) 205 | } 206 | }) 207 | } 208 | 209 | func TestMemoryCache_Get(t *testing.T) { 210 | t.Run("", func(t *testing.T) { 211 | var list0 []string 212 | var list1 []string 213 | var count = 10000 214 | var mc = New[string, any](WithInterval(100*time.Millisecond, 100*time.Millisecond)) 215 | for i := 0; i < count; i++ { 216 | key := string(utils.AlphabetNumeric.Generate(8)) 217 | exp := rand.Intn(1000) 218 | if exp == 0 { 219 | list1 = append(list1, key) 220 | } else { 221 | list0 = append(list0, key) 222 | } 223 | mc.Set(key, 1, time.Duration(exp)*time.Millisecond) 224 | } 225 | for i := 0; i < count; i++ { 226 | key := string(utils.AlphabetNumeric.Generate(8)) 227 | list1 = append(list1, key) 228 | exp := rand.Intn(1000) + 3000 229 | mc.Set(key, 1, time.Duration(exp)*time.Millisecond) 230 | } 231 | time.Sleep(1100 * time.Millisecond) 232 | 233 | for _, item := range list0 { 234 | _, ok := mc.Get(item) 235 | assert.False(t, ok) 236 | } 237 | for _, item := range list1 { 238 | _, ok := mc.Get(item) 239 | assert.True(t, ok) 240 | } 241 | }) 242 | 243 | t.Run("expire", func(t *testing.T) { 244 | var mc = New[string, any]( 245 | WithInterval(10*time.Second, 10*time.Second), 246 | ) 247 | 248 | var wg = &sync.WaitGroup{} 249 | wg.Add(1) 250 | 251 | mc.SetWithCallback("ming", 128, 10*time.Millisecond, func(ele *Element[string, any], reason Reason) { 252 | assert.Equal(t, reason, ReasonExpired) 253 | assert.Equal(t, ele.Value.(int), 128) 254 | wg.Done() 255 | }) 256 | 257 | time.Sleep(2 * time.Second) 258 | v, ok := mc.Get("ming") 259 | assert.False(t, ok) 260 | assert.Nil(t, v) 261 | wg.Wait() 262 | }) 263 | } 264 | 265 | func TestMemoryCache_GetWithTTL(t *testing.T) { 266 | t.Run("", func(t *testing.T) { 267 | var list []string 268 | var count = 10000 269 | var mc = New[string, any](WithInterval(100*time.Millisecond, 100*time.Millisecond)) 270 | for i := 0; i < count; i++ { 271 | key := string(utils.AlphabetNumeric.Generate(8)) 272 | exp := rand.Intn(1000) + 200 273 | list = append(list, key) 274 | mc.Set(key, 1, time.Duration(exp)*time.Millisecond) 275 | } 276 | var keys = getKeys(mc) 277 | for _, key := range keys { 278 | mc.GetWithTTL(key, 2*time.Second) 279 | } 280 | 281 | time.Sleep(1100 * time.Millisecond) 282 | 283 | for _, item := range list { 284 | _, ok := mc.Get(item) 285 | assert.True(t, ok) 286 | } 287 | 288 | mc.Delete(list[0]) 289 | _, ok := mc.GetWithTTL(list[0], -1) 290 | assert.False(t, ok) 291 | }) 292 | 293 | t.Run("update ttl", func(t *testing.T) { 294 | var mc = New[string, any](WithBucketNum(1)) 295 | var count = 1000 296 | for i := 0; i < count; i++ { 297 | key := strconv.Itoa(utils.Numeric.Intn(count)) 298 | exp := time.Duration(utils.Numeric.Intn(count)+10) * time.Second 299 | mc.Set(key, 1, exp) 300 | } 301 | 302 | for i := 0; i < count; i++ { 303 | key := strconv.Itoa(utils.Numeric.Intn(count)) 304 | exp := time.Duration(utils.Numeric.Intn(count)+10) * time.Second 305 | mc.GetWithTTL(key, exp) 306 | } 307 | 308 | var list1 []int 309 | var list2 []int 310 | for _, b := range mc.storage { 311 | b.Lock() 312 | for _, item := range b.Heap.Data { 313 | ele := b.List.Get(item) 314 | list1 = append(list1, int(ele.ExpireAt)) 315 | } 316 | b.Unlock() 317 | } 318 | sort.Ints(list1) 319 | 320 | for _, b := range mc.storage { 321 | b.Lock() 322 | for b.Heap.Len() > 0 { 323 | ele := b.List.Get(b.Heap.Pop()) 324 | list2 = append(list2, int(ele.ExpireAt)) 325 | } 326 | b.Unlock() 327 | } 328 | 329 | assert.Equal(t, len(list1), len(list2)) 330 | for i, v := range list2 { 331 | assert.Equal(t, list1[i], v) 332 | } 333 | }) 334 | } 335 | 336 | func TestMemoryCache_Delete(t *testing.T) { 337 | t.Run("1", func(t *testing.T) { 338 | var count = 10000 339 | var mc = New[string, any](WithInterval(100*time.Millisecond, 100*time.Millisecond)) 340 | for i := 0; i < count; i++ { 341 | key := string(utils.AlphabetNumeric.Generate(8)) 342 | exp := rand.Intn(1000) + 200 343 | mc.Set(key, 1, time.Duration(exp)*time.Millisecond) 344 | } 345 | 346 | var keys = getKeys(mc) 347 | for i := 0; i < 100; i++ { 348 | deleted := mc.Delete(keys[i]) 349 | assert.True(t, deleted) 350 | 351 | key := string(utils.AlphabetNumeric.Generate(8)) 352 | deleted = mc.Delete(key) 353 | assert.False(t, deleted) 354 | } 355 | assert.Equal(t, mc.Len(), count-100) 356 | }) 357 | 358 | t.Run("2", func(t *testing.T) { 359 | var mc = New[string, any]() 360 | var wg = &sync.WaitGroup{} 361 | wg.Add(1) 362 | mc.SetWithCallback("ming", 1, -1, func(ele *Element[string, any], reason Reason) { 363 | assert.Equal(t, reason, ReasonDeleted) 364 | wg.Done() 365 | }) 366 | mc.SetWithCallback("ting", 2, -1, func(ele *Element[string, any], reason Reason) { 367 | wg.Done() 368 | }) 369 | go mc.Delete("ming") 370 | wg.Wait() 371 | }) 372 | 373 | t.Run("3", func(t *testing.T) { 374 | var mc = New[string, any]() 375 | var wg = &sync.WaitGroup{} 376 | wg.Add(1) 377 | mc.GetOrCreateWithCallback("ming", 1, -1, func(ele *Element[string, any], reason Reason) { 378 | assert.Equal(t, reason, ReasonDeleted) 379 | wg.Done() 380 | }) 381 | mc.GetOrCreateWithCallback("ting", 2, -1, func(ele *Element[string, any], reason Reason) { 382 | wg.Done() 383 | }) 384 | go mc.Delete("ting") 385 | wg.Wait() 386 | }) 387 | 388 | t.Run("batch delete", func(t *testing.T) { 389 | var mc = New[string, any](WithBucketNum(1)) 390 | var count = 1000 391 | for i := 0; i < count; i++ { 392 | key := strconv.Itoa(utils.Numeric.Intn(count)) 393 | exp := time.Duration(utils.Numeric.Intn(count)+10) * time.Second 394 | mc.Set(key, 1, exp) 395 | } 396 | for i := 0; i < count/2; i++ { 397 | key := strconv.Itoa(utils.Numeric.Intn(count)) 398 | mc.Delete(key) 399 | } 400 | 401 | var list1 []int 402 | var list2 []int 403 | for _, b := range mc.storage { 404 | b.Lock() 405 | for _, item := range b.Heap.Data { 406 | ele := b.List.Get(item) 407 | list1 = append(list1, int(ele.ExpireAt)) 408 | } 409 | b.Unlock() 410 | } 411 | sort.Ints(list1) 412 | 413 | for _, b := range mc.storage { 414 | b.Lock() 415 | for b.Heap.Len() > 0 { 416 | ele := b.List.Get(b.Heap.Pop()) 417 | list2 = append(list2, int(ele.ExpireAt)) 418 | } 419 | b.Unlock() 420 | } 421 | 422 | assert.Equal(t, len(list1), len(list2)) 423 | for i, v := range list2 { 424 | assert.Equal(t, list1[i], v) 425 | } 426 | }) 427 | } 428 | 429 | func TestMaxCap(t *testing.T) { 430 | var mc = New[string, any]( 431 | WithBucketNum(1), 432 | WithBucketSize(10, 100), 433 | WithInterval(100*time.Millisecond, 100*time.Millisecond), 434 | ) 435 | 436 | var wg = &sync.WaitGroup{} 437 | wg.Add(900) 438 | for i := 0; i < 1000; i++ { 439 | key := string(utils.AlphabetNumeric.Generate(16)) 440 | mc.SetWithCallback(key, 1, -1, func(ele *Element[string, any], reason Reason) { 441 | assert.Equal(t, reason, ReasonEvicted) 442 | wg.Done() 443 | }) 444 | } 445 | time.Sleep(200 * time.Millisecond) 446 | assert.Equal(t, mc.Len(), 100) 447 | wg.Wait() 448 | } 449 | 450 | func TestMemoryCache_SetWithCallback(t *testing.T) { 451 | var as = assert.New(t) 452 | var count = 1000 453 | var mc = New[string, any]( 454 | WithBucketNum(16), 455 | WithInterval(10*time.Millisecond, 100*time.Millisecond), 456 | ) 457 | defer mc.Clear() 458 | 459 | var wg = &sync.WaitGroup{} 460 | wg.Add(count) 461 | for i := 0; i < count; i++ { 462 | key := string(utils.AlphabetNumeric.Generate(16)) 463 | exp := time.Duration(rand.Intn(1000)+10) * time.Millisecond 464 | mc.SetWithCallback(key, i, exp, func(ele *Element[string, any], reason Reason) { 465 | as.True(time.Now().UnixMilli() > ele.ExpireAt) 466 | as.Equal(reason, ReasonExpired) 467 | wg.Done() 468 | }) 469 | } 470 | wg.Wait() 471 | } 472 | 473 | func TestMemoryCache_GetOrCreate(t *testing.T) { 474 | 475 | var count = 1000 476 | var mc = New[string, any]( 477 | WithBucketNum(16), 478 | WithInterval(10*time.Millisecond, 100*time.Millisecond), 479 | ) 480 | defer mc.Clear() 481 | 482 | for i := 0; i < count; i++ { 483 | key := string(utils.AlphabetNumeric.Generate(16)) 484 | exp := time.Duration(rand.Intn(1000)+10) * time.Millisecond 485 | mc.GetOrCreate(key, i, exp) 486 | } 487 | } 488 | 489 | func TestMemoryCache_GetOrCreateWithCallback(t *testing.T) { 490 | var as = assert.New(t) 491 | 492 | t.Run("", func(t *testing.T) { 493 | var count = 1000 494 | var mc = New[string, any]( 495 | WithBucketNum(16), 496 | WithInterval(10*time.Millisecond, 100*time.Millisecond), 497 | ) 498 | defer mc.Clear() 499 | 500 | var wg = &sync.WaitGroup{} 501 | wg.Add(count) 502 | for i := 0; i < count; i++ { 503 | key := string(utils.AlphabetNumeric.Generate(16)) 504 | exp := time.Duration(rand.Intn(1000)+10) * time.Millisecond 505 | mc.GetOrCreateWithCallback(key, i, exp, func(ele *Element[string, any], reason Reason) { 506 | as.True(time.Now().UnixMilli() > ele.ExpireAt) 507 | as.Equal(reason, ReasonExpired) 508 | wg.Done() 509 | }) 510 | } 511 | wg.Wait() 512 | }) 513 | 514 | t.Run("exists", func(t *testing.T) { 515 | var mc = New[string, any]() 516 | mc.Set("ming", 1, -1) 517 | v, exist := mc.GetOrCreateWithCallback("ming", 2, time.Second, func(ele *Element[string, any], reason Reason) {}) 518 | as.True(exist) 519 | as.Equal(v.(int), 1) 520 | }) 521 | 522 | t.Run("create", func(t *testing.T) { 523 | var mc = New[string, any]( 524 | WithBucketNum(1), 525 | WithBucketSize(0, 1), 526 | ) 527 | mc.Set("ming", 1, -1) 528 | v, exist := mc.GetOrCreateWithCallback("wang", 2, time.Second, func(ele *Element[string, any], reason Reason) {}) 529 | as.False(exist) 530 | as.Equal(v.(int), 2) 531 | as.Equal(mc.Len(), 1) 532 | }) 533 | } 534 | 535 | func TestMemoryCache_Stop(t *testing.T) { 536 | var mc = New[string, any]() 537 | mc.Stop() 538 | mc.Stop() 539 | 540 | select { 541 | case <-mc.ctx.Done(): 542 | default: 543 | t.Fail() 544 | } 545 | } 546 | 547 | func TestMemoryCache_Range(t *testing.T) { 548 | const count = 1000 549 | var mc = New[string, int]() 550 | for i := 0; i < count; i++ { 551 | var key = string(utils.AlphabetNumeric.Generate(16)) 552 | mc.Set(key, 1, time.Hour) 553 | } 554 | 555 | t.Run("", func(t *testing.T) { 556 | var keys []string 557 | mc.Range(func(s string, i int) bool { 558 | keys = append(keys, s) 559 | return true 560 | }) 561 | assert.Equal(t, len(keys), count) 562 | }) 563 | 564 | t.Run("", func(t *testing.T) { 565 | var keys []string 566 | mc.Range(func(s string, i int) bool { 567 | keys = append(keys, s) 568 | return len(keys) < 500 569 | }) 570 | assert.Equal(t, len(keys), 500) 571 | }) 572 | 573 | t.Run("", func(t *testing.T) { 574 | mc.Set("exp", 1, time.Millisecond) 575 | time.Sleep(100 * time.Millisecond) 576 | var keys []string 577 | mc.Range(func(s string, i int) bool { 578 | keys = append(keys, s) 579 | return true 580 | }) 581 | assert.Equal(t, len(keys), count) 582 | }) 583 | } 584 | 585 | func TestMemoryCache_LRU(t *testing.T) { 586 | const count = 10000 587 | var mc = New[string, int]( 588 | WithBucketNum(1), 589 | ) 590 | var indexes []int 591 | for i := 0; i < count; i++ { 592 | indexes = append(indexes, i) 593 | mc.Set(strconv.Itoa(i), 1, time.Hour) 594 | } 595 | for i := 0; i < count; i++ { 596 | a, b := utils.AlphabetNumeric.Intn(count), utils.AlphabetNumeric.Intn(count) 597 | indexes[a], indexes[b] = indexes[b], indexes[a] 598 | } 599 | for _, item := range indexes { 600 | key := strconv.Itoa(item) 601 | mc.Get(key) 602 | } 603 | 604 | var keys []string 605 | var q = mc.storage[0].List 606 | for q.Len() > 0 { 607 | keys = append(keys, q.PopFront().Key) 608 | } 609 | for i, item := range indexes { 610 | key := strconv.Itoa(item) 611 | assert.Equal(t, key, keys[i]) 612 | } 613 | } 614 | 615 | func TestMemoryCache_Conflict(t *testing.T) { 616 | t.Run("", func(t *testing.T) { 617 | var mc = New[string, any]() 618 | var wg = &sync.WaitGroup{} 619 | wg.Add(1) 620 | mc.hasher = new(utils.Fnv32Hasher) 621 | mc.SetWithCallback("O4XOUsgCQqkVCvLQ", 1, time.Hour, func(element *Element[string, any], reason Reason) { 622 | assert.Equal(t, element.Value, 1) 623 | wg.Done() 624 | }) 625 | assert.False(t, mc.Set("wYLAGPVADrDTi7VT", 2, time.Hour)) 626 | assert.True(t, mc.Set("wYLAGPVADrDTi7VT", 2, time.Hour)) 627 | 628 | v1, ok1 := mc.Get("O4XOUsgCQqkVCvLQ") 629 | assert.False(t, ok1) 630 | assert.Nil(t, v1) 631 | 632 | v2, ok2 := mc.Get("wYLAGPVADrDTi7VT") 633 | assert.True(t, ok2) 634 | assert.Equal(t, v2, 2) 635 | assert.Equal(t, mc.Len(), 1) 636 | wg.Wait() 637 | }) 638 | 639 | t.Run("", func(t *testing.T) { 640 | var mc = New[string, any]() 641 | var wg = &sync.WaitGroup{} 642 | wg.Add(1) 643 | mc.hasher = new(utils.Fnv32Hasher) 644 | mc.SetWithCallback("O4XOUsgCQqkVCvLQ", 1, time.Hour, func(element *Element[string, any], reason Reason) { 645 | assert.Equal(t, element.Value, 1) 646 | wg.Done() 647 | }) 648 | 649 | v1, ok1 := mc.Get("O4XOUsgCQqkVCvLQ") 650 | assert.True(t, ok1) 651 | assert.Equal(t, v1, 1) 652 | 653 | v2, ok2 := mc.GetOrCreate("wYLAGPVADrDTi7VT", 2, time.Hour) 654 | assert.False(t, ok2) 655 | assert.Equal(t, v2, 2) 656 | 657 | v3, ok3 := mc.GetOrCreate("wYLAGPVADrDTi7VT", 3, time.Hour) 658 | assert.True(t, ok3) 659 | assert.Equal(t, v3, 2) 660 | assert.Equal(t, mc.Len(), 1) 661 | 662 | wg.Wait() 663 | }) 664 | } 665 | 666 | func TestMemoryCache_Random(t *testing.T) { 667 | t.Run("with lru", func(t *testing.T) { 668 | const count = 10000 669 | var mc = New[string, int]( 670 | WithBucketNum(16), 671 | WithBucketSize(100, 625), 672 | ) 673 | for i := 0; i < count; i++ { 674 | var key = string(utils.AlphabetNumeric.Generate(3)) 675 | var val = utils.AlphabetNumeric.Intn(count) 676 | mc.Set(key, val, time.Hour) 677 | } 678 | 679 | for i := 0; i < count; i++ { 680 | var key = string(utils.AlphabetNumeric.Generate(3)) 681 | var val = utils.AlphabetNumeric.Intn(count) 682 | switch utils.AlphabetNumeric.Intn(8) { 683 | case 0, 1: 684 | mc.Set(key, val, time.Hour) 685 | case 2: 686 | mc.SetWithCallback(key, val, time.Hour, func(entry *Element[string, int], reason Reason) {}) 687 | case 3: 688 | mc.Get(key) 689 | case 4: 690 | mc.GetWithTTL(key, time.Hour) 691 | case 5: 692 | mc.GetOrCreate(key, val, time.Hour) 693 | case 6: 694 | mc.GetOrCreateWithCallback(key, val, time.Hour, func(entry *Element[string, int], reason Reason) {}) 695 | case 7: 696 | mc.Delete(key) 697 | } 698 | } 699 | 700 | for _, b := range mc.storage { 701 | assert.Equal(t, b.Map.Count(), b.Heap.Len()) 702 | assert.Equal(t, b.Heap.Len(), b.List.Len()) 703 | b.List.Range(func(ele *Element[string, int]) bool { 704 | var v = ele 705 | var v1 = b.Heap.Data[v.index] 706 | assert.Equal(t, v.addr, v1) 707 | 708 | var v2, _ = b.Map.Get(v.hashcode) 709 | assert.Equal(t, v.addr, v2) 710 | return true 711 | }) 712 | assert.True(t, isSorted(b.Heap)) 713 | } 714 | }) 715 | } 716 | --------------------------------------------------------------------------------