
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 2024-01-24 19:42:30 +08:00 锁范围太大了,而且你这个定时清理要遍历并阻碍全部读写,能快就有鬼了……上个最小堆或者时间轮还能加速一下。 if time.Now().UnixNano() > item.expiration 这段还重新上锁,你确定代码能执行么? 其他的话,没有预分配,没有对象池化,GC 压力会很大。 大小 key 没分开处理,hash 算法对 64 和 32 位有特殊处理,是我的话会手动 padding 对齐 |
2 mason961125 2024-01-24 19:50:37 +08:00 跑一下 CPU Profiling 就能知道你要的答案了。 |
3 wqtacc 2024-01-24 22:02:36 +08:00 github 上找前几个实现,大多都对内存分配,key 、value 存储结构,锁的粒度做了优化 |
4 q1450718943 2024-01-24 22:14:15 +08:00 这 go 代码 get 方法难道没死锁? |
5 Ipsum 2024-01-24 22:16:45 +08:00 为啥要自己造轮子呢? |
6 hahadaxigua834 2024-01-24 22:21:15 +08:00 哈哈 这个问题我来回答,之前正好看了 java 的 concurrent hashmap 。 简单的讲就是 java 的 concurrentHashmap 是无锁算法实现的,做了无数优化,最早 1.多少甚至就了桶碰撞过多的链表转树优化,而 go 的 sync.map 我记得只是加了一把大锁。 在标准库中并发容器方面的实现真的差的不是一点半点,可以看看这个 https://github.com/dgraph-io/ristretto ,给数据库用的 cache ,应该差不到哪去。 |
7 rekulas 2024-01-24 23:02:13 +08:00 求速度至少得用 atomic 实现吧 直接一个 syncmap 套上去能快才是奇事 |
8 icy37785 2024-01-24 23:06:33 +08:00 via iPhone 首先不理解再有很多优秀的轮子的情况下要自己造轮子了,其次不能理解的是既然一定要自己造轮子了,为什么不先看看那些优秀的轮子,随便看一个轮子的实现就不会这样加锁了。 |
9 Rehtt 2024-01-25 09:19:52 +08:00 试着调了一下你的 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 2024-01-25 10:06:06 +08:00 sync.Map 适合读多写少的,一旦要写就会重新复制整个 map ,开销挺大的。 真正的高并发写需要避免多个 CPU 核同时访问一个 cacheline 地址。最简单的方式是先对 key 进行 hash ,然后分成多个 map ,这样并发访问不同的 key 大概率不会同时对一个 map 加锁。 |
15 PungentSauce OP @mengzhuo 还真是,,那个原生 map 实现的锁位置还真是有问题。 |
16 1194129822 2024-01-25 19:41:53 +08:00 java 本身性能本身不弱于 go ,其次 CHM 是 java 里面最优秀的并发结构,最优秀的实现算法,是一个完全无锁并行的 Map 。你把 CHM 换成 Hashtable 或者其他同步 Map 结果可能就不一样了。 |