package cache
import (
"sync"
"time"
)
type Item struct {
value interface{}
expiration int64
}
type Cache struct {
items map[string]Item
mu sync.RWMutex
defaultExpiration time.Duration
cleanupInterval time.Duration
}
func NewCache(defaultExpiration, cleanupInterval time.Duration) *Cache {
cache := &Cache{
items: make(map[string]Item),
defaultExpiration: defaultExpiration,
cleanupInterval: cleanupInterval,
}
go cache.cleanupExpired()
return cache
}
func (c *Cache) Set(key string, value interface{}, expiration time.Duration) {
c.mu.Lock()
defer c.mu.Unlock()
var exp int64
now := time.Now().UnixNano()
if expiration > 0 {
exp = now + int64(expiration)
} else {
exp = now + int64(c.defaultExpiration)
}
item := Item{
value: value,
expiration: exp,
}
c.items[key] = item
}
func (c *Cache) Get(key string) (interface{}, bool) {
c.mu.RLock()
defer c.mu.RUnlock()
item, found := c.items[key]
if !found {
return nil, false
}
if time.Now().UnixNano() > item.expiration {
c.mu.Lock()
defer c.mu.Unlock()
delete(c.items, key)
return nil, false
}
return item.value, true
}
func (c *Cache) cleanupExpired() {
for {
time.Sleep(c.cleanupInterval)
now := time.Now().UnixNano()
c.mu.Lock()
for key, item := range c.items {
if now > item.expiration {
delete(c.items, key)
}
}
c.mu.Unlock()
}
}
func TestCache1(t *testing.T) {
cache := NewCache(2*time.Second, 2*time.Second)
start := time.Now()
for i := 1; i < 9999999; i++ {
cache.Set(fmt.Sprintf("%d", i), cast.ToString(i), 2*time.Second)
//if i%2 == 0 {
// endTime := time.Now()
// duration := endTime.Sub(start)
// if duration.Milliseconds() > 100 {
// fmt.Println("timeUnit", duration.Milliseconds(), "ms")
// }
// start = time.Now()
//}
if i%100000 == 0 {
var m runtime.MemStats
runtime.ReadMemStats(&m)
fmt.Println(cast.ToString(m.Alloc/1024/1024)+"MB",
cast.ToString(m.TotalAlloc/1024/1024)+"MB")
}
}
endTime := time.Now()
duration := endTime.Sub(start)
fmt.Println("timeUnit", duration.Milliseconds(), "ms")
}
1551MB 1940MB
1555MB 1944MB
timeUnit 5759 ms
还有一个基于 sync.map 的
package cache
import (
"sync"
"time"
)
//var cacheStd = NewCache(time.Second*5, time.Second*10)
type Item struct {
value interface{}
expiration int64
}
type Cache struct {
items sync.Map
defaultExpiration time.Duration
cleanupInterval time.Duration
}
func NewCache(defaultExpiration, cleanupInterval time.Duration) *Cache {
cache := &Cache{
defaultExpiration: defaultExpiration,
cleanupInterval: cleanupInterval,
}
go cache.cleanupExpired()
return cache
}
func (c *Cache) Set(key string, value interface{}, expiration time.Duration) {
var exp int64
now := time.Now().UnixNano()
if expiration > 0 {
exp = now + int64(expiration)
} else {
exp = now + int64(c.defaultExpiration)
}
item := Item{
value: value,
expiration: exp,
}
c.items.Store(key, item)
}
func (c *Cache) Get(key string) (interface{}, bool) {
item, found := c.items.Load(key)
if !found {
return nil, false
}
cachedItem := item.(Item)
if time.Now().UnixNano() > cachedItem.expiration {
c.items.Delete(key)
return nil, false
}
return cachedItem.value, true
}
func (c *Cache) cleanupExpired() {
for {
time.Sleep(c.cleanupInterval)
now := time.Now().UnixNano()
c.items.Range(func(key, value interface{}) bool {
item := value.(Item)
if now > item.expiration {
c.items.Delete(key)
}
return true
})
}
}
//func GetFromCache[T any](key string, action func() T) T {
// data, ok := cacheStd.Get(key)
// if ok {
// return data.(T)
// }
// res := action()
// cacheStd.Set(key, res, 0)
// return res
//}
测试结果差了一倍
2461MB 3114MB
2473MB 3126MB
timeUnit 12503 ms
想对应的 java 代码
package com.example.jtool.controller;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
public class Cache {
private ConcurrentHashMap<String, Item> items;
private long defaultExpiration;
private long cleanupInterval;
private ScheduledExecutorService executor;
public Cache(long defaultExpiration, long cleanupInterval) {
this.items = new ConcurrentHashMap<>();
this.defaultExpiration = defaultExpiration;
this.cleanupInterval = cleanupInterval;
this.executor = Executors.newSingleThreadScheduledExecutor();
this.executor.scheduleAtFixedRate(this::cleanupExpired, cleanupInterval, cleanupInterval, TimeUnit.NANOSECONDS);
}
public void set(String key, Object value, long expiration) {
long exp = expiration > 0 ? System.nanoTime() + expiration : System.nanoTime() + defaultExpiration;
Item item = new Item(value, exp);
items.put(key, item);
}
public Object get(String key) {
Item item = items.get(key);
if (item == null || System.nanoTime() > item.getExpiration()) {
items.remove(key);
return null;
}
return item.getValue();
}
private void cleanupExpired() {
long now = System.nanoTime();
items.forEach((key, value) -> {
Item item = value;
if (now > item.expiration) {
items.remove(key);
}
});
}
public static void main(String[] args) {
long startTime = System.currentTimeMillis();
// 在这里放置需要测量时间的代码
Cache cache = new Cache(2000000000L, 20000000000L); // 5 seconds, 10 seconds
for (Integer i = 1; i < 9999999; i++ ){
cache.set(i.toString(), i.toString(), 2000000000L);
if( i%100000 == 0 ){
Runtime runtime = Runtime.getRuntime();
long memoryUsed = runtime.totalMemory() - runtime.freeMemory();
System.out.println("Memory used: " + memoryUsed/1024/1024 + "MB");
}
}
System.out.println("end");
long endTime = System.currentTimeMillis();
long elapsedTime = endTime - startTime;
System.out.println("程序运行时间:" + elapsedTime + " 毫秒");
}
}
class Item {
private Object value;
public long expiration;
public Item(Object value, long expiration) {
this.value = value;
this.expiration = expiration;
}
public Object getValue() {
return value;
}
public long getExpiration() {
return expiration;
}
}
Memory used: 1632MB
Memory used: 1648MB
Memory used: 1664MB
Memory used: 1680MB
Memory used: 1680MB
end
程序运行时间:3020 毫秒
更加不能理解的是,go 版本的 cache 测试过程中会有明显的阻塞感。有时候可能达到几十上百。有没有同学清楚 go 版本的 cache 哪里写的有问题
1
mengzhuo 305 天前 1
锁范围太大了,而且你这个定时清理要遍历并阻碍全部读写,能快就有鬼了……上个最小堆或者时间轮还能加速一下。
if time.Now().UnixNano() > item.expiration 这段还重新上锁,你确定代码能执行么? 其他的话,没有预分配,没有对象池化,GC 压力会很大。 大小 key 没分开处理,hash 算法对 64 和 32 位有特殊处理,是我的话会手动 padding 对齐 |
2
mason961125 305 天前
跑一下 CPU Profiling 就能知道你要的答案了。
|
3
wqtacc 305 天前
github 上找前几个实现,大多都对内存分配,key 、value 存储结构,锁的粒度做了优化
|
4
q1450718943 305 天前
这 go 代码 get 方法难道没死锁?
|
5
Ipsum 305 天前
为啥要自己造轮子呢?
|
6
hahadaxigua834 305 天前
哈哈 这个问题我来回答,之前正好看了 java 的 concurrent hashmap 。
简单的讲就是 java 的 concurrentHashmap 是无锁算法实现的,做了无数优化,最早 1.多少甚至就了桶碰撞过多的链表转树优化,而 go 的 sync.map 我记得只是加了一把大锁。 在标准库中并发容器方面的实现真的差的不是一点半点,可以看看这个 https://github.com/dgraph-io/ristretto ,给数据库用的 cache ,应该差不到哪去。 |
7
rekulas 305 天前
求速度至少得用 atomic 实现吧 直接一个 syncmap 套上去能快才是奇事
|
8
icy37785 305 天前 via iPhone 1
首先不理解再有很多优秀的轮子的情况下要自己造轮子了,其次不能理解的是既然一定要自己造轮子了,为什么不先看看那些优秀的轮子,随便看一个轮子的实现就不会这样加锁了。
|
9
Rehtt 305 天前
试着调了一下你的 set 和 get 锁的位置就优化了 0.5 秒
|
10
PungentSauce OP @icy37785 sync.map 的那个是第一版,原生 map 的是第二版,sync.map 在我现在写的数据量上也够用。只是测了一下数据量一大就有点离谱了
|
11
PungentSauce OP @Rehtt 啊哈,怎么改的
|
12
PungentSauce OP @hahadaxigua834 get
|
13
PungentSauce OP @q1450718943 原生 map 是后来写的,一开始写的是第二个版本,因为只是一些小量数据的缓存,就自己搞了。
|
14
keakon 305 天前
sync.Map 适合读多写少的,一旦要写就会重新复制整个 map ,开销挺大的。
真正的高并发写需要避免多个 CPU 核同时访问一个 cacheline 地址。最简单的方式是先对 key 进行 hash ,然后分成多个 map ,这样并发访问不同的 key 大概率不会同时对一个 map 加锁。 |
15
PungentSauce OP @mengzhuo 还真是,囧,那个原生 map 实现的锁位置还真是有问题。
|
16
1194129822 304 天前
java 本身性能本身不弱于 go ,其次 CHM 是 java 里面最优秀的并发结构,最优秀的实现算法,是一个完全无锁并行的 Map 。你把 CHM 换成 Hashtable 或者其他同步 Map 结果可能就不一样了。
|