使用 `sync.Map` 结合分片技术进行优化


使用 sync.Map 结合分片技术进行优化

在高并发场景下,sync.Map 是 Go 语言提供的一个线程安全的映射表,适用于多 goroutine 并发访问的场景。然而,在某些情况下,即使使用 sync.Map,仍然可能遇到性能瓶颈。为了进一步提升性能,我们可以结合分片技术对 sync.Map 进行优化。

什么是分片技术?

分片技术是指将一个大的数据结构拆分成多个小的数据结构,每个小的数据结构称为一个“分片”。每个分片独立管理自己的数据,这样可以减少锁的竞争,提高并发性能。

优化思路

  1. 分片设计:将 sync.Map 拆分成多个小的 sync.Map,每个小的 sync.Map 负责管理一部分数据。
  2. 哈希取模:通过哈希取模的方式决定数据存储在哪个分片中。
  3. 并发安全:每个分片独立加锁,减少锁的竞争。

代码实现

下面是一个使用 sync.Map 结合分片技术进行优化的示例代码:

package main
 
import ( 
	"fmt" 
	"sync"
) 
 
type ShardedSyncMap struct {
	shards []*sync.Map
	shardCount int
}
 
func NewShardedSyncMap(shardCount int) *ShardedSyncMap {
	shards := make([]*sync.Map, shardCount)
	for i := range shards { 
		shards[i] = &sync.Map{}
	}
	return &ShardedSyncMap{ 
		shards: shards,
		shardCount: shardCount,
	}
} 
 
func (s *ShardedSyncMap) getShard(key string) *sync.Map { 
	hash := fnvHash(key)
	return s.shards[hash%s.shardCount]
} 
 
func (s *ShardedSyncMap) Store(key, value interface{}) {
	shard := s.getShard(key.(string))
	shard.Store(key, value)
} 
 
func (s *ShardedSyncMap) Load(key interface{}) (interface{}, bool) {
	shard := s.getShard(key.(string))
	return shard.Load(key)
} 
 
func (s *ShardedSyncMap) Delete(key interface{}) {
	shard := s.getShard(key.(string))
	shard.Delete(key)
} 
 
func fnvHash(key string) int {
	h := uint32(2166136261)
	for _, c := range key {
		h ^= uint32(c)
		h *= 16777619
	}
	return int(h)
}
 
func main() { 
	shardedMap := NewShardedSyncMap(16) // 创建一个包含16个分片的ShardedSyncMap 
 
	// 并发写入数据
	var wg sync.WaitGroup
	for i := 0; i < 1000; i++ { 
		wg.Add(1)
		go func(i int) { 
			defer wg.Done()
			shardedMap.Store(fmt.Sprintf("key%d", i), fmt.Sprintf("value%d", i)) 
		}(i)
	} 
	wg.Wait()
 
	// 读取数据
	for i := 0; i < 1000; i++ { 
		key := fmt.Sprintf("key%d", i) 
		value, _ := shardedMap.Load(key)
		fmt.Printf("Key: %s, Value: %s\n", key, value)
	} 
} 

代码解释

  1. ShardedSyncMap 结构体:包含一个 shards 切片,每个元素是一个 sync.Map,以及一个 shardCount 表示分片的数量。
  2. NewShardedSyncMap 函数:初始化 ShardedSyncMap,创建指定数量的 sync.Map 分片。
  3. getShard 方法:根据键的哈希值选择对应的分片。
  4. Store、Load 和 Delete 方法:分别用于存储、加载和删除数据,通过 getShard 方法选择对应的分片进行操作。
  5. fnvHash 函数:计算键的哈希值,使用 FNV 哈希算法。

运行测试

func BenchmarkSyncMap(b *testing.B) {
	var sm sync.Map
	for i := 0; i < b.N; i++ {
		sm.Store(i, i)
		_, _ = sm.Load(i)
	}
}

func BenchmarkShardedSyncMap(b *testing.B) {
	sm := NewShardedSyncMap()
	for i := 0; i < b.N; i++ {
		sm.Store(i, i)
		_, _ = sm.Load(i)
	}
}
go_demos$ go test -bench=. -cpu=2,4
goos: linux
goarch: amd64
pkg: go_demo
cpu: 12th Gen Intel(R) Core(TM) i5-12400
BenchmarkSyncMap-2               1745912               760.1 ns/op
BenchmarkSyncMap-4               2164318               601.6 ns/op
BenchmarkShardedSyncMap-2        1996474               689.1 ns/op
BenchmarkShardedSyncMap-4        2122142               556.8 ns/op

性能优势

通过分片技术,我们将一个大的 sync.Map 拆分成多个小的 sync.Map,每个分片独立加锁,减少了锁的竞争,提高了并发性能。特别是在高并发场景下,这种优化效果更为显著。

结论

使用 sync.Map 结合分片技术可以有效地提升高并发场景下的性能。通过合理的设计和实现,我们可以在保证线程安全的前提下,最大限度地提高系统的吞吐量。希望本文对你有所帮助,如果你有任何问题或建议,欢迎留言交流。

wx

关注公众号

©2017-2023 鲁ICP备17023316号-1 Powered by Hugo