Using Redis's Incr can easily implement a rate limiter.
There is also a detailed example in the redis official documentation.
FUNCTION LIMIT_API_CALL(ip)
ts = CURRENT_UNIX_TIME()
keyname = ip+":"+ts
current = GET(keyname)
IF current != NULL AND current > 10 THEN
ERROR "too many requests per second"
END
IF current == NULL THEN
MULTI
INCR(keyname, 1)
EXPIRE(keyname, 1)
EXEC
ELSE
INCR(keyname, 1)
END
PERFORM_API_CALL()
An Example
Suppose there is a service that can only accept no more than 10 requests from a user within 1 minute. At this time, we can set the user's IP address as the key. Every time the user requests, the program goes to redis to get the value of the key. If it is greater than or equal to 10, it returns an error; otherwise, increment the value corresponding to the key by 1. If the value is 0, then set an expiration time of 1 minute for the key.
But now there is a requirement. We can push a message to the user within a specified time, but the user is required to receive only 1 message per minute, no more than 5 messages per hour, and no more than 10 messages per day.
For example, I submit a piece of data to this interface now, requiring to send a piece of data to a user at 2019-11-11 11:11:11. Then when I submit another piece of data, requiring to send a piece of data to the same user at 2019-11-11 11:11:12, the interface will return an error.
At this point, using only Incr cannot satisfy this requirement.
Solution
Use set or zset to use user ip + send date as key, convert send time to seconds of the day as value, and insert into set or zset. Every time information is submitted to the user, all send times in set can be obtained, and then compared one by one. If conditions are not met, error is returned.
The following is pseudo code implementation
function RateLimit(ip,sendtime){
// Get send date based on send time
sendDate = getSendDate(sendtime)
// Get seconds difference between send time and 0 o'clock of the day
second = getSendSecond(sendtime)
// List all send times in the send date
secondList = listSeconds(ip + sendDate)
// Check successfully
if check(second,secondList) {
// Add send time to cache
return addToList(ip + sendDate,second)
}
return false
}
The following is golang implementation
func RateLimit(ctx context.Context, ip string, sendTime time.Time) error {
sendTime = sendTime.UTC()
if sendTime.Before(time.Now()) {
return nil
}
zeroStr := sendTime.Format("2006-01-02")
key := ip + zeroStr
zero, _ := time.Parse("2006-01-02", zeroStr)
// Get seconds from send time to today's time
second := int(sendTime.Sub(zero).Seconds())
ress, err := cache.SMembers(ctx, key)
if err != nil {
return err
}
// Process return parameters, convert []string to []int
var sends []int
for _, v := range ress {
if vv, err := strconv.Atoi(v); err == nil {
sends = append(sends, vv)
}
}
if len(sends) >= 10 {
return errors.ErrMsg1DayLimit
}
var expire bool
if len(sends) == 0 {
expire = true
}
var hourCount int
for _, s := range sends {
if math.Abs(float64(second-s)) < 60 {
return errors.ErrMsg1MinuteLimit
}
if math.Abs(float64(second-s)) < 3600 {
hourCount++
}
}
if hourCount > 5 {
return errors.ErrMsg1HourLimit
}
// Asynchronous update
go func() {
// Start transaction
t := cache.Pipeline()
defer t.Close()
t.SAdd(context.Background(), key, []byte(strconv.Itoa(second)))
if expire {
ttl := zero.Add(time.Hour*24).Sub(time.Now()).Seconds()
if ttl <= 0 {
t.Rollback()
return
}
t.Expire( key, int64(ttl))
}
t.Commit()
}()
return nil
}
The above can implement a rate limiter for a specified time.