Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,4 @@ cmd/
bin/
examples/
.DS_Store
.serena
90 changes: 52 additions & 38 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,29 +4,25 @@
<h5>To the time to life, rather than to life in time.</h5>
</div>


[中文](README_CN.md)

[![Build Status][1]][2] [![codecov][3]][4]

[1]: https://github.com/lxzan/memorycache/workflows/Go%20Test/badge.svg?branch=main

[2]: https://github.com/lxzan/memorycache/actions?query=branch%3Amain

[3]: https://codecov.io/gh/lxzan/memorycache/graph/badge.svg?token=OHD6918OPT

[4]: https://codecov.io/gh/lxzan/memorycache

### Description

Minimalist in-memory KV storage, powered by `HashMap` and `Minimal Quad Heap`.

**Cache Elimination Policy:**
**Cache Eviction Policy:**

- Set method cleans up overflowed keys
- Active cycle cleans up expired keys
- `Set` evicts LRU keys on capacity overflow
- Periodic cycle evicts expired keys

### Principle
### Design

- Storage Data Limit: Limited by maximum capacity
- Expiration Time: Supported
Expand All @@ -40,22 +36,36 @@ Minimalist in-memory KV storage, powered by `HashMap` and `Minimal Quad Heap`.
- Simple and easy to use
- High performance
- Low memory usage
- Use quadruple heap to maintain the expiration time, effectively reduce the height of the tree, and improve the
insertion performance
- Uses a 4-ary min-heap to maintain expiration times. Compared to binary heaps, this reduces tree height and improves insertion performance.

### Methods

- [x] **Set** : Set key-value pair with expiring time. If the key already exists, the value will be updated. Also the
expiration time will be updated.
- [x] **SetWithCallback** : Set key-value pair with expiring time and callback function. If the key already exists,
the value will be updated. Also the expiration time will be updated.
- [x] **Get** : Get value by key. If the key does not exist, the second return value will be false.
- [x] **GetWithTTL** : Get value by key. If the key does not exist, the second return value will be false. When return
value, method will refresh the expiration time.
- [x] **Delete** : Delete key-value pair by key.
- [x] **GetOrCreate** : Get value by key. If the key does not exist, the value will be created.
- [x] **GetOrCreateWithCallback** : Get value by key. If the key does not exist, the value will be created. Also the
callback function will be called.
- [x] **Set** : Set key-value pair with expiring time. If the key already exists, the value will be updated. Also the
expiration time will be updated. `exp<=0` means never expire.
- [x] **SetWithCallback** : Similar to `Set`, but with a callback function. The callback is triggered on capacity overflow or expiration.
- [x] **Get** : Get value by key. If the key does not exist, the second return value will be false.
- [x] **GetTTL** : Get the expiration time of the key.
- [x] **GetWithTTL** : Get value by key. If the key exists, refreshes the expiration time.
- [x] **UpdateTTL** : Update the expiration time of the key. `d<=0` means never expire.
- [x] **Delete** : Delete key-value pair by key.
- [x] **GetOrCreate** : Get value by key. If the key does not exist, the value will be created.
- [x] **GetOrCreateWithCallback** : Get value by key. If the key does not exist, the value will be created. Also the
callback function will be called.
- [x] **Clear** : Clear all caches. Triggers `ReasonCleared` callback for alive non-expired elements.
- [x] **Range** : Iterate all key-value pairs in the cache.
- [x] **Len** : Get the current number of cached elements (may include expired but un-cleaned elements).
- [x] **Stop** : Stop the background goroutines and release resources.

### Options

| Option | Default | Description |
| --------------------------- | ------------ | ------------------------------------------------------ |
| `WithBucketNum(num)` | 16 | Number of storage buckets (auto-rounded to power of 2) |
| `WithBucketSize(size, cap)` | 1000, 100000 | Initial size and max capacity per bucket |
| `WithInterval(min, max)` | 5s, 30s | Adaptive TTL check period |
| `WithDeleteLimits(num)` | 1000 | Max keys deleted per TTL check per bucket |
| `WithCachedTime(enabled)` | true | Enable time caching for lower `time.Now()` overhead |
| `WithSwissTable(enabled)` | false | Use SwissTable instead of Go runtime map |

### Example

Expand All @@ -64,22 +74,24 @@ package main

import (
"fmt"
"github.com/lxzan/memorycache"
"time"

"github.com/lxzan/memorycache"
)

func main() {
mc := memorycache.New[string, any](
// Set the number of storage buckets, y=pow(2,x)
memorycache.WithBucketNum(128),

// Set bucket size, initial size and maximum capacity (single bucket)
// Set bucket size, initial size and maximum capacity (per bucket)
memorycache.WithBucketSize(1000, 10000),

// Set the expiration time check period.
// Set the expiration time check period.
// If the number of expired elements is small, take the maximum value, otherwise take the minimum value.
memorycache.WithInterval(5*time.Second, 30*time.Second),
)
defer mc.Stop() // Stop background goroutines and release resources

mc.SetWithCallback("xxx", 1, time.Second, func(element *memorycache.Element[string, any], reason memorycache.Reason) {
fmt.Printf("callback: key=%s, reason=%v\n", element.Key, reason)
Expand All @@ -98,22 +110,24 @@ func main() {

### Benchmark

- 1,000,000 elements
- Pre-populated with 1,000,000 elements. benchtime=3s, parallel, 10 CPU cores.

```
goos: linux
goarch: amd64
goos: darwin
goarch: arm64
pkg: github.com/lxzan/memorycache/benchmark
cpu: AMD Ryzen 5 PRO 4650G with Radeon Graphics
BenchmarkMemoryCache_Set-8 16107153 74.85 ns/op 15 B/op 0 allocs/op
BenchmarkMemoryCache_Get-8 28859542 42.34 ns/op 0 B/op 0 allocs/op
BenchmarkMemoryCache_SetAndGet-8 27317874 63.02 ns/op 0 B/op 0 allocs/op
BenchmarkRistretto_Set-8 13343023 272.6 ns/op 120 B/op 2 allocs/op
BenchmarkRistretto_Get-8 19799044 55.06 ns/op 17 B/op 1 allocs/op
BenchmarkRistretto_SetAndGet-8 11212923 119.6 ns/op 30 B/op 1 allocs/op
BenchmarkTheine_Set-8 3775975 322.5 ns/op 30 B/op 0 allocs/op
BenchmarkTheine_Get-8 21579301 54.94 ns/op 0 B/op 0 allocs/op
BenchmarkTheine_SetAndGet-8 6265330 224.6 ns/op 0 B/op 0 allocs/op
cpu: Apple M1 Max
BenchmarkMemoryCache_Set-10 90382902 41.77 ns/op 4 B/op 0 allocs/op
BenchmarkMemoryCache_Get-10 79059558 50.51 ns/op 0 B/op 0 allocs/op
BenchmarkMemoryCache_SetAndGet-10 78810615 48.48 ns/op 0 B/op 0 allocs/op
BenchmarkMemoryCache_Delete-10 153390056 23.76 ns/op 0 B/op 0 allocs/op
BenchmarkMemoryCache_UpdateTTL-10 100000000 32.73 ns/op 0 B/op 0 allocs/op
BenchmarkRistretto_Set-10 68245581 673.1 ns/op 112 B/op 2 allocs/op
BenchmarkRistretto_Get-10 86053975 45.96 ns/op 17 B/op 1 allocs/op
BenchmarkRistretto_SetAndGet-10 27956532 110.6 ns/op 31 B/op 1 allocs/op
BenchmarkTheine_Set-10 9754111 381.7 ns/op 22 B/op 0 allocs/op
BenchmarkTheine_Get-10 32876373 109.1 ns/op 0 B/op 0 allocs/op
BenchmarkTheine_SetAndGet-10 23014824 172.4 ns/op 0 B/op 0 allocs/op
PASS
ok github.com/lxzan/memorycache/benchmark 53.498s
ok github.com/lxzan/memorycache/benchmark 103.750s
```
97 changes: 58 additions & 39 deletions README_CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,50 +4,66 @@
<h5>To the time to life, rather than to life in time.</h5>
</div>

[English](README.md)

[![Build Status][1]][2] [![codecov][3]][4]

[1]: https://github.com/lxzan/memorycache/workflows/Go%20Test/badge.svg?branch=main

[2]: https://github.com/lxzan/memorycache/actions?query=branch%3Amain

[3]: https://codecov.io/gh/lxzan/memorycache/graph/badge.svg?token=OHD6918OPT

[4]: https://codecov.io/gh/lxzan/memorycache

### 简介
### 简介

极简的内存键值(KV)存储系统,其核心由哈希表(HashMap) 和最小四叉堆(Minimal Quad Heap) 构成.
极简的内存键值存储,由 `HashMap` 和最小四叉堆Minimal Quad Heap)驱动。

**缓存淘汰策略:**
**缓存驱逐策略:**

- Set 方法清理溢出的键值对
- 周期清理过期的键值对
- `Set` 方法在容量溢出时驱逐键值对
- 周期性清理过期键值对

### 原则:
### 设计

- 存储数据限制:受最大容量限制
- 过期时间:支持
- 缓存驱逐策略:LRU
- 持久化:无
- 锁定机制:分片和互斥锁
- GC 优化:无指针技术实现的哈希表, 最小堆和链表(不包括用户KV)
- 锁定机制:分桶 + 互斥锁
- GC 优化:无指针技术实现的哈希表、堆和链表(不包括用户 KV)

### 优势
### 优势

- 简单易用
- 高性能
- 内存占用低
- 使用四叉堆维护过期时间, 有效降低树高度, 提高插入性能

### 方法:

- [x] **Set** : 设置键值对及其过期时间。如果键已存在,将更新其值和过期时间。
- [x] **SetWithCallback** : 与 Set 类似,但可指定回调函数。
- [x] **Get** : 根据键获取值。如果键不存在,第二个返回值为 false。
- [x] **GetWithTTL** : 根据键获取值,如果键不存在,第二个返回值为 false。在返回值时,该方法将刷新过期时间。
- [x] **Delete** : 根据键删除键值对。
- [x] **GetOrCreate** : 根据键获取值。如果键不存在,将创建该值。
- [x] **GetOrCreateWithCallback** : 根据键获取值。如果键不存在,将创建该值,并可调用回调函数。
- 使用四叉堆维护过期时间。相比二叉堆,可有效降低堆的高度,提高插入性能。

### 方法

- [x] **Set** : 设置键值对及其过期时间。如果键已存在,将更新其值和过期时间。`exp<=0` 表示永不过期。
- [x] **SetWithCallback** : 与 `Set` 类似,但可指定回调函数。容量溢出或过期时触发回调。
- [x] **Get** : 根据键获取值。如果键不存在,第二个返回值为 false。
- [x] **GetTTL** : 获取键的过期时间。
- [x] **GetWithTTL** : 根据键获取值。如果键存在,刷新其过期时间。
- [x] **UpdateTTL** : 更新键的过期时间。`d<=0` 表示永不过期。
- [x] **Delete** : 根据键删除键值对。
- [x] **GetOrCreate** : 根据键获取值。如果键不存在,将创建该值。
- [x] **GetOrCreateWithCallback** : 根据键获取值。如果键不存在,将创建该值,并可指定回调函数。
- [x] **Clear** : 清空所有缓存。对存活非过期元素触发 `ReasonCleared` 回调。
- [x] **Range** : 遍历缓存中的所有键值对。
- [x] **Len** : 获取当前缓存元素数量(可能包含已过期但未清除的元素)。
- [x] **Stop** : 停止后台协程,释放资源。

### 配置项

| 选项 | 默认值 | 说明 |
| --------------------------- | ------------ | ---------------------------------------- |
| `WithBucketNum(num)` | 16 | 存储桶数量(自动取 2 的幂次) |
| `WithBucketSize(size, cap)` | 1000, 100000 | 单桶初始大小和最大容量 |
| `WithInterval(min, max)` | 5s, 30s | 自适应 TTL 检查周期 |
| `WithDeleteLimits(num)` | 1000 | 每次 TTL 检查单桶最大删除数 |
| `WithCachedTime(enabled)` | true | 开启时间缓存,减少 `time.Now()` 调用开销 |
| `WithSwissTable(enabled)` | false | 使用 SwissTable 替代 Go runtime map |

### 使用

Expand All @@ -66,12 +82,13 @@ func main() {
// 设置存储桶数量, y=pow(2,x)
memorycache.WithBucketNum(128),

// 设置单个存储桶的初始化容量和最大容量
// 设置单个存储桶的初始容量和最大容量
memorycache.WithBucketSize(1000, 10000),

// 设置过期时间检查周期. 如果过期元素较少, 取最大值, 反之取最小值.
// 设置过期时间检查周期如果过期元素较少, 取最大值, 反之取最小值
memorycache.WithInterval(5*time.Second, 30*time.Second),
)
defer mc.Stop() // 停止后台协程, 释放资源

mc.SetWithCallback("xxx", 1, time.Second, func(element *memorycache.Element[string, any], reason memorycache.Reason) {
fmt.Printf("callback: key=%s, reason=%v\n", element.Key, reason)
Expand All @@ -90,22 +107,24 @@ func main() {

### 基准测试

- 1,000,000 元素
- 预填充 1,000,000 个元素。benchtime=3s,并发执行,10 CPU 核心。

```
goos: linux
goarch: amd64
goos: darwin
goarch: arm64
pkg: github.com/lxzan/memorycache/benchmark
cpu: AMD Ryzen 5 PRO 4650G with Radeon Graphics
BenchmarkMemoryCache_Set-8 16107153 74.85 ns/op 15 B/op 0 allocs/op
BenchmarkMemoryCache_Get-8 28859542 42.34 ns/op 0 B/op 0 allocs/op
BenchmarkMemoryCache_SetAndGet-8 27317874 63.02 ns/op 0 B/op 0 allocs/op
BenchmarkRistretto_Set-8 13343023 272.6 ns/op 120 B/op 2 allocs/op
BenchmarkRistretto_Get-8 19799044 55.06 ns/op 17 B/op 1 allocs/op
BenchmarkRistretto_SetAndGet-8 11212923 119.6 ns/op 30 B/op 1 allocs/op
BenchmarkTheine_Set-8 3775975 322.5 ns/op 30 B/op 0 allocs/op
BenchmarkTheine_Get-8 21579301 54.94 ns/op 0 B/op 0 allocs/op
BenchmarkTheine_SetAndGet-8 6265330 224.6 ns/op 0 B/op 0 allocs/op
cpu: Apple M1 Max
BenchmarkMemoryCache_Set-10 90382902 41.77 ns/op 4 B/op 0 allocs/op
BenchmarkMemoryCache_Get-10 79059558 50.51 ns/op 0 B/op 0 allocs/op
BenchmarkMemoryCache_SetAndGet-10 78810615 48.48 ns/op 0 B/op 0 allocs/op
BenchmarkMemoryCache_Delete-10 153390056 23.76 ns/op 0 B/op 0 allocs/op
BenchmarkMemoryCache_UpdateTTL-10 100000000 32.73 ns/op 0 B/op 0 allocs/op
BenchmarkRistretto_Set-10 68245581 673.1 ns/op 112 B/op 2 allocs/op
BenchmarkRistretto_Get-10 86053975 45.96 ns/op 17 B/op 1 allocs/op
BenchmarkRistretto_SetAndGet-10 27956532 110.6 ns/op 31 B/op 1 allocs/op
BenchmarkTheine_Set-10 9754111 381.7 ns/op 22 B/op 0 allocs/op
BenchmarkTheine_Get-10 32876373 109.1 ns/op 0 B/op 0 allocs/op
BenchmarkTheine_SetAndGet-10 23014824 172.4 ns/op 0 B/op 0 allocs/op
PASS
ok github.com/lxzan/memorycache/benchmark 53.498s
ok github.com/lxzan/memorycache/benchmark 103.750s
```
35 changes: 35 additions & 0 deletions benchmark/benchmark_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,40 @@ func BenchmarkMemoryCache_SetAndGet(b *testing.B) {
})
}

func BenchmarkMemoryCache_Delete(b *testing.B) {
var mc = memorycache.New[string, int](options...)
for i := 0; i < benchcount; i++ {
mc.Set(benchkeys[i%benchcount], 1, time.Hour)
}

b.ResetTimer()
b.RunParallel(func(pb *testing.PB) {
var i = 0
for pb.Next() {
index := getIndex(i)
i++
mc.Delete(benchkeys[index])
}
})
}

func BenchmarkMemoryCache_UpdateTTL(b *testing.B) {
var mc = memorycache.New[string, int](options...)
for i := 0; i < benchcount; i++ {
mc.Set(benchkeys[i%benchcount], 1, time.Hour)
}

b.ResetTimer()
b.RunParallel(func(pb *testing.PB) {
var i = 0
for pb.Next() {
index := getIndex(i)
i++
mc.UpdateTTL(benchkeys[index], time.Hour)
}
})
}

func BenchmarkRistretto_Set(b *testing.B) {
var mc, _ = ristretto.NewCache(&ristretto.Config{
NumCounters: capacity * sharding * 10, // number of keys to track frequency of (10M).
Expand Down Expand Up @@ -209,6 +243,7 @@ func TestLRU_Impl(t *testing.T) {
memorycache.WithBucketNum(1),
memorycache.WithBucketSize(capacity, capacity),
)
defer mc.Stop()
var cache, _ = lru.New[string, int](capacity)
for i := 0; i < count; i++ {
key := string(utils.AlphabetNumeric.Generate(16))
Expand Down
Loading