chiachan
chiachan
Published on 2025-01-09 / 18 Visits
0

谜题排查:concurrent map read and map write报错

#go

问题

image-yjsh.png

今天收到同事发来的报错concurrent map read and map write

报错位置源码如下:

var data = make(map[string]int64, 100)

func GetValue(name string) int64 {
	return data[name]
}

分析

看到这个报错,瞬间想到的就是同时写触发,但是报错的位置却是读。
模拟了同时读、同时读写的情况,都没有触发该报错。
下面是ai给出判断

image-cbmg.png

这里关注到一个词潜在的panic,这就很有意思了,这里的意思是读写混合并发不会必现路径。
深入猜测,那就可能是读时恰好map触发了扩容,果然,成功复现

成功复现案例

import (
	"math/rand"
	"sync"
	"testing"
	"time"
)

var m = make(map[string]int64, 100)

func TestReadMaps(t *testing.T) {
	wg := sync.WaitGroup{}
	wg.Add(40)
	for i := 0; i < 20; i++ {
		go func() {
			for i := 0; i < 1000; i++ {
				println(m["a"])
			}
			wg.Done()
		}()
	}
	for i := 0; i < 20; i++ {
		go func(x int) {
			m[GenerateRandomString(x)] = rand.Int63n(99)
			wg.Done()
		}(i)
	}

	wg.Wait()
}

func GenerateRandomString(length int) string {
	rand.New(rand.NewSource(time.Now().UnixNano()))
	digits := "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
	result := ""
	for i := 0; i < length; i++ {
		index := rand.Intn(len(digits))
		result += string(digits[index])
	}
	return result
}

虽然已经复现了,但是还是存在谜题。map初始容量是100,但是排查了实际情况,map最多写入74个key,不太应该触发扩容。
思考再三,应该是map扩容机制问题。下面是ai给出的原因

image-ecya.png

这里提到了哈希冲突过多,那确实是会有这种情况,key是string类型,74/100出现多次冲突是正常了

解决方案

  1. 使用sync.Map代替
  2. 添加读写锁sync.RWMutex确保读时不会触发扩容
  3. 如果已知map里的所有key,初始化更合适的map,确保数据不会触发扩容

我这里的情况刚好适合第3种解决方案,服务启动初始化阶段,就把所有的key都加载到map里面了,后续便不会再有扩容的情况发生