├── 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 |

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 |

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 |
--------------------------------------------------------------------------------