Skip to content

Commit db93db0

Browse files
authored
Merge pull request #1 from lxzan/dev
v1.1.1
2 parents 232c37a + f674336 commit db93db0

File tree

13 files changed

+185
-64
lines changed

13 files changed

+185
-64
lines changed

.github/workflows/go.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ jobs:
1919
- name: Set up Go
2020
uses: actions/setup-go@v3
2121
with:
22-
go-version: 1.18
22+
go-version: 1.19
2323

2424
- name: Test
2525
run: go test -v ./...

.gitignore

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,5 @@
22
.vscode/
33
vendor/
44
examples/
5-
bin/
5+
bin/
6+
go.work

README.md

Lines changed: 15 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,17 @@
1111
[4]: https://codecov.io/gh/lxzan/memorycache
1212

1313
### Description
14-
Minimalist in-memory KV storage, powered by hashmap and minimal heap, with no special optimizations for GC.
14+
Minimalist in-memory KV storage, powered by hashmap and minimal heap, without optimizations for GC.
1515
It has O(1) read efficiency, O(logN) write efficiency.
16-
Cache deprecation policy: obsolete or overflowed keys are flushed, with a 30s (default) check.
16+
Cache deprecation policy: the set method cleans up overflowed keys; the cycle cleans up expired keys.
17+
18+
### Principle
19+
- Storage Data Limit: Limited by maximum capacity
20+
- Expiration Time: Supported
21+
- Cache Elimination Policy: LRU-Like, Set method and Cycle Cleanup
22+
- GC Optimization: None
23+
- Persistent: None
24+
- Locking Mechanism: Slicing + Mutual Exclusion Locking
1725

1826
### Usage
1927
```go
@@ -45,17 +53,16 @@ func main() {
4553
```
4654

4755
### Benchmark
48-
- 10,000 elements
4956
- 1,000,000 elements
5057
```
5158
go test -benchmem -run=^$ -bench . github.com/lxzan/memorycache/benchmark
5259
goos: darwin
5360
goarch: arm64
5461
pkg: github.com/lxzan/memorycache/benchmark
55-
BenchmarkSet/10000-8 13830640 87.25 ns/op 0 B/op 0 allocs/op
56-
BenchmarkSet/1000000-8 3615801 326.6 ns/op 58 B/op 0 allocs/op
57-
BenchmarkGet/10000-8 14347058 82.28 ns/op 0 B/op 0 allocs/op
58-
BenchmarkGet/1000000-8 3899768 262.6 ns/op 54 B/op 0 allocs/op
62+
BenchmarkMemoryCache_Set-8 7038808 153.9 ns/op 26 B/op 0 allocs/op
63+
BenchmarkMemoryCache_Get-8 22969712 50.92 ns/op 0 B/op 0 allocs/op
64+
BenchmarkRistretto_Set-8 13417420 242.9 ns/op 138 B/op 2 allocs/op
65+
BenchmarkRistretto_Get-8 15895714 75.81 ns/op 18 B/op 1 allocs/op
5966
PASS
60-
ok github.com/lxzan/memorycache/benchmark 13.037s
67+
ok github.com/lxzan/memorycache/benchmark 10.849s
6168
```

benchmark/benchmark_test.go

Lines changed: 59 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
package benchmark
22

33
import (
4+
"github.com/dgraph-io/ristretto"
45
"github.com/lxzan/memorycache"
56
"github.com/lxzan/memorycache/internal/utils"
7+
"sync/atomic"
68
"testing"
79
"time"
810
)
@@ -17,33 +19,70 @@ func init() {
1719
}
1820
}
1921

20-
func BenchmarkSet(b *testing.B) {
21-
var f = func(n, count int) {
22-
var mc = memorycache.New(memorycache.WithBucketNum(16))
23-
for i := 0; i < n; i++ {
24-
var key = benchkeys[i%count]
25-
mc.Set(key, 1, time.Hour)
22+
func BenchmarkMemoryCache_Set(b *testing.B) {
23+
var mc = memorycache.New(
24+
memorycache.WithBucketNum(128),
25+
memorycache.WithBucketSize(1000, 10000),
26+
)
27+
var i = int64(0)
28+
b.RunParallel(func(pb *testing.PB) {
29+
for pb.Next() {
30+
index := atomic.AddInt64(&i, 1) % benchcount
31+
mc.Set(benchkeys[index], 1, time.Hour)
2632
}
33+
})
34+
}
35+
36+
func BenchmarkMemoryCache_Get(b *testing.B) {
37+
var mc = memorycache.New(
38+
memorycache.WithBucketNum(16),
39+
memorycache.WithBucketSize(100, 1000),
40+
)
41+
for i := 0; i < benchcount; i++ {
42+
mc.Set(benchkeys[i%benchcount], 1, time.Hour)
2743
}
2844

29-
b.Run("10000", func(b *testing.B) { f(b.N, 10000) })
30-
b.Run("1000000", func(b *testing.B) { f(b.N, 1000000) })
45+
var i = int64(0)
46+
b.ResetTimer()
47+
b.RunParallel(func(pb *testing.PB) {
48+
for pb.Next() {
49+
index := atomic.AddInt64(&i, 1) % benchcount
50+
mc.Get(benchkeys[index])
51+
}
52+
})
3153
}
3254

33-
func BenchmarkGet(b *testing.B) {
34-
var f = func(n, count int) {
35-
var mc = memorycache.New(memorycache.WithBucketNum(16))
36-
for i := 0; i < count; i++ {
37-
var key = benchkeys[i]
38-
mc.Set(key, 1, time.Hour)
55+
func BenchmarkRistretto_Set(b *testing.B) {
56+
var mc, _ = ristretto.NewCache(&ristretto.Config{
57+
NumCounters: 1e7, // number of keys to track frequency of (10M).
58+
MaxCost: 1 << 30, // maximum cost of cache (1GB).
59+
BufferItems: 64, // number of keys per Get buffer.
60+
})
61+
var i = int64(0)
62+
b.RunParallel(func(pb *testing.PB) {
63+
for pb.Next() {
64+
index := atomic.AddInt64(&i, 1) % benchcount
65+
mc.SetWithTTL(benchkeys[index], 1, 1, time.Hour)
3966
}
67+
})
68+
}
4069

41-
for i := 0; i < n; i++ {
42-
var key = benchkeys[i%count]
43-
mc.Get(key)
44-
}
70+
func BenchmarkRistretto_Get(b *testing.B) {
71+
var mc, _ = ristretto.NewCache(&ristretto.Config{
72+
NumCounters: 1e7, // number of keys to track frequency of (10M).
73+
MaxCost: 1 << 30, // maximum cost of cache (1GB).
74+
BufferItems: 64, // number of keys per Get buffer.
75+
})
76+
for i := 0; i < benchcount; i++ {
77+
mc.SetWithTTL(benchkeys[i%benchcount], 1, 1, time.Hour)
4578
}
4679

47-
b.Run("10000", func(b *testing.B) { f(b.N, 10000) })
48-
b.Run("1000000", func(b *testing.B) { f(b.N, 1000000) })
80+
var i = int64(0)
81+
b.ResetTimer()
82+
b.RunParallel(func(pb *testing.PB) {
83+
for pb.Next() {
84+
index := atomic.AddInt64(&i, 1) % benchcount
85+
mc.Get(benchkeys[index])
86+
}
87+
})
4988
}

benchmark/go.mod

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
module github.com/lxzan/memorycache/benchmark
2+
3+
go 1.19
4+
5+
require (
6+
github.com/dgraph-io/ristretto v0.1.1
7+
github.com/lxzan/memorycache v1.0.0
8+
)
9+
10+
require (
11+
github.com/cespare/xxhash/v2 v2.1.2 // indirect
12+
github.com/dustin/go-humanize v1.0.0 // indirect
13+
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b // indirect
14+
github.com/pkg/errors v0.9.1 // indirect
15+
golang.org/x/sys v0.0.0-20221010170243-090e33056c14 // indirect
16+
)
17+
18+
replace github.com/lxzan/memorycache v1.0.0 => ../

benchmark/go.sum

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
2+
github.com/cespare/xxhash/v2 v2.1.2 h1:YRXhKfTDauu4ajMg1TPgFO5jnlC2HCbmLXMcTG5cbYE=
3+
github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
4+
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
5+
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
6+
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
7+
github.com/dgraph-io/ristretto v0.1.1 h1:6CWw5tJNgpegArSHpNHJKldNeq03FQCwYvfMVWajOK8=
8+
github.com/dgraph-io/ristretto v0.1.1/go.mod h1:S1GPSBCYCIhmVNfcth17y2zZtQT6wzkzgwUve0VDWWA=
9+
github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2 h1:tdlZCpZ/P9DhczCTSixgIKmwPv6+wP5DGjqLYw5SUiA=
10+
github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw=
11+
github.com/dustin/go-humanize v1.0.0 h1:VSnTsYCnlFHaM2/igO1h6X3HA71jcobQuxemgkq4zYo=
12+
github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
13+
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b h1:VKtxabqXZkF25pY9ekfRL6a582T4P37/31XEstQ5p58=
14+
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
15+
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
16+
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
17+
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
18+
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
19+
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
20+
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
21+
github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk=
22+
golang.org/x/sys v0.0.0-20221010170243-090e33056c14 h1:k5II8e6QD8mITdi+okbbmR/cIyEbeXLBhy5Ha4nevyc=
23+
golang.org/x/sys v0.0.0-20221010170243-090e33056c14/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
24+
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
25+
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
26+
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=

go.mod

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
module github.com/lxzan/memorycache
22

3-
go 1.18
3+
go 1.19
44

55
require github.com/stretchr/testify v1.8.1
66

index.go

Lines changed: 15 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ package memorycache
33
import (
44
"github.com/lxzan/memorycache/internal/heap"
55
"github.com/lxzan/memorycache/internal/types"
6-
"github.com/lxzan/memorycache/internal/utils"
6+
"hash/maphash"
77
"math"
88
"strings"
99
"sync"
@@ -13,6 +13,7 @@ import (
1313
type MemoryCache struct {
1414
config *types.Config
1515
storage []*bucket
16+
seed maphash.Seed
1617
}
1718

1819
// New 创建缓存数据库实例
@@ -27,6 +28,7 @@ func New(options ...Option) *MemoryCache {
2728
mc := &MemoryCache{
2829
config: config,
2930
storage: make([]*bucket, config.BucketNum),
31+
seed: maphash.MakeSeed(),
3032
}
3133

3234
for i, _ := range mc.storage {
@@ -41,8 +43,10 @@ func New(options ...Option) *MemoryCache {
4143
defer ticker.Stop()
4244
for {
4345
<-ticker.C
44-
for _, bucket := range mc.storage {
45-
bucket.expireTimeCheck(config.MaxKeysDeleted, config.MaxCapacity)
46+
47+
var now = time.Now().UnixMilli()
48+
for _, b := range mc.storage {
49+
b.expireTimeCheck(now, config.MaxKeysDeleted)
4650
}
4751
}
4852
}()
@@ -51,7 +55,7 @@ func New(options ...Option) *MemoryCache {
5155
}
5256

5357
func (c *MemoryCache) getBucket(key string) *bucket {
54-
var idx = utils.Fnv32(key) & (c.config.BucketNum - 1)
58+
var idx = maphash.String(c.seed, key) & uint64(c.config.BucketNum-1)
5559
return c.storage[idx]
5660
}
5761

@@ -79,14 +83,12 @@ func (c *MemoryCache) Set(key string, value any, exp time.Duration) (replaced bo
7983
return true
8084
}
8185

82-
var ele = &types.Element{
83-
Key: key,
84-
Value: value,
85-
ExpireAt: expireAt,
86-
}
87-
86+
var ele = &types.Element{Key: key, Value: value, ExpireAt: expireAt}
8887
b.Heap.Push(ele)
8988
b.Map[key] = ele
89+
if b.Heap.Len() > c.config.MaxCapacity {
90+
delete(b.Map, b.Heap.Pop().Key)
91+
}
9092
return false
9193
}
9294

@@ -181,18 +183,12 @@ type bucket struct {
181183
}
182184

183185
// 过期时间检查
184-
func (c *bucket) expireTimeCheck(maxNum int, maxCap int) {
186+
func (c *bucket) expireTimeCheck(now int64, num int) {
185187
c.Lock()
186188
defer c.Unlock()
187189

188-
var now = time.Now().UnixMilli()
189-
var num = 0
190-
for c.Heap.Len() > 0 && c.Heap.Front().Expired(now) && num < maxNum {
191-
delete(c.Map, c.Heap.Pop().Key)
192-
num++
193-
}
194-
for c.Heap.Len() > maxCap && num < maxNum {
190+
for c.Heap.Len() > 0 && c.Heap.Front().Expired(now) && num > 0 {
195191
delete(c.Map, c.Heap.Pop().Key)
196-
num++
192+
num--
197193
}
198194
}

internal/types/types.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ func (c *Element) Expired(now int64) bool {
2222

2323
type Config struct {
2424
Interval time.Duration // 检查周期
25-
BucketNum uint32 // 存储桶数量
25+
BucketNum int // 存储桶数量
2626
MaxKeysDeleted int // 每次检查至多删除key的数量(单个存储桶)
2727
InitialSize int // 初始化大小(单个存储桶)
2828
MaxCapacity int // 最大容量(单个存储桶)

internal/utils/hash.go

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,15 @@
11
package utils
22

33
const (
4-
offset32 = 2166136261
5-
prime32 = 16777619
4+
prime64 = 1099511628211
5+
offset64 = 14695981039346656037
66
)
77

8-
// Fnv32 returns a new 32-bit FNV-1 hash.Hash.
9-
func Fnv32(s string) uint32 {
10-
var hash uint32 = offset32
8+
func Fnv64(s string) uint64 {
9+
var hash uint64 = offset64
1110
for _, c := range s {
12-
hash *= prime32
13-
hash ^= uint32(c)
11+
hash *= prime64
12+
hash ^= uint64(c)
1413
}
1514
return hash
1615
}

internal/utils/helper_test.go

Lines changed: 38 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,15 +3,50 @@ package utils
33
import (
44
"github.com/stretchr/testify/assert"
55
"hash/fnv"
6+
"hash/maphash"
67
"testing"
78
)
89

9-
func TestNewFnv32(t *testing.T) {
10+
func BenchmarkHash_Fnv64(b *testing.B) {
11+
b.Run("16", func(b *testing.B) {
12+
key := string(AlphabetNumeric.Generate(16))
13+
for i := 0; i < b.N; i++ {
14+
Fnv64(key)
15+
}
16+
})
17+
18+
b.Run("32", func(b *testing.B) {
19+
key := string(AlphabetNumeric.Generate(32))
20+
for i := 0; i < b.N; i++ {
21+
Fnv64(key)
22+
}
23+
})
24+
}
25+
26+
func BenchmarkHash_MapHash(b *testing.B) {
27+
seed := maphash.MakeSeed()
28+
29+
b.Run("16", func(b *testing.B) {
30+
key := string(AlphabetNumeric.Generate(16))
31+
for i := 0; i < b.N; i++ {
32+
maphash.String(seed, key)
33+
}
34+
})
35+
36+
b.Run("32", func(b *testing.B) {
37+
key := string(AlphabetNumeric.Generate(32))
38+
for i := 0; i < b.N; i++ {
39+
maphash.String(seed, key)
40+
}
41+
})
42+
}
43+
44+
func TestNewFnv64(t *testing.T) {
1045
for i := 0; i < 10; i++ {
1146
key := AlphabetNumeric.Generate(16)
12-
h := fnv.New32()
47+
h := fnv.New64()
1348
h.Write(key)
14-
assert.Equal(t, h.Sum32(), Fnv32(string(key)))
49+
assert.Equal(t, h.Sum64(), Fnv64(string(key)))
1550
}
1651
}
1752

0 commit comments

Comments
 (0)