背景 - 限制接口调用次数:
提供内部调用的全量拉取数据接口,以用户appid作为维度区分,一定时间段内(比如1小时),查询某一接口的appid和limit、offset等组合参数只能查询一次,多次查询相同组合参数则返回错误值。
思路:
采用string类型,key就是模块名:api_limiter_xxx(根据需要自定义key名称),value就是当前可以访问的页数(每次访问成功都自增1)。第一次访问某接口时,若key不存在,那么就添加;若存在,则获取当前redis中对应key的"页数值",并与入参Limit和Offset计算后的页数结果进行比较,若相等则调用成功,反之调用失败。
其中,在设置key和value的时候也要设置"过期时间"。所以关键在于 SETNX 的使用,若存在,则匹配计算的页数与redis中的计数器页数是否相等,若不存在,则设置kv。
注意:其中每次set key的值的时候,过期时间的处理。这里在set之前先通过TTL获取key剩余的过期时间,然后set时再带入接下来剩余的时间即可,避免每次都重新设置固定的过期时间。
- 初始时,redisPageCnt = 1
- 首次调用接口时,Limit=10,Offset=0,calculateCnt = (Limit + Offset) / Limit = 1;redisPageCnt=1
- 此时 calculateCnt = redisPageCnt 相等,redisPageCnt++,redisPageCnt = 2
- 再次调用接口时,Limit=10,Offset=10,calculateCnt = (Limit + Offset) / Limit = 2;redisPageCnt=2
- 此时 calculateCnt = redisPageCnt 相等,redisPageCnt++,redisPageCnt = 3
- ...
这样的话就可以按页数从前往后递增,每页在限制的超时时间内(1小时)访问一次。在key的超时时间内,访问过的页数也不能再次访问,除非redis中的key超时时间失效。
请求示例:
var req = TestReq{ Appid: 1256299843, Limit: 10, Offset: 20, }
packagemainimport ( "fmt""strconv""time""github.com/go-redis/redis") // 限流:针对含有分页参数的接口,对于不同"Limit和Offset"组合做限制,避免某个用户大量请求影响后台服务处理其他正常请求// 示例:// var req = TestReq{// Appid: 1256299843,// Limit: 10,// Offset: 20,// }typeTestReqstruct { Appiduint64LimitintOffsetint} funcAPILimiter(reqTestReq) (errerror) { ifreq.Limit!=10 { // 校验入参,暂时写死,根据情况修改fmt.Println("req.Limit format err: ") // return response ...return } redisCli :=redis.NewClient(&redis.Options{ Addr: "xxx.xxx.xxx.xxx:3306", // ip:portPassword: "yundingyd", PoolSize: 10, }) // pong, err := redisCli.Ping().Result()// 不存在 -> 初始化;存在 -> 不处理redisKey :="api_limiter_"+strconv.FormatUint(req.Appid, 10) isOk, err :=redisCli.SetNX(redisKey, 1, time.Hour).Result() // 限制超时1小时,Result()针对不同命令自动转换对应结果iferr!=nil&&err!=redis.Nil&&isOk==false { fmt.Println("APILimiter SetNX err: ", err) // return response ...return } calculatePageCnt := (req.Limit+req.Offset) /req.LimitredisPageCnt, err :=redisCli.Get(redisKey).Int() iferr!=nil&&err!=redis.Nil&&isOk==false { fmt.Println("APILimiter redisCli.Get err: ", err) // return response ...return } ifcalculatePageCnt!=redisPageCnt { fmt.Printf("接口调用失败:当前Limit和Offset对应的页数无效: limit=%d, offset=%d, calculateCnt=%d, redisPageCnt=%d\n", req.Limit, req.Offset, calculatePageCnt, redisPageCnt) // return response ...return } deferfunc() { iferr==nil { // 具体的业务逻辑,期间可能会产生err,只有err为空时才执行以下逻辑~// 先查询当前key的剩余过期时间timeDuration, err :=redisCli.TTL(redisKey).Result() iferr!=nil&&err!=redis.Nil { fmt.Println("redisCli.TTL err: ", err) // return response ...return } // 再设置当前key的剩余过期时间isOk, err :=redisCli.Set(redisKey, redisPageCnt+1, timeDuration).Result() // page + 1iferr!=nil&&err!=redis.Nil&&isOk!="OK" { fmt.Println("redisCli.Set err: ", err) // return response ...return } fmt.Println("接口调用成功!!!") } }() // 具体的业务逻辑(期间可能会产生err,除非期间err为nil,否则defer函数不执行 page+1 等操作):// ...return} funcmain() { varreq=TestReq{ Appid: 1256299843, Limit: 10, Offset: 20, } err :=APILimiter(req) iferr!=nil { fmt.Println("APILimiter err: ", err) return } }