题目一场景:某一高并发web服务器需限制IP的频繁访问,现模拟100个IP同时并发访问该服务器,每个IP需访问1000次。
题目一要求:每个IP三分钟内只能访问一次。
题目一内容:修改以下代码完成上述题目要求,要求能成功输出【success100】。
题目代码如下:
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)
}
解析:上述问题主要考察的是并发情况下map的读写问题,上述案例代码中又存在for循环中启动goroutine时变量使用问题以及goroutine执行滞后问题。
我们首先要做的就是保证启动的goroutine能得到正常的参数,然后就是保证map的并发读写,左后保证三分钟之内只能访问一次。
多CPU核心下修改int的值极端情况下会存在不同步的情况,一次需要原子性的修改int值,来看下代码示例:
package main
import (
"context"
"fmt"
"sync"
"time"
)
type Ban struct {
visitIPs map[string]time.Time
lock sync.Mutex
}
func NewBan(ctx context.Context) *Ban {
o := &Ban{visitIPs: make(map[string]time.Time)}
go func() {
timer := time.NewTimer(time.Minute * 1)
for {
select {
case <-timer.C:
o.lock.Lock()
for k, v := range o.visitIPs {
if time.Now().Sub(v) >= time.Minute*1 {
delete(o.visitIPs, k)
}
}
o.lock.Unlock()
timer.Reset(time.Minute * 1)
case <-ctx.Done():
return
}
}
}()
return o
}
func (o *Ban) visit(ip string) bool {
o.lock.Lock()
defer o.lock.Unlock()
if _, ok := o.visitIPs[ip]; ok {
return true
}
o.visitIPs[ip] = time.Now()
return false
}
func main() {
success := int64(0)
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
ban := NewBan(ctx)
wait := &sync.WaitGroup{}
wait.Add(1000 * 100)
for i := 0; i < 1000; i++ {
for j := 0; j < 100; j++ {
go func(num int) {
defer wait.Done()
ip := fmt.Sprintf("192.168.1.%d", num)
if !ban.visit(ip) {
success++
}
}(j)
}
}
wait.Wait()
fmt.Println("success:", success)
}
上述代码启动了一个协程每分钟查验一下map中过期的ip,for循环在启动goroutine时传参也做了优化。
题目二:写出以下逻辑,要求每秒钟调用一次proc并保证程序不退出。
package main
func main() {
go func() {
// 1 在这里需要你写算法
// 2 要求每秒钟调用一次proc函数
// 3 要求程序不能退出
}()
select {}
}
func proc() {
panic("ok")
}
解析:
上述题目中考察知识点如下:
-
定时执行任务
-
捕获panic错误
为实现题目中每秒执行一次的要求,我们使用time.Ticker对象,这个函数可以每秒钟往chan中放一个Time,符合预期,之后使用recover()函数来捕获panic异常。
代码案例如下:
package main
import (
"fmt"
"time"
)
func main() {
go func() {
// 1 在这里需要你写算法
// 2 要求每秒钟调用一次proc函数
// 3 要求程序不能退出
t := time.NewTicker(time.Second * 1)
for {
select {
case <-t.C:
go func() {
defer func() {
if err := recover(); err != nil {
fmt.Println(err)
}
}()
}()
proc()
}
}
}()
select {}
}
func proc() {
panic("ok")
}
题目三:使sync.WaitGroup中的Wait函数支持WaitTimeout功能。
package main
import (
"fmt"
"sync"
"time"
)
func main() {
wg := sync.WaitGroup{}
c := make(chan struct{})
for i := 0; i < 10; i++ {
wg.Add(1)
go func(num int, close <-chan struct{}) {
defer wg.Done()
<-close
fmt.Println(num)
}(i, c)
}
if WaitTimeout(&wg, time.Second*5) {
close(c)
fmt.Println("timeout exit")
}
time.Sleep(time.Second * 10)
}
func WaitTimeout(wg *sync.WaitGroup, timeout time.Duration) bool {
// 要求手写代码
// 要求sync.WaitGroup支持timeout功能
// 如果timeout到了超时时间返回true
// 如果WaitGroup自然结束返回false
}
解析:本身sync.WaitGroup对象中的Wait函数就是阻塞的,同时,超时用到的time.Timer对象也要阻塞读。
同时阻塞两个对象肯定要对应每一个对象启动一个协程,每个协程去处理一个阻塞,难点就在于如何知道那个阻塞先完成。
可以声明一个无缓冲的chan,那个协程的阻塞先完成就先往里写入数据。
代码案例如下:
package main
import (
"fmt"
"sync"
"time"
)
func main() {
wg := sync.WaitGroup{}
c := make(chan struct{})
for i := 0; i < 10; i++ {
wg.Add(1)
go func(num int, close <-chan struct{}) {
defer wg.Done()
<-close
fmt.Println(num)
}(i, c)
}
if WaitTimeout(&wg, time.Second*5) {
close(c)
fmt.Println("timeout exit")
}
time.Sleep(time.Second * 10)
}
func WaitTimeout(wg *sync.WaitGroup, timeout time.Duration) bool {
// 要求手写代码
// 要求sync.WaitGroup支持timeout功能
// 如果timeout到了超时时间返回true
// 如果WaitGroup自然结束返回false
ch := make(chan bool, 1)
go time.AfterFunc(timeout, func() {
ch <- true
})
go func() {
wg.Wait()
ch <- false
}()
return <-ch
}
扫码关注公众号,获取更多优质内容。