引言:在go语言中map是非线程安全的,也就是其底层代码是不带锁的,不带锁,在并发场景下就会出现安全问题。
1、为什么map不设置成并发安全的?
Go官方在经过了长时间的讨论后,认为Go map 更应适配典型使用场景(不需要从多个 goroutine 中进行安全访问),而不是为了小部分情况(并发访问),导致大部分程序付出加锁代价(性能),决定了不支持。
场景: 2个协程同时读和写,以下程序会出现致命错误:fatalerror:concurrent map writes
2、那么如果真的有并发访问的场景该如何处理?
1、使用sync.Mutex
既然他底层代码没有锁,那么我们就调用go语言的sync包下的Mutex给它单独上锁,每次要操作map的前后都加锁,这样就不会出现多个协程同时对map进行读写操作了
var Locker sync.Mutex
var clientMap = make(map[string]int)
func main() {
for i := 0; i < 100; i++ {
go func() {
key := fmt.Sprintf("key%d", i)
Locker.Lock()
clientMap[key] = i
Locker.Unlock()
}()
}
}
2、使用sync.Map{}
我们可以使用sync包下的一个sync.Map{}这个map是内部是有锁的,可以支持并发访问安全
func main() {
mapTest := sync.Map{}
success := 0
for i := 0; i < 100; i++ {
//h++
go func() {
key := fmt.Sprintf("key%d", i)
mapTest.Store(key, "value")
success++
}()
}
time.Sleep(3 * time.Second)
fmt.Println(success)
}
3、简单小场景测试
//场景:在一个高并发的web服务器中,要限制IP的频繁访问。
//以下代码模拟100个IP同时并发访问服务器,每个IP会重复访问1000次。限制每个IP三分钟之内只能访问一次。
每句代码的解析都放到注释里了,可以复制到goland上测试一下。
// Ban 使用map存储IP访问记录但是因为是高并发场景,普通的map是无法满足并发访问的,需要使用互斥锁来保证线程安全。
// 如果不用锁的话,一定会出现map在多个协程中同时读写,产生panic错误
type Ban struct {
visitIPs map[string]time.Time
mu sync.Mutex // 使用互斥锁来保证线程安全
}
// NewBan 创建一个Ban对象
func NewBan() *Ban {
return &Ban{
visitIPs: make(map[string]time.Time),
mu: sync.Mutex{},
}
}
// true 表示三分钟内访问过 限制访问,false表示三分钟内没有访问过
func (o *Ban) visit(ip string) bool {
o.mu.Lock()
defer o.mu.Unlock()
// 检查当前时间与IP的最后访问时间之间的差异
lastVisit, ok := o.visitIPs[ip]
time.Since(lastVisit) < 3*time.Minute 计算从 lastVisit 到当前时间的持续时间
if ok && time.Since(lastVisit) < 3*time.Minute {
return true
}
// 更新IP的最后访问时间
o.visitIPs[ip] = time.Now()
return false
}
func main() {
// 统计成功次数
success := 0
ban := NewBan()
// 模拟1000次访问
for j := 0; j < 1000; j++ {
// 模拟100个IP同时并发访问服务器
for i := 0; i < 100; i++ {
ip := fmt.Sprintf("192.168.1.%d", i)
go func(ip string) {
if !ban.visit(ip) {
success++ //操作的成功次数
// 模拟耗时操作 在协程中的其他操作所耗费世界
time.Sleep(time.Millisecond * 200)
}
}(ip)
}
}
// 等待所有goroutine完成
time.Sleep(3 * time.Second)
fmt.Println("success:", success)
}