一、需求场景

高并发查询场景:先查缓存,缓存未命中再查数据库/计算。

核心要求:

  • 并发安全:多goroutine同时读写
  • 高性能:读多写少优化
  • 过期机制:防止数据过时

二、最简实现:sync.Map

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
package cache

import "sync"

type Cache struct {
data sync.Map
}

func New() *Cache {
return &Cache{}
}

func (c *Cache) Get(key string) (any, bool) {
return c.data.Load(key)
}

func (c *Cache) Set(key string, value any) {
c.data.Store(key, value)
}

func (c *Cache) Delete(key string) {
c.data.Delete(key)
}

优点:简单、无锁读取
缺点:无过期机制

三、进阶实现:带过期时间

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
package cache

import (
"sync"
"time"
)

type entry struct {
value any
expireAt int64 // 过期时间戳(纳秒)
}

type Cache struct {
data sync.Map
}

func New() *Cache {
return &Cache{}
}

func (c *Cache) Get(key string) (any, bool) {
v, ok := c.data.Load(key)
if !ok {
return nil, false
}

e := v.(*entry)
if time.Now().UnixNano() > e.expireAt {
c.data.Delete(key) // 惰性删除
return nil, false
}
return e.value, true
}

func (c *Cache) Set(key string, value any, ttl time.Duration) {
c.data.Store(key, &entry{
value: value,
expireAt: time.Now().Add(ttl).UnixNano(),
})
}

func (c *Cache) Delete(key string) {
c.data.Delete(key)
}

惰性删除:读取时检查过期,减少后台开销。

四、高并发场景:分片锁优化

sync.Map在大量写入时性能下降,改用分片锁:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
package cache

import (
"sync"
"time"
)

const shardCount = 32

type entry struct {
value any
expireAt int64
}

type shard struct {
mu sync.RWMutex
data map[string]*entry
}

type Cache struct {
shards [shardCount]*shard
}

func New() *Cache {
c := &Cache{}
for i := 0; i < shardCount; i++ {
c.shards[i] = &shard{data: make(map[string]*entry)}
}
return c
}

// 哈希取模选择分片
func (c *Cache) getShard(key string) *shard {
hash := fnv32(key)
return c.shards[hash%shardCount]
}

func (c *Cache) Get(key string) (any, bool) {
s := c.getShard(key)
s.mu.RLock()
defer s.mu.RUnlock()

e, ok := s.data[key]
if !ok {
return nil, false
}
if time.Now().UnixNano() > e.expireAt {
return nil, false
}
return e.value, true
}

func (c *Cache) Set(key string, value any, ttl time.Duration) {
s := c.getShard(key)
s.mu.Lock()
defer s.mu.Unlock()

s.data[key] = &entry{
value: value,
expireAt: time.Now().Add(ttl).UnixNano(),
}
}

func (c *Cache) Delete(key string) {
s := c.getShard(key)
s.mu.Lock()
defer s.mu.Unlock()
delete(s.data, key)
}

// FNV-32 哈希
func fnv32(key string) uint32 {
hash := uint32(2166136261)
const prime32 = uint32(16777619)
for i := 0; i < len(key); i++ {
hash *= prime32
hash ^= uint32(key[i])
}
return hash
}

优化原理

  • 32个分片,锁竞争概率降低32倍
  • 读操作用RLock,允许并发读
  • 写操作只锁定对应分片

五、使用示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
package main

import (
"fmt"
"time"
"yourpkg/cache"
)

func main() {
c := cache.New()

// 设置缓存,10秒过期
c.Set("user:123", map[string]any{"name": "张三"}, 10*time.Second)

// 高并发读取
var wg sync.WaitGroup
for i := 0; i < 100; i++ {
wg.Add(1)
go func() {
defer wg.Done()
if v, ok := c.Get("user:123"); ok {
fmt.Println(v)
}
}()
}
wg.Wait()
}

六、超时处理:缓存查询超时控制

实际场景中,缓存查询可能涉及外部调用(如Redis),需要超时控制:

6.1 带超时的Get

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
import (
"context"
"time"
)

// GetWithContext 带超时控制的查询
func (c *Cache) GetWithContext(ctx context.Context, key string) (any, bool) {
// 本地内存缓存,通常不需要超时
// 但如果涉及外部调用,可以这样:
type result struct {
value any
ok bool
}
ch := make(chan result, 1)

go func() {
v, ok := c.Get(key)
ch <- result{v, ok}
}()

select {
case r := <-ch:
return r.value, r.ok
case <-ctx.Done():
return nil, false // 超时返回
}
}

// 使用示例
ctx, cancel := context.WithTimeout(context.Background(), 100*time.Millisecond)
defer cancel()
value, ok := cache.GetWithContext(ctx, "user:123")

6.2 单飞模式:防止缓存穿透

高并发时,多个请求同时查询一个不存在的key,导致大量请求打到数据库。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
import "golang.org/x/sync/singleflight"

type Cache struct {
data sync.Map
group singleflight.Group // 合并相同请求
loader func(key string) (any, error) // 数据加载函数
}

// GetWithLoad 缓存未命中时加载,singleflight保证只加载一次
func (c *Cache) GetWithLoad(key string) (any, error) {
// 1. 先查缓存
if v, ok := c.data.Load(key); ok {
return v, nil
}

// 2. 缓存未命中,使用singleflight合并请求
v, err, _ := c.group.Do(key, func() (any, error) {
// 双重检查,防止重复加载
if v, ok := c.data.Load(key); ok {
return v, nil
}

// 加载数据
data, err := c.loader(key)
if err != nil {
return nil, err
}

// 写入缓存
c.data.Store(key, data)
return data, nil
})

return v, err
}

singleflight原理:相同key的多个请求,只有第一个会真正执行,其他等待结果。

6.3 完整的单飞+超时实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
func (c *Cache) GetWithLoadAndTimeout(ctx context.Context, key string, timeout time.Duration) (any, error) {
// 快速路径:缓存命中
if v, ok := c.data.Load(key); ok {
return v, nil
}

// 慢路径:加载+超时
resultCh := make(chan singleflight.Result, 1)

go func() {
v, err, _ := c.group.Do(key, func() (any, error) {
if v, ok := c.data.Load(key); ok {
return v, nil
}
data, err := c.loader(key)
if err != nil {
return nil, err
}
c.data.Store(key, data)
return data, nil
})
resultCh <- singleflight.Result{Val: v, Err: err}
}()

select {
case r := <-resultCh:
return r.Val, r.Err
case <-time.After(timeout):
return nil, fmt.Errorf("cache load timeout")
case <-ctx.Done():
return nil, ctx.Err()
}
}

七、读写锁 vs sync.Map 深度对比

7.1 两种方案实现对比

方案A:sync.RWMutex + map

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
type CacheRWMutex struct {
mu sync.RWMutex
data map[string]any
}

func (c *CacheRWMutex) Get(key string) (any, bool) {
c.mu.RLock() // 读锁
defer c.mu.RUnlock()
v, ok := c.data[key]
return v, ok
}

func (c *CacheRWMutex) Set(key string, value any) {
c.mu.Lock() // 写锁
defer c.mu.Unlock()
c.data[key] = value
}

方案B:sync.Map

1
2
3
4
5
6
7
8
9
10
11
type CacheSyncMap struct {
data sync.Map
}

func (c *CacheSyncMap) Get(key string) (any, bool) {
return c.data.Load(key) // 无锁读取
}

func (c *CacheSyncMap) Set(key string, value any) {
c.data.Store(key, value)
}

7.2 核心区别

特性 RWMutex + map sync.Map
读操作 RLock,有锁 无锁(atomic)
写操作 Lock,互斥 CAS + 分离读写map
内存模型 单一map 读写分离(read + dirty)
适用场景 读写均衡 读多写少
删除策略 直接删除 延迟删除(标记删除)

7.3 sync.Map的读写分离原理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// sync.Map 内部结构简化
type Map struct {
mu sync.Mutex
read atomic.Pointer[readOnly] // 读map,无锁访问
dirty map[any]*entry // 写map,需要加锁
}

type readOnly struct {
m map[any]*entry
amended bool // dirty是否有read没有的数据
}

type entry struct {
p unsafe.Pointer // 指向实际值,或标记为已删除
}

读取流程

1
2
3
1. 先查read(无锁)
2. 找到且未被删除 → 返回
3. 找不到且amended=true → 加锁查dirty → 更新read → 返回

写入流程

1
2
3
1. read中找到且未被删除 → CAS更新(无锁)
2. 否则加锁,写入dirty
3. 定期将dirty提升为read

7.4 性能测试对比

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
func BenchmarkRWMutexRead(b *testing.B) {
c := &CacheRWMutex{data: make(map[string]any)}
c.Set("key", "value")

b.RunParallel(func(pb *testing.PB) {
for pb.Next() {
c.Get("key")
}
})
}

func BenchmarkSyncMapRead(b *testing.B) {
c := &CacheSyncMap{}
c.Set("key", "value")

b.RunParallel(func(pb *testing.PB) {
for pb.Next() {
c.Get("key")
}
})
}

典型结果(读多写少场景):

1
2
BenchmarkRWMutexRead-8    10000000    120 ns/op
BenchmarkSyncMapRead-8 50000000 25 ns/op // 快5倍

典型结果(读写均衡场景):

1
2
BenchmarkRWMutexReadWrite-8    5000000    280 ns/op
BenchmarkSyncMapReadWrite-8 3000000 450 ns/op // 更慢

7.5 选择建议

1
2
3
4
5
6
7
8
9
┌─────────────────────────────────────────────────────────┐
│ 如何选择? │
├─────────────────────────────────────────────────────────┤
│ 读>>写(如配置缓存) → sync.Map │
│ 读写均衡 → RWMutex + map 或 分片锁 │
│ 需要遍历所有key → RWMutex + map │
│ key集合稳定不变 → sync.Map 更优 │
│ key频繁增删 → 分片锁 RWMutex │
└─────────────────────────────────────────────────────────┘

7.6 分片锁:结合两者优势

对于高并发读写均衡场景,分片锁是最佳选择:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
type shardedCache struct {
shards [32]struct {
mu sync.RWMutex // 每个分片用RWMutex
data map[string]any
}
}

func (c *shardedCache) Get(key string) (any, bool) {
s := c.getShard(key)
s.mu.RLock() // 读锁,允许并发读
defer s.mu.RUnlock()
v, ok := s.data[key]
return v, ok
}

func (c *shardedCache) Set(key string, value any) {
s := c.getShard(key)
s.mu.Lock() // 写锁,互斥
defer s.mu.Unlock()
s.data[key] = value
}

优势

  • 32个分片,锁竞争概率降低32倍
  • 每个分片内RWMutex支持并发读
  • 比sync.Map在写多场景更稳定

八、性能对比总结

实现方式 读性能 写性能 适用场景
sync.Map ⭐⭐⭐⭐⭐ ⭐⭐ 读多写少
RWMutex + map ⭐⭐⭐ ⭐⭐⭐ 读写均衡、需遍历
分片锁 ⭐⭐⭐⭐ ⭐⭐⭐⭐ 高并发读写
单锁 ⭐⭐ ⭐⭐ 简单场景

九、生产级考虑

实际生产环境还需考虑:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 1. 容量限制
type Cache struct {
maxSize int
current int
}

// 2. 定期清理过期数据
func (c *Cache) StartCleanup(interval time.Duration) {
go func() {
ticker := time.NewTicker(interval)
for range ticker.C {
c.cleanup()
}
}()
}

// 3. 统计信息
type Stats struct {
Hits int64
Misses int64
}

十、总结

方案 复杂度 推荐场景
sync.Map 读多写少、无过期需求
sync.Map + 过期 读多写少、有过期需求
RWMutex + map 读写均衡、需要遍历所有key
分片锁 高并发读写、性能敏感
分片锁 + singleflight 防穿透、防雪崩

核心选择原则

  1. 读写比例:读>>写选sync.Map,读写均衡选分片锁
  2. 是否需要遍历:sync.Map不支持高效遍历,需要遍历用RWMutex
  3. 防穿透需求:加singleflight
  4. 超时控制:用context + channel

对于大多数场景,sync.Map + 惰性过期 已经足够;高并发场景推荐 分片锁 + singleflight 组合。