Skip to content

Commit

Permalink
Merge pull request #3 from lxzan/dev
Browse files Browse the repository at this point in the history
v1.1.2
  • Loading branch information
lxzan authored Oct 29, 2023
2 parents db93db0 + ce1d9f2 commit 2871dd1
Show file tree
Hide file tree
Showing 6 changed files with 92 additions and 70 deletions.
13 changes: 6 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,7 @@
[4]: https://codecov.io/gh/lxzan/memorycache

### Description
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.
Minimalist in-memory KV storage, powered by hashmap and minimal quad heap, without optimizations for GC.
Cache deprecation policy: the set method cleans up overflowed keys; the cycle cleans up expired keys.

### Principle
Expand Down Expand Up @@ -59,10 +58,10 @@ go test -benchmem -run=^$ -bench . github.com/lxzan/memorycache/benchmark
goos: darwin
goarch: arm64
pkg: github.com/lxzan/memorycache/benchmark
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
BenchmarkMemoryCache_Set-8 9836241 117.0 ns/op 19 B/op 0 allocs/op
BenchmarkMemoryCache_Get-8 17689178 67.77 ns/op 0 B/op 0 allocs/op
BenchmarkRistretto_Set-8 14112769 256.2 ns/op 135 B/op 2 allocs/op
BenchmarkRistretto_Get-8 15645778 77.72 ns/op 18 B/op 1 allocs/op
PASS
ok github.com/lxzan/memorycache/benchmark 10.849s
ok github.com/lxzan/memorycache/benchmark 14.059s
```
20 changes: 10 additions & 10 deletions benchmark/benchmark_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,29 +24,29 @@ func BenchmarkMemoryCache_Set(b *testing.B) {
memorycache.WithBucketNum(128),
memorycache.WithBucketSize(1000, 10000),
)
var i = int64(0)
var i = atomic.Int64{}
b.RunParallel(func(pb *testing.PB) {
for pb.Next() {
index := atomic.AddInt64(&i, 1) % benchcount
index := i.Add(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),
memorycache.WithBucketNum(128),
memorycache.WithBucketSize(1000, 10000),
)
for i := 0; i < benchcount; i++ {
mc.Set(benchkeys[i%benchcount], 1, time.Hour)
}

var i = int64(0)
var i = atomic.Int64{}
b.ResetTimer()
b.RunParallel(func(pb *testing.PB) {
for pb.Next() {
index := atomic.AddInt64(&i, 1) % benchcount
index := i.Add(1) % benchcount
mc.Get(benchkeys[index])
}
})
Expand All @@ -58,10 +58,10 @@ func BenchmarkRistretto_Set(b *testing.B) {
MaxCost: 1 << 30, // maximum cost of cache (1GB).
BufferItems: 64, // number of keys per Get buffer.
})
var i = int64(0)
var i = atomic.Int64{}
b.RunParallel(func(pb *testing.PB) {
for pb.Next() {
index := atomic.AddInt64(&i, 1) % benchcount
index := i.Add(1) % benchcount
mc.SetWithTTL(benchkeys[index], 1, 1, time.Hour)
}
})
Expand All @@ -77,11 +77,11 @@ func BenchmarkRistretto_Get(b *testing.B) {
mc.SetWithTTL(benchkeys[i%benchcount], 1, 1, time.Hour)
}

var i = int64(0)
var i = atomic.Int64{}
b.ResetTimer()
b.RunParallel(func(pb *testing.PB) {
for pb.Next() {
index := atomic.AddInt64(&i, 1) % benchcount
index := i.Add(1) % benchcount
mc.Get(benchkeys[index])
}
})
Expand Down
33 changes: 17 additions & 16 deletions index.go
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,17 @@ func (c *MemoryCache) getExp(d time.Duration) int64 {
return time.Now().Add(d).UnixMilli()
}

// Clear 清空所有缓存
// clear all caches
func (c *MemoryCache) Clear() {
for _, b := range c.storage {
b.Lock()
b.Heap = heap.New(c.config.InitialSize)
b.Map = make(map[string]*types.Element, c.config.InitialSize)
b.Unlock()
}
}

// Set 设置键值和过期时间. exp<=0表示永不过期.
// Set the key value and expiration time. exp<=0 means never expire.
func (c *MemoryCache) Set(key string, value any, exp time.Duration) (replaced bool) {
Expand Down Expand Up @@ -137,15 +148,15 @@ func (c *MemoryCache) Delete(key string) (deleted bool) {
return true
}

// Keys 获取前缀匹配的key. 可以通过星号获取所有的key.
// Get prefix matching key, You can get all the keys with an asterisk.
// Keys 获取前缀匹配的key
// Get prefix matching key
func (c *MemoryCache) Keys(prefix string) []string {
var arr = make([]string, 0)
var now = time.Now().UnixMilli()
for _, b := range c.storage {
b.Lock()
for _, v := range b.Heap.Data {
if !v.Expired(now) && (prefix == "*" || strings.HasPrefix(v.Key, prefix)) {
if !v.Expired(now) && strings.HasPrefix(v.Key, prefix) {
arr = append(arr, v.Key)
}
}
Expand All @@ -154,23 +165,13 @@ func (c *MemoryCache) Keys(prefix string) []string {
return arr
}

// Len 获取元素数量
// Len 获取当前元素数量
// Get the number of elements
// @check: 是否检查过期时间 (whether to check expiration time)
func (c *MemoryCache) Len(check bool) int {
func (c *MemoryCache) Len() int {
var num = 0
var now = time.Now().UnixMilli()
for _, b := range c.storage {
b.Lock()
if !check {
num += b.Heap.Len()
} else {
for _, v := range b.Heap.Data {
if !v.Expired(now) {
num++
}
}
}
num += b.Heap.Len()
b.Unlock()
}
return num
Expand Down
17 changes: 9 additions & 8 deletions index_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ func TestNew(t *testing.T) {
db.Set("c", 1, time.Millisecond)

time.Sleep(20 * time.Millisecond)
as.ElementsMatch(db.Keys("*"), []string{"b", "d", "e"})
as.ElementsMatch(db.Keys(""), []string{"b", "d", "e"})
})

t.Run("", func(t *testing.T) {
Expand All @@ -34,7 +34,7 @@ func TestNew(t *testing.T) {
db.Set("a", 1, 40*time.Millisecond)

time.Sleep(30 * time.Millisecond)
as.ElementsMatch(db.Keys("*"), []string{"a", "c", "d", "e"})
as.ElementsMatch(db.Keys(""), []string{"a", "c", "d", "e"})
})

t.Run("", func(t *testing.T) {
Expand All @@ -46,14 +46,15 @@ func TestNew(t *testing.T) {
db.Set("d", 1, 40*time.Millisecond)

time.Sleep(50 * time.Millisecond)
as.Equal(0, db.Len(true))
as.Equal(0, len(db.Keys("")))
})
}

func TestMemoryCache_Set(t *testing.T) {
var list []string
var count = 10000
var mc = New(WithInterval(100 * time.Millisecond))
mc.Clear()
for i := 0; i < count; i++ {
key := string(utils.AlphabetNumeric.Generate(8))
exp := rand.Intn(1000)
Expand All @@ -69,7 +70,7 @@ func TestMemoryCache_Set(t *testing.T) {
mc.Set(key, 1, time.Duration(exp)*time.Millisecond)
}
time.Sleep(1100 * time.Millisecond)
assert.ElementsMatch(t, utils.Uniq(list), mc.Keys("*"))
assert.ElementsMatch(t, utils.Uniq(list), mc.Keys(""))
}

func TestMemoryCache_Get(t *testing.T) {
Expand Down Expand Up @@ -115,7 +116,7 @@ func TestMemoryCache_GetAndRefresh(t *testing.T) {
list = append(list, key)
mc.Set(key, 1, time.Duration(exp)*time.Millisecond)
}
var keys = mc.Keys("*")
var keys = mc.Keys("")
for _, key := range keys {
mc.GetAndRefresh(key, 2*time.Second)
}
Expand All @@ -141,7 +142,7 @@ func TestMemoryCache_Delete(t *testing.T) {
mc.Set(key, 1, time.Duration(exp)*time.Millisecond)
}

var keys = mc.Keys("*")
var keys = mc.Keys("")
for i := 0; i < 100; i++ {
deleted := mc.Delete(keys[i])
assert.True(t, deleted)
Expand All @@ -150,7 +151,7 @@ func TestMemoryCache_Delete(t *testing.T) {
deleted = mc.Delete(key)
assert.False(t, deleted)
}
assert.Equal(t, mc.Len(true), count-100)
assert.Equal(t, mc.Len(), count-100)
}

func TestMaxCap(t *testing.T) {
Expand All @@ -164,5 +165,5 @@ func TestMaxCap(t *testing.T) {
mc.Set(key, 1, -1)
}
time.Sleep(200 * time.Millisecond)
assert.Equal(t, mc.Len(false), 100)
assert.Equal(t, mc.Len(), 100)
}
48 changes: 33 additions & 15 deletions internal/heap/heap.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,13 @@ type Heap struct {

func (c *Heap) Less(i, j int) bool { return c.Data[i].ExpireAt < c.Data[j].ExpireAt }

func (c *Heap) min(i, j int) int {
if c.Data[i].ExpireAt < c.Data[j].ExpireAt {
return i
}
return j
}

func (c *Heap) Len() int {
return len(c.Data)
}
Expand All @@ -30,10 +37,12 @@ func (c *Heap) Push(ele *types.Element) {
}

func (c *Heap) Up(i int) {
var j = (i - 1) / 2
if j >= 0 && c.Less(i, j) {
c.Swap(i, j)
c.Up(j)
if i > 0 {
var j = (i - 1) >> 2
if c.Less(i, j) {
c.Swap(i, j)
c.Up(j)
}
}
}

Expand Down Expand Up @@ -61,18 +70,27 @@ func (c *Heap) Delete(i int) {
}

func (c *Heap) Down(i, n int) {
var j = 2*i + 1
var k = 2*i + 2
var x = -1
if j < n {
x = j
}
if k < n && c.Less(k, j) {
x = k
var j = -1
var index1 = i<<2 + 1
var index2 = i<<2 + 2
var index3 = i<<2 + 3
var index4 = i<<2 + 4

if index1 >= n {
return
} else if index4 < n {
j = c.min(c.min(index1, index2), c.min(index3, index4))
} else if index3 < n {
j = c.min(c.min(index1, index2), index3)
} else if index2 < n {
j = c.min(index1, index2)
} else {
j = index1
}
if x != -1 && c.Less(x, i) {
c.Swap(i, x)
c.Down(x, n)

if j >= 0 && c.Less(j, i) {
c.Swap(i, j)
c.Down(j, n)
}
}

Expand Down
31 changes: 17 additions & 14 deletions internal/heap/heap_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,29 +3,32 @@ package heap
import (
"github.com/lxzan/memorycache/internal/types"
"github.com/stretchr/testify/assert"
"math/rand"
"sort"
"testing"
)

func TestHeap_Sort(t *testing.T) {
var as = assert.New(t)
var h = New(0)
h.Push(&types.Element{ExpireAt: 1})
h.Push(&types.Element{ExpireAt: 3})
h.Push(&types.Element{ExpireAt: 5})
h.Push(&types.Element{ExpireAt: 7})
h.Push(&types.Element{ExpireAt: 9})
h.Push(&types.Element{ExpireAt: 2})
h.Push(&types.Element{ExpireAt: 4})
h.Push(&types.Element{ExpireAt: 6})
h.Push(&types.Element{ExpireAt: 8})
h.Push(&types.Element{ExpireAt: 10})
for i := 0; i < 1000; i++ {
num := rand.Int63n(1000)
h.Push(&types.Element{ExpireAt: num})
}

as.LessOrEqual(h.Front().ExpireAt, h.Data[1].ExpireAt)
as.LessOrEqual(h.Front().ExpireAt, h.Data[2].ExpireAt)
as.LessOrEqual(h.Front().ExpireAt, h.Data[3].ExpireAt)
as.LessOrEqual(h.Front().ExpireAt, h.Data[4].ExpireAt)

as.Equal(h.Front().ExpireAt, int64(1))
var listA = make([]int64, 0)
var list = make([]int64, 0)
for h.Len() > 0 {
listA = append(listA, h.Pop().ExpireAt)
list = append(list, h.Pop().ExpireAt)
}
as.ElementsMatch(listA, []int64{1, 2, 3, 4, 5, 6, 7, 8, 9, 10})
ok := sort.SliceIsSorted(list, func(i, j int) bool {
return list[i] < list[j]
})
as.True(ok)
as.Nil(h.Pop())
}

Expand Down

0 comments on commit 2871dd1

Please sign in to comment.