网站的运营中,经常会遇到需要对用户访问次数做限制的情况,比如非常典型的是对于某些付费访问服务,需要对访问频率做比较精确的限制,比如单个用户每天只允许访问多少次,然后每小时只允许访问多少次等等,ratelimit就是专门针对这种情况而设计。
对于这种需求,目前相对较为简单的是用rdeis来实现,与采用redis的方案相比:
1)性能:每秒约能处理250万次,与之对比redis大约在10万左右。
2)简单,无依赖,无需安装任何第三方软件(诸如redis),可以以包的形式直接嵌入项目。
3)同样支持持久化,可定期把历史数据备份到本地磁盘。
##使用案例如下
```go
package main
import (
"fmt"
"log"
"strconv"
"sync"
"time"
"github.com/yudeguang/ratelimit"
)
var num int //因并发问题num比实际数量小
func main() {
log.SetFlags(log.Lshortfile | log.Ltime)
test1()
test2()
}
func test1() {
fmt.Println("\r\n测试1,加载备份数据并自动定期备份以及性能测试:")
//初始化访问规则,支持用多个规则形成的复杂规则,规则必须至少包含一条规则
//支持从本地磁盘加载备份好的历史记录,并支持定期自动备份
r := ratelimit.NewRule()
r.AddRule(time.Second*10, 5) //每10秒只允许访问5次
r.AddRule(time.Minute*30, 50) //每30分钟只允许访问50次
r.AddRule(time.Hour*24, 500) //每天只允许访问500次
err := r.LoadingAndAutoSaveToDisc("test1", time.Second*10) //设置10秒备份一次(不填写则默认60秒备份一次),备份到程序当前文件夹下,文件名为test1.ratelimit
if err == nil {
log.Println("加载历史访问记录成功")
} else {
log.Println(err)
}
//构建若干个用户,模拟用户访问
var users = make(map[string]bool)
for i := 1; i < 1000; i++ {
users["user_"+strconv.Itoa(i)] = true
}
begin := time.Now()
//模拟多个协程访问
chanNum := 200
var wg sync.WaitGroup
wg.Add(chanNum)
for i := 0; i < chanNum; i++ {
go func(i int, wg *sync.WaitGroup) {
for ii := 0; ii < 50; ii++ {
for user := range users {
for {
if r.AllowVisit(user) {
num++
} else {
num++
break
}
}
}
}
wg.Done()
}(i, &wg)
}
//所有线程结束,完工
wg.Wait()
log.Println("性能测试完成,每秒约完成:", num/int(time.Now().Sub(begin).Seconds()), "次操作")
err = r.SaveToDiscOnce("test2") //在自动备份的同时,还支持手动备份,一般在程序要退出时调用此函数
if err == nil {
log.Println("完成手动数据备份")
} else {
log.Println(err)
}
}
func test2() {
fmt.Println("\r\n测试2,模拟用户访问并打印:")
r := ratelimit.NewRule()
r.AddRule(time.Second*10, 5) //每10秒只允许访问5次
r.AddRule(time.Minute*30, 50) //每30分钟只允许访问50次
r.AddRule(time.Hour*24, 500) //每天只允许访问500次
r.LoadingAndAutoSaveToDisc("test2", time.Second*10) //设置10秒备份一次(不填写则默认60秒备份一次),备份到程序当前文件夹下,文件名为test2.ratelimit
//构建若干个用户,模拟用户访问
users := []string{"andy", "小余", "130x"}
for _, user := range users {
fmt.Println("\r\n开始模拟以下用户访问:", user)
for {
if r.AllowVisit(user) {
log.Println(user, "访问1次,剩余:", r.RemainingVisits(user))
} else {
log.Println(user, "访问过多,稍后再试")
break
}
time.Sleep(time.Second * 1)
}
}
//打印所有用户访问数据情况
fmt.Println("\r\n开始打印所有用户在相关时间段内详细的剩余访问次数情况:\r\n")
for _, user := range users {
fmt.Println(user)
fmt.Println(" 概述:", r.RemainingVisits(user))
fmt.Println(" 具体:")
r.PrintRemainingVisits(user)
fmt.Println("")
}
}
```
结果如下:
```go
测试1,加载备份数据并自动定期备份以及性能测试:
22:45:27 t.go:31: 加载历史访问记录成功
22:45:31 t.go:65: 性能测试完成,每秒约完成: 2495806 次操作
22:45:31 t.go:68: 完成手动数据备份
测试2,模拟用户访问并打印:
开始模拟以下用户访问: andy
22:45:31 t.go:86: andy 访问1次,剩余: [4 49 499]
22:45:32 t.go:86: andy 访问1次,剩余: [3 48 498]
22:45:33 t.go:86: andy 访问1次,剩余: [2 47 497]
22:45:34 t.go:86: andy 访问1次,剩余: [1 46 496]
22:45:35 t.go:86: andy 访问1次,剩余: [0 45 495]
22:45:36 t.go:88: andy 访问过多,稍后再试
开始模拟以下用户访问: 小余
22:45:36 t.go:86: 小余 访问1次,剩余: [4 49 499]
22:45:37 t.go:86: 小余 访问1次,剩余: [4 48 498]
22:45:38 t.go:86: 小余 访问1次,剩余: [3 47 497]
22:45:39 t.go:86: 小余 访问1次,剩余: [2 46 496]
22:45:40 t.go:86: 小余 访问1次,剩余: [1 45 495]
22:45:41 t.go:86: 小余 访问1次,剩余: [0 44 494]
22:45:42 t.go:88: 小余 访问过多,稍后再试
开始模拟以下用户访问: 130x
22:45:42 t.go:86: 130x 访问1次,剩余: [4 49 499]
22:45:43 t.go:86: 130x 访问1次,剩余: [3 48 498]
22:45:44 t.go:86: 130x 访问1次,剩余: [2 47 497]
22:45:45 t.go:86: 130x 访问1次,剩余: [1 46 496]
22:45:46 t.go:86: 130x 访问1次,剩余: [0 45 495]
22:45:47 t.go:88: 130x 访问过多,稍后再试
开始打印所有用户在相关时间段内详细的剩余访问次数情况:
andy
概述: [5 45 495]
具体:
andy 在 10s 内共允许访问 5 次,剩余 5
andy 在 30m0s 内共允许访问 50 次,剩余 45
andy 在 24h0m0s 内共允许访问 500 次,剩余 495
小余
概述: [1 44 494]
具体:
小余 在 10s 内共允许访问 5 次,剩余 1
小余 在 30m0s 内共允许访问 50 次,剩余 44
小余 在 24h0m0s 内共允许访问 500 次,剩余 494
130x
概述: [0 45 495]
具体:
130x 在 10s 内共允许访问 5 次,剩余 0
130x 在 30m0s 内共允许访问 50 次,剩余 45
130x 在 24h0m0s 内共允许访问 500 次,剩余 495
```
如果要封装成http服务, 那么这个http服务会变成单点, 在高并发下这个关键路径会成为系统瓶颈.
其次, 性能会比业务直接连 redis 性能上要差, 因为 `退化`成了 http调用, 一次http调用明显比一次 redis call 要差.
再其次, 虽然标榜不需要redis, 但是还是要平白无故多搞一个独立的 http 服务
再者, 都是把单机的算法进行封装, 为什么不用golang自带的ratelimit呢, 连第三方包都不用引用, 文档齐全查资料多经受住了国内外项目的考验. 而且你这个包中 `把历史数据备份到本地磁盘` 的功能其实 nobody care. 实在需要看这个东西直接存日志就行了
感觉 LZ 实际的经验还是不太足够啊.....
#6
更多评论
…………
你不用redis我能理解。
但是,这种功能肯定是一个网关啊……
不带http的反向代理测出来的数据毫无意义。
带了http的话,会发现走内存没啥意义。
而且这种功能肯定是实现一个接口,然后写一个内存驱动一个redis驱动……
#2