- 设计思路
- 数据结构选择:考虑使用Go语言中的
sync.Map
,它是Go标准库专门为高并发场景设计的键值对存储结构。如果不使用sync.Map
,也可以基于普通的map
结合同步机制来实现。
- 同步机制
- sync.Map:
sync.Map
内部采用了复杂的结构来实现高效的并发访问。它使用了两个数据结构,一个是read
,它是一个线程安全的只读缓存,另一个是dirty
,它是一个读写都需要加锁的结构。当read
中找不到数据时,会尝试从dirty
中查找。
- 对于写入操作,优先写入
read
,如果read
满了,会将read
提升为dirty
,然后再进行写入。这样的设计使得大多数读操作不需要加锁,从而提高了并发性能。
- 互斥锁(Mutex):
- 如果基于普通
map
实现,在读写操作时都需要加互斥锁。例如在写操作时:
var mu sync.Mutex
var m = make(map[string]int)
func set(key string, value int) {
mu.Lock()
m[key] = value
mu.Unlock()
}
- 读操作同样加锁:
func get(key string) int {
mu.Lock()
value := m[key]
mu.Unlock()
return value
}
- 读写锁(RWMutex):
- 读写锁适合读多写少的场景。读操作使用读锁,写操作使用写锁。例如读操作:
var mu sync.RWMutex
var m = make(map[string]int)
func get(key string) int {
mu.RLock()
value := m[key]
mu.RUnlock()
return value
}
- 写操作:
func set(key string, value int) {
mu.Lock()
m[key] = value
mu.Unlock()
}
- 减少锁带来的性能损耗
- 使用sync.Map:如上述所说,利用其内部结构减少锁的使用频率,多数读操作无锁。
- 分段锁:如果使用普通
map
和锁,可以将map
分为多个段,每个段使用单独的锁。例如将map
按哈希值分为16个段,不同的键根据哈希值对应不同的段,读写操作只需要锁定对应的段,从而减少锁的粒度,提高并发性能。
- 读写分离:对于读多写少的场景,使用读写锁。多个读操作可以同时进行,只有写操作时才独占锁,减少写操作对读操作的影响。
- map动态扩容对设计的影响
- sync.Map:其内部已经处理了动态扩容相关的问题,不需要额外处理。它会在适当的时候将
read
提升为dirty
,并进行数据迁移等操作。
- 基于普通
map
和锁的实现:在动态扩容时,由于Go语言中map
的扩容是自动的,但为了保证并发安全,扩容操作需要加锁。在扩容期间,读写操作都需要等待扩容完成,这可能会导致性能下降。可以采用一种延迟扩容的策略,即当达到扩容条件时,并不立即扩容,而是在读写操作时逐步进行扩容,减少对并发操作的影响。