Skip to content

Commit

Permalink
Merge pull request #1 from lxzan/dev
Browse files Browse the repository at this point in the history
v1.1.1
  • Loading branch information
lxzan authored Oct 28, 2023
2 parents 232c37a + f674336 commit db93db0
Show file tree
Hide file tree
Showing 13 changed files with 185 additions and 64 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/go.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ jobs:
- name: Set up Go
uses: actions/setup-go@v3
with:
go-version: 1.18
go-version: 1.19

- name: Test
run: go test -v ./...
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,5 @@
.vscode/
vendor/
examples/
bin/
bin/
go.work
23 changes: 15 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,17 @@
[4]: https://codecov.io/gh/lxzan/memorycache

### Description
Minimalist in-memory KV storage, powered by hashmap and minimal heap, with no special optimizations for GC.
Minimalist in-memory KV storage, powered by hashmap and minimal heap, without optimizations for GC.
It has O(1) read efficiency, O(logN) write efficiency.
Cache deprecation policy: obsolete or overflowed keys are flushed, with a 30s (default) check.
Cache deprecation policy: the set method cleans up overflowed keys; the cycle cleans up expired keys.

### Principle
- Storage Data Limit: Limited by maximum capacity
- Expiration Time: Supported
- Cache Elimination Policy: LRU-Like, Set method and Cycle Cleanup
- GC Optimization: None
- Persistent: None
- Locking Mechanism: Slicing + Mutual Exclusion Locking

### Usage
```go
Expand Down Expand Up @@ -45,17 +53,16 @@ func main() {
```

### Benchmark
- 10,000 elements
- 1,000,000 elements
```
go test -benchmem -run=^$ -bench . github.com/lxzan/memorycache/benchmark
goos: darwin
goarch: arm64
pkg: github.com/lxzan/memorycache/benchmark
BenchmarkSet/10000-8 13830640 87.25 ns/op 0 B/op 0 allocs/op
BenchmarkSet/1000000-8 3615801 326.6 ns/op 58 B/op 0 allocs/op
BenchmarkGet/10000-8 14347058 82.28 ns/op 0 B/op 0 allocs/op
BenchmarkGet/1000000-8 3899768 262.6 ns/op 54 B/op 0 allocs/op
BenchmarkMemoryCache_Set-8 7038808 153.9 ns/op 26 B/op 0 allocs/op
BenchmarkMemoryCache_Get-8 22969712 50.92 ns/op 0 B/op 0 allocs/op
BenchmarkRistretto_Set-8 13417420 242.9 ns/op 138 B/op 2 allocs/op
BenchmarkRistretto_Get-8 15895714 75.81 ns/op 18 B/op 1 allocs/op
PASS
ok github.com/lxzan/memorycache/benchmark 13.037s
ok github.com/lxzan/memorycache/benchmark 10.849s
```
79 changes: 59 additions & 20 deletions benchmark/benchmark_test.go
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
package benchmark

import (
"github.com/dgraph-io/ristretto"
"github.com/lxzan/memorycache"
"github.com/lxzan/memorycache/internal/utils"
"sync/atomic"
"testing"
"time"
)
Expand All @@ -17,33 +19,70 @@ func init() {
}
}

func BenchmarkSet(b *testing.B) {
var f = func(n, count int) {
var mc = memorycache.New(memorycache.WithBucketNum(16))
for i := 0; i < n; i++ {
var key = benchkeys[i%count]
mc.Set(key, 1, time.Hour)
func BenchmarkMemoryCache_Set(b *testing.B) {
var mc = memorycache.New(
memorycache.WithBucketNum(128),
memorycache.WithBucketSize(1000, 10000),
)
var i = int64(0)
b.RunParallel(func(pb *testing.PB) {
for pb.Next() {
index := atomic.AddInt64(&i, 1) % benchcount
mc.Set(benchkeys[index], 1, time.Hour)
}
})
}

func BenchmarkMemoryCache_Get(b *testing.B) {
var mc = memorycache.New(
memorycache.WithBucketNum(16),
memorycache.WithBucketSize(100, 1000),
)
for i := 0; i < benchcount; i++ {
mc.Set(benchkeys[i%benchcount], 1, time.Hour)
}

b.Run("10000", func(b *testing.B) { f(b.N, 10000) })
b.Run("1000000", func(b *testing.B) { f(b.N, 1000000) })
var i = int64(0)
b.ResetTimer()
b.RunParallel(func(pb *testing.PB) {
for pb.Next() {
index := atomic.AddInt64(&i, 1) % benchcount
mc.Get(benchkeys[index])
}
})
}

func BenchmarkGet(b *testing.B) {
var f = func(n, count int) {
var mc = memorycache.New(memorycache.WithBucketNum(16))
for i := 0; i < count; i++ {
var key = benchkeys[i]
mc.Set(key, 1, time.Hour)
func BenchmarkRistretto_Set(b *testing.B) {
var mc, _ = ristretto.NewCache(&ristretto.Config{
NumCounters: 1e7, // number of keys to track frequency of (10M).
MaxCost: 1 << 30, // maximum cost of cache (1GB).
BufferItems: 64, // number of keys per Get buffer.
})
var i = int64(0)
b.RunParallel(func(pb *testing.PB) {
for pb.Next() {
index := atomic.AddInt64(&i, 1) % benchcount
mc.SetWithTTL(benchkeys[index], 1, 1, time.Hour)
}
})
}

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

b.Run("10000", func(b *testing.B) { f(b.N, 10000) })
b.Run("1000000", func(b *testing.B) { f(b.N, 1000000) })
var i = int64(0)
b.ResetTimer()
b.RunParallel(func(pb *testing.PB) {
for pb.Next() {
index := atomic.AddInt64(&i, 1) % benchcount
mc.Get(benchkeys[index])
}
})
}
18 changes: 18 additions & 0 deletions benchmark/go.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
module github.com/lxzan/memorycache/benchmark

go 1.19

require (
github.com/dgraph-io/ristretto v0.1.1
github.com/lxzan/memorycache v1.0.0
)

require (
github.com/cespare/xxhash/v2 v2.1.2 // indirect
github.com/dustin/go-humanize v1.0.0 // indirect
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b // indirect
github.com/pkg/errors v0.9.1 // indirect
golang.org/x/sys v0.0.0-20221010170243-090e33056c14 // indirect
)

replace github.com/lxzan/memorycache v1.0.0 => ../
26 changes: 26 additions & 0 deletions benchmark/go.sum
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/cespare/xxhash/v2 v2.1.2 h1:YRXhKfTDauu4ajMg1TPgFO5jnlC2HCbmLXMcTG5cbYE=
github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dgraph-io/ristretto v0.1.1 h1:6CWw5tJNgpegArSHpNHJKldNeq03FQCwYvfMVWajOK8=
github.com/dgraph-io/ristretto v0.1.1/go.mod h1:S1GPSBCYCIhmVNfcth17y2zZtQT6wzkzgwUve0VDWWA=
github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2 h1:tdlZCpZ/P9DhczCTSixgIKmwPv6+wP5DGjqLYw5SUiA=
github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw=
github.com/dustin/go-humanize v1.0.0 h1:VSnTsYCnlFHaM2/igO1h6X3HA71jcobQuxemgkq4zYo=
github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b h1:VKtxabqXZkF25pY9ekfRL6a582T4P37/31XEstQ5p58=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk=
golang.org/x/sys v0.0.0-20221010170243-090e33056c14 h1:k5II8e6QD8mITdi+okbbmR/cIyEbeXLBhy5Ha4nevyc=
golang.org/x/sys v0.0.0-20221010170243-090e33056c14/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
module github.com/lxzan/memorycache

go 1.18
go 1.19

require github.com/stretchr/testify v1.8.1

Expand Down
34 changes: 15 additions & 19 deletions index.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ package memorycache
import (
"github.com/lxzan/memorycache/internal/heap"
"github.com/lxzan/memorycache/internal/types"
"github.com/lxzan/memorycache/internal/utils"
"hash/maphash"
"math"
"strings"
"sync"
Expand All @@ -13,6 +13,7 @@ import (
type MemoryCache struct {
config *types.Config
storage []*bucket
seed maphash.Seed
}

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

for i, _ := range mc.storage {
Expand All @@ -41,8 +43,10 @@ func New(options ...Option) *MemoryCache {
defer ticker.Stop()
for {
<-ticker.C
for _, bucket := range mc.storage {
bucket.expireTimeCheck(config.MaxKeysDeleted, config.MaxCapacity)

var now = time.Now().UnixMilli()
for _, b := range mc.storage {
b.expireTimeCheck(now, config.MaxKeysDeleted)
}
}
}()
Expand All @@ -51,7 +55,7 @@ func New(options ...Option) *MemoryCache {
}

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

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

var ele = &types.Element{
Key: key,
Value: value,
ExpireAt: expireAt,
}

var ele = &types.Element{Key: key, Value: value, ExpireAt: expireAt}
b.Heap.Push(ele)
b.Map[key] = ele
if b.Heap.Len() > c.config.MaxCapacity {
delete(b.Map, b.Heap.Pop().Key)
}
return false
}

Expand Down Expand Up @@ -181,18 +183,12 @@ type bucket struct {
}

// 过期时间检查
func (c *bucket) expireTimeCheck(maxNum int, maxCap int) {
func (c *bucket) expireTimeCheck(now int64, num int) {
c.Lock()
defer c.Unlock()

var now = time.Now().UnixMilli()
var num = 0
for c.Heap.Len() > 0 && c.Heap.Front().Expired(now) && num < maxNum {
delete(c.Map, c.Heap.Pop().Key)
num++
}
for c.Heap.Len() > maxCap && num < maxNum {
for c.Heap.Len() > 0 && c.Heap.Front().Expired(now) && num > 0 {
delete(c.Map, c.Heap.Pop().Key)
num++
num--
}
}
2 changes: 1 addition & 1 deletion internal/types/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ func (c *Element) Expired(now int64) bool {

type Config struct {
Interval time.Duration // 检查周期
BucketNum uint32 // 存储桶数量
BucketNum int // 存储桶数量
MaxKeysDeleted int // 每次检查至多删除key的数量(单个存储桶)
InitialSize int // 初始化大小(单个存储桶)
MaxCapacity int // 最大容量(单个存储桶)
Expand Down
13 changes: 6 additions & 7 deletions internal/utils/hash.go
Original file line number Diff line number Diff line change
@@ -1,16 +1,15 @@
package utils

const (
offset32 = 2166136261
prime32 = 16777619
prime64 = 1099511628211
offset64 = 14695981039346656037
)

// Fnv32 returns a new 32-bit FNV-1 hash.Hash.
func Fnv32(s string) uint32 {
var hash uint32 = offset32
func Fnv64(s string) uint64 {
var hash uint64 = offset64
for _, c := range s {
hash *= prime32
hash ^= uint32(c)
hash *= prime64
hash ^= uint64(c)
}
return hash
}
41 changes: 38 additions & 3 deletions internal/utils/helper_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,50 @@ package utils
import (
"github.com/stretchr/testify/assert"
"hash/fnv"
"hash/maphash"
"testing"
)

func TestNewFnv32(t *testing.T) {
func BenchmarkHash_Fnv64(b *testing.B) {
b.Run("16", func(b *testing.B) {
key := string(AlphabetNumeric.Generate(16))
for i := 0; i < b.N; i++ {
Fnv64(key)
}
})

b.Run("32", func(b *testing.B) {
key := string(AlphabetNumeric.Generate(32))
for i := 0; i < b.N; i++ {
Fnv64(key)
}
})
}

func BenchmarkHash_MapHash(b *testing.B) {
seed := maphash.MakeSeed()

b.Run("16", func(b *testing.B) {
key := string(AlphabetNumeric.Generate(16))
for i := 0; i < b.N; i++ {
maphash.String(seed, key)
}
})

b.Run("32", func(b *testing.B) {
key := string(AlphabetNumeric.Generate(32))
for i := 0; i < b.N; i++ {
maphash.String(seed, key)
}
})
}

func TestNewFnv64(t *testing.T) {
for i := 0; i < 10; i++ {
key := AlphabetNumeric.Generate(16)
h := fnv.New32()
h := fnv.New64()
h.Write(key)
assert.Equal(t, h.Sum32(), Fnv32(string(key)))
assert.Equal(t, h.Sum64(), Fnv64(string(key)))
}
}

Expand Down
Loading

0 comments on commit db93db0

Please sign in to comment.