怎么处理锁分段

题目来源:网易互娱

答案1:

在golang的原生map是非并发安全的,为了实现了map的并发安全,最安全有效的方式就是给map加锁,如果遇到大数据,高并发的场景下,直接对整个map进行加锁的话,就会显得整个并发访问控制及其缓慢,由此在sync.map还未出来之前,比较流行的做法就是使用分段锁,降低锁的颗粒度,从而使每个分片上的数据读写互不影响,从而提高map整体的读写效率

模拟分段锁实现

  1. // Map 分片
  2. type ConcurrentMap []*ConcurrentMapShared
  3. // 每一个Map 是一个加锁的并发安全Map
  4. type ConcurrentMapShared struct {
  5. items map[string]interface{}
  6. sync.RWMutex // 各个分片Map各自的锁
  7. }

主流的分段锁,即通过hash取模的方式找到当前访问的key处于哪一个分片之上,再对该分片进行加锁之后再读写。分片定位时,常用有BKDR, FNV32等hash算法得到key的hash值。

  1. func New() ConcurrentMap {
  2. // SHARD_COUNT 默认32个分片
  3. m := make(ConcurrentMap, SHARD_COUNT)
  4. for i := 0; i < SHARD_COUNT; i++ {
  5. m[i] = &ConcurrentMapShared{
  6. items: make(map[string]interface{}),
  7. }
  8. }
  9. return m
  10. }

在初始化好分片后, 对分片上的数据进行读写时就需要用hash取模进行分段定位来确认即将要读写的分片。

  1. func (m ConcurrentMap) GetShard(key string) *ConcurrentMapShared {
  2. return m[uint(fnv32(key))%uint(SHARD_COUNT)]
  3. }
  4. // FNV hash
  5. func fnv32(key string) uint32 {
  6. hash := uint32(2166136261)
  7. const prime32 = uint32(16777619)
  8. for i := 0; i < len(key); i++ {
  9. hash *= prime32
  10. hash ^= uint32(key[i])
  11. }
  12. return hash
  13. }
  14. func (m ConcurrentMap) Set(key string, value interface{}) {
  15. shard := m.GetShard(key) // 段定位找到分片
  16. shard.Lock() // 分片上锁
  17. shard.items[key] = value // 分片操作
  18. shard.Unlock() // 分片解锁
  19. }
  20. func (m ConcurrentMap) Get(key string) (interface{}, bool) {
  21. shard := m.GetShard(key)
  22. shard.RLock()
  23. val, ok := shard.items[key]
  24. shard.RUnlock()
  25. return val, ok
  26. }

由此一个分段锁Map就实现了, 但是比起普通的Map, 常用到的方法比如获取所有key, 获取所有Val 操作是要比原生Map复杂的,因为要遍历每一个分片的每一个数据, 好在golang的并发特性使得解决这类问题变得非常简单

参与讨论