go语言实现,用户登录:限制一分钟内密码错误的次数(固定时间窗口限流算法)

1. 前言

限流在做服务端开发时应该并不少见,最先映入脑海的应该有淘宝 双11,过年回家通过 12306 买火车票的场景等。

这里简单列举一种业务场景:用户的账号密码登录,为防止恶意程序尝试暴力破解密码,频繁的调用登录请求,可以添加一系列的防护措施,如:添加验证码校验,设置时间段内可尝试错误请求的次数等。

2. go 实现

借助 go-cache 开源仓库来做缓存控制。

package main

import (
	"fmt"
	"github.com/patrickmn/go-cache"
	"time"
)

// 使用
func main() {
    
    
    // 设置一分钟内可以用户a可以尝试10次
	c := NewCache(10, time.Minute, time.Second)
	key := "a"
	go func() {
    
    
		ticker := time.NewTicker(time.Millisecond * 200)
		for {
    
    
			select {
    
    
			case <-ticker.C:
				fmt.Println("add")
				c.Add(key)
			}
		}
	}()

	for {
    
    
		if c.Limit(key) {
    
    
			fmt.Println("end")
			return
		}
	}
}


type Cache struct {
    
    
	c          *cache.Cache
	countLimit int
}

// cleanupInterval 定时清理缓存的时间
func NewCache(countLimit int, timeLimit, cleanupInterval time.Duration) *Cache {
    
    
	res := new(Cache)
	res.c = cache.New(timeLimit, cleanupInterval)
	res.countLimit = countLimit

	return res
}

func (this *Cache) Limit(key string) bool {
    
    
	count, ok := this.c.Get(key)
	if !ok {
    
    
		return false
	}
	curCount := count.(int)
	if curCount >= this.countLimit {
    
    
		return true
	}
	return false
}

func (this *Cache) Add(key string) {
    
    
	count, expiration, ok := this.c.GetWithExpiration(key)
	if !ok {
    
    
		this.c.SetDefault(key, 1)
		return
	}

    // 这里的过期时间重新算一下
	duration := expiration.Sub(time.Now())
	if duration <= 0 {
    
    
		return
	}

	curCount := count.(int)
	this.c.Set(key, curCount+1, duration)
	return
}

上述代码输出 10add 后输出 end 结束程序。

3. 限流策略

限流策略有很多,这里介绍几种常见的,还是要根据实际情况进行选择。

3.1 固定时间窗口限流

很粗糙,两个时间窗口内,若前一次突发流量在最末尾,后一次突发流量在最前端,还是有可能造成系统瘫痪的。

3.2 滑动时间窗口

对固定时间窗口算法的一种改进,流量经过滑动时间窗口算法整形之后,可以保证任意时间窗口内,都不会超过最大允许的限流值,从流量曲线上来看会更加平滑,可以部分解决上面提到的临界突发流量问题。

3.3 令牌桶

  1. 接口限制 t 秒内最大访问次数为 n,则每隔 t/n 秒会放一个 token 到桶中;
  2. 桶中最多可以存放 b 个 token,如果 token 到达时令牌桶已经满了,那么这个 token 会被丢弃;
  3. 接口请求会先从令牌桶中取 token,拿到 token 则处理接口请求,拿不到 token 则执行限流。

匀速生成令牌,直到达到桶的最大值,每来个请求就拿走一个,拿不到的就舍弃。

3.4 漏桶

一共能容纳多少,进出都做记录,桶满了就不再给进来了。

4. 如何配置合理的限流规则

限流规则包含三个部分:时间粒度,接口粒度,最大限流值。

限流的调整,热更新(开启/关闭限流,调整限流规则,更换限流算法)。

5. 总结

本文主要以一个业务来简单的描述了一下限流(固定时间窗口),并附带了一个 go 版本的实现。

限流方式很多,还是要找到最适合本身业务的限流方式,难的不是实现,而是怎么去限流,怎么设计。

6. 参考

InfoQ 微服务接口限流的设计与思考

2022/10/18 更新

Add 方法 和 Limit 方法合并。

package main

import (
	"fmt"
	"github.com/patrickmn/go-cache"
	"time"
)

func main() {
    
    
	c := NewCache(10, time.Minute, time.Minute)
	key := "a"

	ticker := time.NewTicker(time.Millisecond * 200)
	i := 0
	for {
    
    
		select {
    
    
		case <-ticker.C:
			i++
			if c.Limit1(key) {
    
    
				fmt.Println("end:", i)
				return
			}
		}
	}
}

func (this *Cache) Limit1(key string) bool {
    
    
	count, expiration, ok := this.c.GetWithExpiration(key)
	if !ok {
    
    
		this.c.SetDefault(key, 1)
		return false
	}

	duration := expiration.Sub(time.Now())
	if duration <= 0 {
    
     // 过期了重新设置值
		this.c.SetDefault(key, 1)
		fmt.Println("过期了")
		return false
	}

	curCount := count.(int)
	if curCount >= this.countLimit {
    
    
		return true
	}
	this.c.Set(key, curCount+1, duration)
	return false
}

输出结果:end:11

猜你喜欢

转载自blog.csdn.net/DisMisPres/article/details/127305658