一道Gloang并发、锁的面试题,你会吗?

1.题目描述

源地址:一道并发和锁的golang面试题

场景:
在一个高并发的web服务器中,要限制IP的频繁访问。
现模拟100个IP同时并发访问服务器,每个IP要重复访问1000次。
每个IP三分钟之内只能访问一次。
修改以下代码完成该过程,要求能成功输出 success:100

package main
 
import (
	"fmt"
	"time"
)
 
type Ban struct {
	visitIPs map[string]time.Time
}
 
func NewBan() *Ban {
	return &Ban{visitIPs: make(map[string]time.Time)}
}
func (o *Ban) visit(ip string) bool {
	if _, ok := o.visitIPs[ip]; ok {
		return true
	}
	o.visitIPs[ip] = time.Now()
	return false
}
func main() {
	success := 0
	ban := NewBan()
	for i := 0; i < 1000; i++ {
		for j := 0; j < 100; j++ {
			go func() {
				ip := fmt.Sprintf("192.168.1.%d", j)
				if !ban.visit(ip) {
					success++
				}
			}()
		}
 
	}
	fmt.Println("success:", success)
}

2.问题分析

如果直接运行上述代码的话,会报错。

2.1问题一

变量j,表示的ip地址的最后一位,被多个协程使用,然而并不是以值的方式传入的。这种方式相当于拿着j的地址运行。看下面代码:

for j := 0; j < 10; j++ {
	go func() {
		fmt.Println(j)
	}()
}

打印结果,肯定不是0 1 2 3 4 …9

2.2问题二

变量success,被多个协程同时读写,且并未加锁

2.3问题三

visit函数中的map,也不是协程安全的

2.4问题四

启动了多个协程,并未等待这些协程,就有可能退出主main

2.5问题五

并未判断是否超时3s,假设时间超过了3s,success也不会加一

3.问题解决方法

① 给闭包添加形参,将j作为实参传入
② success前后加锁,或者原子操作
③ map操作前后加锁,或者sync.map
④ 使用sync.WaitGroup对协程进行阻塞控制
⑤ 添加时间判断,超过3s的,success++,并重新更新时间

4.代码实现

go run -race *.go
检测是否存在数据读写竞争

4.1 map前后加锁的方式

visit这样写可能不是最优,但是最容易理解

package main

import (
	"fmt"
	"sync"
	"sync/atomic"
	"time"
)

type Ban struct {
	visitIPs map[string]time.Time
}

var mx sync.Mutex

func NewBan() *Ban {
	return &Ban{visitIPs: make(map[string]time.Time)}
}
func (o *Ban) visit(ip string) bool {  
	mx.Lock()
	defer mx.Unlock()
	if t, ok := o.visitIPs[ip]; ok { //查看该ip是否已经访问过
		if time.Now().Before(t.Add(3 * time.Second)) { //时间检测,查看当前时间是否在计时开始的3s后
			//true 代表尚未到达3s
			return true
		} else {
			//false 代表已经超过3s,则可以访问,并更新时间
			o.visitIPs[ip] = time.Now()
			return false
		}
	}
	o.visitIPs[ip] = time.Now()
	return false
}

func main() {
	var success int64
	var wg sync.WaitGroup
	ban := NewBan()
	for i := 0; i < 1000; i++ {
		for j := 0; j < 100; j++ {
			wg.Add(1)
			go func(temp int) {
				ip := fmt.Sprintf("192.168.1.%d", temp)
				if !ban.visit(ip) {
					atomic.AddInt64(&success, 1)
				}
				wg.Done()
			}(j)
		}

	}
	wg.Wait()
	fmt.Println("success:", success)
}

4.2 sync.map解决方式

只需要更改以下函数

type Ban struct {
	visitIPs sync.Map
}

func NewBan() *Ban {
	return &Ban{visitIPs: sync.Map{}}
}

func (o *Ban) visit(ip string) bool {
	mx.Lock()
	defer mx.Unlock()
	if v, ok := o.visitIPs.Load(ip); ok {
		t := v.(time.Time)
		if time.Now().Before(t.Add(3 * time.Second)) { //时间检测,查看当前时间是否在计时开始的3s后
			//true 代表尚未到达3s
			return true
		} else {
			//false 代表已经超过3s,则可以访问,并更新时间
			o.visitIPs.Store(ip, time.Now())
			return false
		}
	}
	o.visitIPs.Store(ip, time.Now())
	return false
}
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

秋山刀名鱼丶

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值