有时候啊,你写个服务跑得好好的,结果一查日志发现数据库被打爆了,QPS 一高延迟直接飙上去。这时候你就会想,要不我加个缓存试试?
没错, 缓存 基本是性能优化里最简单粗暴但最有效的手段。尤其在 Go 项目里,自己写个轻量级缓存比引入 Redis 那种外部服务方便太多。下面咱就来聊聊在 Go 里怎么实现一个本地缓存,够用、稳定,还能支持过期清理。
最初的想法很简单,用个 map 存 key-value 就完事了:
package main
import (
"fmt"
)
func main() {
cache := make(map[string]string)
cache["user_1"] = "Tom"
cache["user_2"] = "Jerry"
fmt.Println(cache["user_1"]) // 输出 Tom
}
看似没问题,但这个实现只能在单线程下跑,一旦有并发访问,比如多个协程同时读写 map,Go 直接给你报 fatal error。 所以第一个坑: map 不是线程安全的 。
那咋办?加锁呗,用
sync.RWMutex
就行。这样我们可以同时读,但写的时候要锁定整个 map。
package main
import (
"fmt"
"sync"
)
type Cache struct {
data map[string]string
mu sync.RWMutex
}
func NewCache() *Cache {
return &Cache{data: make(map[string]string)}
}
func (c *Cache) Set(key, value string) {
c.mu.Lock()
defer c.mu.Unlock()
c.data[key] = value
}
func (c *Cache) Get(key string) (string, bool) {
c.mu.RLock()
defer c.mu.RUnlock()
val, ok := c.data[key]
return val, ok
}
func main() {
cache := NewCache()
cache.Set("user_1", "Tom")
cache.Set("user_2", "Jerry")
if v, ok := cache.Get("user_1"); ok {
fmt.Println(v)
}
}
这个版本已经能在多协程下用了,但问题也明显:
我们想要的效果是:写入时可以设置过期时间,到期自动清理掉。 实现思路也简单,每条数据记录它的过期时间,然后用一个后台协程定期清理过期的 key。
package main
import (
"fmt"
"sync"
"time"
)
type item struct {
value string
expireTime int64
}
type Cache struct {
data map[string]item
mu sync.RWMutex
}
func NewCache() *Cache {
c := &Cache{data: make(map[string]item)}
go c.cleanup() // 启动清理协程
return c
}
func (c *Cache) Set(key, value string, duration time.Duration) {
c.mu.Lock()
defer c.mu.Unlock()
c.data[key] = item{
value: value,
expireTime: time.Now().Add(duration).UnixNano(),
}
}
func (c *Cache) Get(key string) (string, bool) {
c.mu.RLock()
defer c.mu.RUnlock()
it, ok := c.data[key]
if !ok || time.Now().UnixNano() > it.expireTime {
return "", false
}
return it.value, true
}
func (c *Cache) cleanup() {
for {
time.Sleep(5 * time.Second)
now := time.Now().UnixNano()
c.mu.Lock()
for k, v := range c.data {
if now > v.expireTime {
delete(c.data, k)
}
}
c.mu.Unlock()
}
}
func main() {
cache := NewCache()
cache.Set("session_123", "userA", 3*time.Second)
time.Sleep(2 * time.Second)
if v, ok := cache.Get("session_123"); ok {
fmt.Println("命中:", v)
}
time.Sleep(2 * time.Second)
if _, ok := cache.Get("session_123"); !ok {
fmt.Println("过期啦")
}
}
输出:
命中: userA
过期啦
这下基本满足大部分轻量级缓存需求了。 但再往深想一点,你会发现清理线程其实有点“粗暴”——它每次都全量扫描整个 map。数据量大了,性能就废了。
如果你的数据量很大,比如上万个 key,可以优化成惰性清理(lazy eviction): 只有在调用
Get()
的时候才去检查是否过期,过期就删。 再配合定时器定期小范围扫描,这样性能更均衡。
bigcache
或
ristretto
如果你的项目已经上生产,建议别造轮子了。 Go 社区里有很多成熟的缓存库,比如:
allegro/bigcache
(无锁高性能缓存)
dgraph-io/ristretto
(支持 LFU 驱逐策略)
使用起来特别简单,比如
bigcache
:
package main
import (
"fmt"
"time"
"github.com/allegro/bigcache"
)
func main() {
cache, _ := bigcache.NewBigCache(bigcache.DefaultConfig(10 * time.Second))
cache.Set("foo", []byte("bar"))
entry, _ := cache.Get("foo")
fmt.Println(string(entry))
}
这种方案性能非常强,内部做了分片、无锁读写、并发清理,非常适合高 QPS 的场景。
在 Go 里实现缓存其实不难,关键在于你要想清楚需求:
如果只是临时加速数据访问、减少数据库压力,本地缓存是极好的选择; 如果你要多节点共享缓存,那 Redis、Memcached 还是更靠谱。
写 Go 服务的时候,我一般是先加个本地缓存,跑稳定了再考虑 Redis。 因为缓存这玩意嘛,简单才是第一生产力。
-END
-
我为大家打造了一份RPA教程, 完全免费: songshuhezi.com/rpa.html
🔥
虎哥私藏精品
🔥
虎哥作为一名老码农,整理了全网最全 《 GO后端开发资料合集 》。 总量高达 650GB ,点击下方公众号回复关键字 go 全部免费领取