Realize the daily limit of users in Go (for example, you can only receive benefits three times a day)

Posted by hazel999 on Tue, 11 Jan 2022 06:24:39 +0100

If you write a bug management system and use this PeriodLimit, you can limit each tester to only mention one bug to you every day. Is it much easier to work? 😛

The essential reason for the popularity of microservice architecture today is to reduce the overall complexity of the system and spread the system risks to subsystems, so as to maximize the stability of the system. After being divided into different subsystems through domain division, each subsystem can develop, test and release independently, and the R & D rhythm and efficiency can be significantly improved.

But it also brings problems. For example, the call link is too long, the complexity of deployment architecture is increased, and various middleware need to support distributed scenarios. In order to ensure the normal operation of microservices, service governance is indispensable, usually including current limiting, degradation and fusing.

Current limiting refers to limiting the frequency of interface calls to avoid exceeding the upper limit of bearing and dragging down the system. For example:

  1. E-commerce spike scenario
  2. API current limit for different merchants

Common current limiting algorithms include:

  • Fixed time window current limiting
  • Sliding time window current limiting
  • Leakage barrel current limiting
  • Token bucket current limiting

This paper mainly explains the fixed time window current limiting algorithm, and the main usage scenarios, such as:

  • Each mobile phone number can only send 5 verification code SMS every day
  • Each user can only try the password 3 times in a row per hour
  • Each member can only receive benefits three times a day

working principle

Starting from a certain time point, the number of requests for each request is + 1. At the same time, judge whether the number of requests in the current time window exceeds the limit. If it exceeds the limit, reject the request, and then clear the counter at the beginning of the next time window to wait for the request.

Advantages and disadvantages

advantage

The implementation is simple and efficient. It is especially suitable for limiting scenarios, such as a user can only send 10 articles a day, can only send SMS verification code 5 times, and can only try to log in 5 times. Such scenarios are very common in practical business.

shortcoming

The disadvantage of fixed time window current limiting is that it can not handle the burst scenario of critical area requests.

Assuming that the current is limited to 100 requests per 1s, the user initiates 200 requests within 1 s from the middle 500ms. At this time, all 200 requests can pass. This is inconsistent with our expectation of 100 times of 1s current limiting. The root cause is that the fine granularity of current limiting is too coarse.

Go zero code implementation

core/limit/periodlimit.go

redis expiration time is used in go zero to simulate a fixed time window.

redis lua script:

-- KYES[1]:Current limiter key
-- ARGV[1]:qos,Maximum requests per unit time
-- ARGV[2]:Unit current limiting window time
-- Maximum number of requests,be equal to p.quota
local limit = tonumber(ARGV[1])
-- Window is a unit current limiting cycle,Here we use expiration to simulate the window effect,be equal to p.permit
local window = tonumber(ARGV[2])
-- Number of requests+1,Total number of get requests
local current = redis.call("INCRBY",KYES[1],1)
-- If this is the first request,Set the expiration time and return success
if current == 1 then
  redis.call("expire",KYES[1],window)
  return 1
-- If the current number of requests is less than limit Success is returned
elseif current < limit then
  return 1
-- If the current number of requests==limit The last request is returned
elseif current == limit then
  return 2
-- Number of requests>limit Failure is returned
else
  return 0
end

Fixed time window current limiter definition

type (
  // PeriodOption defines the method to customize a PeriodLimit.
  // Common option parameter patterns in go
  // If there are many parameters, it is recommended to use this mode to set parameters
  PeriodOption func(l *PeriodLimit)

  // A PeriodLimit is used to limit requests during a period of time.
  // Fixed time window current limiter
  PeriodLimit struct {
    // Window size in seconds
    period     int
    // Request upper limit
    quota      int
    // storage
    limitStore *redis.Redis
    // key prefix
    keyPrefix  string
    // Linear current limiting. When this option is enabled, periodic current limiting can be realized
    // For example, when quota=5, the actual value of quota may be 5.4.3.2.1, showing periodic changes
    align      bool
  }
)

Note the align parameter. When align=true, the request upper limit will change periodically.
For example, when quota=5, the actual quota may be 5.4.3.2.1, showing periodic changes

Current limiting logic

In fact, the current limiting logic is implemented in the lua script above. It should be noted that the return value

  • 0: indicates an error, such as redis failure or overload
  • 1: Allow
  • 2: Yes, but the current window has reached the upper limit. If it is a batch business, you can sleep and wait for the next window (the author considers it very carefully)
  • 3: Refuse
// Take requests a permit, it returns the permit state.
// Perform current limiting
// Note the return value:
// 0: indicates an error, such as redis failure or overload
// 1: Allow
// 2: Allowed, but the upper limit has been reached in the current window
// 3: Refuse
func (h *PeriodLimit) Take(key string) (int, error) {
  // Execute lua script
  resp, err := h.limitStore.Eval(periodScript, []string{h.keyPrefix + key}, []string{
    strconv.Itoa(h.quota),
    strconv.Itoa(h.calcExpireSeconds()),
  })
  
  if err != nil {
    return Unknown, err
  }

  code, ok := resp.(int64)
  if !ok {
    return Unknown, ErrUnknownCode
  }

  switch code {
  case internalOverQuota:
    return OverQuota, nil
  case internalAllowed:
    return Allowed, nil
  case internalHitQuota:
    return HitQuota, nil
  default:
    return Unknown, ErrUnknownCode
  }
}

This fixed window may be used to limit the current limit. For example, a user can only send verification code SMS five times a day. At this time, we need to correspond to the Chinese time zone (GMT+8), and the current limit time should start from zero. At this time, we need additional alignment (set align to true).

// Calculate the expiration time, that is, the window time size
// If align==true
// Linear current limiting. When this option is enabled, periodic current limiting can be realized
// For example, when quota=5, the actual value of quota may be 5.4.3.2.1, showing periodic changes
func (h *PeriodLimit) calcExpireSeconds() int {
  if h.align {
    now := time.Now()
    _, offset := now.Zone()
    unix := now.Unix() + int64(offset)
    return h.period - int(unix%int64(h.period))
  }

  return h.period
}

Project address

https://github.com/zeromicro/go-zero

Welcome to go zero and star support us!

Wechat communication group

Focus on the "micro service practice" official account and click on the exchange group to get the community community's two-dimensional code.

Topics: Go Microservices go-zero