golang接口IP限流,IP黑名單,IP白名單的實(shí)例
增加中間件
可以選擇普通模式和LUA腳本模式,建議選擇普通模式,實(shí)際上不需要控制的那么精確。
package Middlewares import ( "github.com/gin-gonic/gin" "strconv" "time" "voteapi/pkg/app/response" "voteapi/pkg/gredis" "voteapi/pkg/util" ) const IP_LIMIT_NUM_KEY = "ipLimit:ipLimitNum" const IP_BLACK_LIST_KEY = "ipLimit:ipBlackList" var prefix = "{gateway}" var delaySeconds int64 = 60 // 觀察時(shí)間跨度,秒 var maxAttempts int64 = 10000 // 限制請(qǐng)求數(shù) var blackSeconds int64 = 0 // 封禁時(shí)長(zhǎng),秒,0-不封禁 func GateWayPlus() gin.HandlerFunc { return func(c *gin.Context) { path := c.FullPath() clientIp := c.ClientIP() // redis配置集群時(shí)必須 param := make(map[string]string) param["path"] = path param["clientIp"] = clientIp if !main(param) { c.Abort() response.JsonResponseError(c, "當(dāng)前IP請(qǐng)求過(guò)于頻繁,暫時(shí)被封禁~") } } } func main(param map[string]string) bool { // 預(yù)知的IP黑名單 var blackList []string if util.InStringArray(param["clientIp"], blackList) { return false } // 預(yù)知的IP白名單 var whiteList []string if util.InStringArray(param["clientIp"], whiteList) { return false } blackKey := prefix + ":" + IP_BLACK_LIST_KEY limitKey := prefix + ":" + IP_LIMIT_NUM_KEY curr := time.Now().Unix() item := util.Md5(param["path"] + "|" + param["clientIp"]) return normal(blackKey, limitKey, item, curr) } // 普通模式 func normal(blackKey string, limitKey string, item string, time int64) (res bool) { if blackSeconds > 0 { timeout, _ := gredis.RawCommand("HGET", blackKey, item) if timeout != nil { to, _ := strconv.Atoi(string(timeout.([]uint8))) if int64(to) > time { // 未解封 return false } // 已解封,移除黑名單 gredis.RawCommand("HDEL", blackKey, item) } } l, _ := gredis.RawCommand("HGET", limitKey, item) if l != nil { last, _ := strconv.Atoi(string(l.([]uint8))) if int64(last) >= maxAttempts { return false } } num, _ := gredis.RawCommand("HINCRBY", limitKey, item, 1) if ttl, _ := gredis.TTLKey(limitKey); ttl == int64(-1) { gredis.Expire(limitKey, int64(delaySeconds)) } if num.(int64) >= maxAttempts && blackSeconds > 0 { // 加入黑名單 gredis.RawCommand("HSET", blackKey, item, time+blackSeconds) // 刪除記錄 gredis.RawCommand("HDEL", limitKey, item) } return true } // LUA腳本模式 // 支持redis集群部署 func luaScript(blackKey string, limitKey string, item string, time int64) (res bool) { script := ` local blackSeconds = tonumber(ARGV[5]) if(blackSeconds > 0) then local timeout = redis.call('hget', KEYS[1], ARGV[1]) if(timeout ~= false) then if(tonumber(timeout) > tonumber(ARGV[2])) then return false end redis.call('hdel', KEYS[1], ARGV[1]) end end local last = redis.call('hget', KEYS[2], ARGV[1]) if(last ~= false and tonumber(last) >= tonumber(ARGV[3])) then return false end local num = redis.call('hincrby', KEYS[2], ARGV[1], 1) local ttl = redis.call('ttl', KEYS[2]) if(ttl == -1) then redis.call('expire', KEYS[2], ARGV[4]) end if(tonumber(num) >= tonumber(ARGV[3]) and blackSeconds > 0) then redis.call('hset', KEYS[1], ARGV[1], ARGV[2] + ARGV[5]) redis.call('hdel', KEYS[2], ARGV[1]) end return true ` result, err := gredis.RawCommand("EVAL", script, 2, blackKey, limitKey, item, time, maxAttempts, delaySeconds, blackSeconds) if err != nil { return false } if result == int64(1) { return true } else { return false } }
補(bǔ)充:golang實(shí)現(xiàn)限制每秒多少次的限頻操作
前言
一些函數(shù)的執(zhí)行可能會(huì)限制頻率,比如某個(gè)api接口要求每秒最大請(qǐng)求30次。下面記錄了自己寫(xiě)的限頻和官方的限頻
代碼
// 加鎖限頻,輸出次數(shù)大概率小于最大值 func ExecLimit(lastExecTime *time.Time, l *sync.RWMutex ,maxTimes int, perDuration time.Duration, f func()) { l.Lock() defer l.Unlock() // per times cost time(s) SecondsPerTimes := float64(perDuration) / float64(time.Second) / float64(maxTimes) now := time.Now() interval := now.Sub(*lastExecTime).Seconds() if interval < SecondsPerTimes { time.Sleep(time.Duration(int64((SecondsPerTimes-interval)*1000000000)) * time.Nanosecond) } f() *lastExecTime = time.Now() } // 官方的,需要引用 "golang.org/x/time/rate" // 基本上可以達(dá)到滿(mǎn)值,比自己寫(xiě)的更優(yōu) func ExecLimit2(l *rate.Limiter, f func()) { go func() { l.Wait(context.Background()) f() }() }
使用
func TestExecLimit(t *testing.T) { runtime.GOMAXPROCS(runtime.NumCPU()) go func() { var lastExecTime time.Time var l sync.RWMutex for { ExecLimit(&lastExecTime, &l, 10, time.Second, func() { fmt.Println("do") }) } }() select { case <-time.After(1 * time.Second): fmt.Println("1秒到時(shí)") } } func TestExecLimit2(t *testing.T) { runtime.GOMAXPROCS(runtime.NumCPU()) l := rate.NewLimiter(1, 30) go func() { for { ExecLimit2(l, func() { fmt.Println("do") }) } }() select { case <-time.After(1 * time.Second): fmt.Println("1秒到時(shí)") } }
輸出:
一秒內(nèi)輸出了<=10次 "do"
如何在多節(jié)點(diǎn)服務(wù)中限制頻
上述使用,定義在某個(gè)服務(wù)節(jié)點(diǎn)的全局變量lastExecTime僅僅會(huì)對(duì)該服務(wù)的函數(shù)f()操作限頻,如果在負(fù)載均衡后,多個(gè)相同服務(wù)的節(jié)點(diǎn),對(duì)第三方的接口累計(jì)限頻,比如三個(gè)服務(wù)共同拉取第三方接口,合計(jì)限頻為30次/s.
則,必須將lastExecTime的獲取,從redis等共享中間件中獲取,而不應(yīng)該從任何一個(gè)單點(diǎn)服務(wù)獲取。
以上為個(gè)人經(jīng)驗(yàn),希望能給大家一個(gè)參考,也希望大家多多支持腳本之家。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教。
相關(guān)文章
詳解Golang time包中的結(jié)構(gòu)體time.Time
在日常開(kāi)發(fā)過(guò)程中,會(huì)頻繁遇到對(duì)時(shí)間進(jìn)行操作的場(chǎng)景,使用 Golang 中的 time 包可以很方便地實(shí)現(xiàn)對(duì)時(shí)間的相關(guān)操作,本文先講解一下 time 包中的結(jié)構(gòu)體 time.Time,需要的朋友可以參考下2023-07-07goland?-sync/atomic原子操作小結(jié)
這篇文章主要介紹了goland?-sync/atomic原子操作,原子操作能夠保證執(zhí)行期間是連續(xù)且不會(huì)被中斷(變量不會(huì)被其他修改,mutex可能存在被其他修改的情況),本文給大家介紹的非常詳細(xì),需要的朋友參考下2022-08-08Hugo?Config模塊構(gòu)建實(shí)現(xiàn)源碼剖析
這篇文章主要為大家介紹了Hugo?Config模塊構(gòu)建實(shí)現(xiàn)源碼剖析,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-02-02Golang源碼分析之golang/sync之singleflight
golang/sync庫(kù)拓展了官方自帶的sync庫(kù),提供了errgroup、semaphore、singleflight及syncmap四個(gè)包,本次先分析第一個(gè)包errgroup的源代碼,下面這篇文章主要給大家介紹了關(guān)于Golang源碼分析之golang/sync之singleflight的相關(guān)資料,需要的朋友可以參考下2022-11-11GoZero實(shí)現(xiàn)數(shù)據(jù)庫(kù)MySQL單例模式連接的簡(jiǎn)單示例
在 GoZero 框架中實(shí)現(xiàn)數(shù)據(jù)庫(kù)的單例連接可以通過(guò)以下步驟來(lái)完成,GoZero 使用 gorm 作為默認(rèn)的數(shù)據(jù)庫(kù)操作框架,接下來(lái)我會(huì)展示一個(gè)簡(jiǎn)單的單例模式實(shí)現(xiàn),需要的朋友可以參考下2025-02-02GO語(yǔ)言基本數(shù)據(jù)類(lèi)型總結(jié)
這篇文章主要介紹了GO語(yǔ)言基本數(shù)據(jù)類(lèi)型,較為詳細(xì)的總結(jié)了GO語(yǔ)言的基本數(shù)據(jù)類(lèi)型,對(duì)于GO語(yǔ)言的學(xué)習(xí)有一定的借鑒參考價(jià)值,需要的朋友可以參考下2014-12-12