策略一:使用读写锁(sync.RWMutex)
- 实现方式:
在对Map进行读操作时,使用读锁(
RLock
),在写操作时,使用写锁(Lock
)。例如:
package main
import (
"fmt"
"sync"
)
var (
cache = make(map[string]interface{})
rwLock sync.RWMutex
)
func read(key string) interface{} {
rwLock.RLock()
defer rwLock.RUnlock()
return cache[key]
}
func write(key string, value interface{}) {
rwLock.Lock()
defer rwLock.Unlock()
cache[key] = value
}
- 优点:
- 简单直接:实现起来较为容易,对已有代码的侵入性较小。
- 能有效控制并发读写:读操作可以并发进行,写操作时会阻止其他读写操作,保证数据一致性。
- 缺点:
- 写操作性能瓶颈:写操作加锁时,其他所有读写操作都被阻塞,在高并发写场景下,性能会急剧下降。
- 锁争用问题:如果读操作非常频繁,写操作可能会长时间等待锁,导致写操作饥饿。
策略二:分段锁(Sharded Map)
- 实现方式:
将整个Map分成多个小的Map(段),每个段都有自己独立的锁。读/写操作时,根据key计算出对应的段,然后对该段的锁进行操作。例如:
package main
import (
"fmt"
"sync"
)
const numShards = 16
type ShardedMap struct {
shards []shard
}
type shard struct {
data map[string]interface{}
lock sync.RWMutex
}
func NewShardedMap() *ShardedMap {
sm := &ShardedMap{
shards: make([]shard, numShards),
}
for i := range sm.shards {
sm.shards[i].data = make(map[string]interface{})
}
return sm
}
func (sm *ShardedMap) getShard(key string) *shard {
hash := int(hashKey(key)) % numShards
return &sm.shards[hash]
}
func (sm *ShardedMap) read(key string) interface{} {
sh := sm.getShard(key)
sh.lock.RLock()
defer sh.lock.RUnlock()
return sh.data[key]
}
func (sm *ShardedMap) write(key string, value interface{}) {
sh := sm.getShard(key)
sh.lock.Lock()
defer sh.lock.Unlock()
sh.data[key] = value
}
func hashKey(key string) uint32 {
// 简单的哈希函数示例
var h uint32
for _, c := range key {
h = 31*h + uint32(c)
}
return h
}
- 优点:
- 减少锁争用:不同段的读写操作可以并发进行,大大提高了并发性能,尤其在高并发读写场景下表现出色。
- 可扩展性:可以通过增加段的数量来进一步提高并发性能。
- 缺点:
- 复杂度增加:代码实现相对复杂,需要额外管理段和锁的关系。
- 需要合适的哈希函数:如果哈希函数设计不合理,可能导致数据分布不均匀,部分段的锁争用依然严重。
策略三:使用sync.Map
- 实现方式:
直接使用Go标准库提供的
sync.Map
,它内部实现了更复杂的并发控制机制,无需手动加锁。例如:
package main
import (
"fmt"
"sync"
)
var cache sync.Map
func read(key string) interface{} {
value, ok := cache.Load(key)
if ok {
return value
}
return nil
}
func write(key string, value interface{}) {
cache.Store(key, value)
}
- 优点:
- 高性能:在高并发读写场景下性能表现良好,内部优化了锁的使用,减少锁争用。
- 方便易用:无需手动管理锁,代码简洁,降低了开发难度。
- 缺点:
- 不支持遍历:如果需要遍历Map中的所有元素,
sync.Map
没有直接提供这样的方法,需要自行实现,相对复杂。
- 内存开销:由于内部实现复杂,相比普通Map可能会有更高的内存开销。