This article has participated in the activity of “New person creation Ceremony”, and started the road of digging gold creation together.

background

The user needs to perform OCR identification. In order to prevent the interface from being flushed, there is a limit (XXX calls per minute). After some research, it was decided to use Redis INCR and EXPIRE to implement this functionality

Note: The following code uses golang

First edition code

// Perform the OCR call
func (o *ocrSvc)doOcr(ctx context.Context,uid int)(interface,err){
	If the number of calls exceeds the specified limit, the request is rejected
	ok,err := o.checkMinute(uid)
	iferr ! =nil {
		return nil,err
	}
	if! ok {return nil,errors.News("frequently called")}// Perform third-party OCR calls (pseudocode)
	ocrRes,err := doOcrByThird()
	iferr ! =nil {
		return nil,err
	}
	// The incr operation is executed if the call succeeds
	iferr := o.redis.Incr(ctx,buildUserOcrCountKey(uid)); err! =nil{
	   return nil,err
	}
	return ocrRes,nil
}

// Check whether the number of calls per minute is exceeded
func (o *ocrSvc)checkMinute (ctx context.Context,uid int) (bool, error) {
	minuteCount, err := o.redis.Get(ctx, buildUserOcrCountKey(uid))
	iferr ! =nil && !errors.Is(err, eredis.Nil) {
		elog.Error("checkMinute: redis.Get failed", zap.Error(err))
		return false, constx.ErrServer
	}
	if errors.Is(err, eredis.Nil) {
		// Expired, or there is no record of the number of calls made by the user (set initial value to 0, expiration time to 1 minute)
		o.redis.Set(ctx, buildUserOcrCountKey(uid),0,time.Minute)
		return true.nil
	}
	// The number of calls per minute has exceeded
	if cast.ToInt(minuteCount) >= config.UserOcrMinuteCount() {
		elog.Warn("checkMinute: user FrequentlyCalled", zap.Int64("uid", uid), zap.String("minuteCount", minuteCount))
		return false.nil
	}
	return true.nil
}
Copy the code

Break down

This version of the code I first do not say what problems exist, you can first YY

Description:

  1. Assume that the number of OCR calls by the current user does not exceed. But the TTL in Redis has 1 second left
  2. The third party OCR is then called for identification
  3. After successful identification, the number of calls +1. Redis sets the key value to 1, TTL to -1, TTL to -1, TTL to -1 (important things to say three times)
  4. This is where a bug occurs: the number of calls made by the user keeps increasing and does not expire until the user’s request is rejected

conclusion

The code above illustrates the problem that INCR and EXPIRE must be atomic. Obviously, our first version of the code does not meet the requirements under boundary conditions, which may cause bugs and affect user experience, so it is strongly not recommended to use it. Next, we will introduce the revised code (Lua script).

Second Edition code

After eating the first version of the code, we decided to put incr+expire in lua scripts. Without further ado, let’s get right to the code

// Perform the OCR call
func (o *ocrSvc)doOcr(ctx context.Context,uid int)(interface,err){
	If the number of calls exceeds the specified limit, the request is rejected
	ok,err := o.checkMinute(uid)
	iferr ! =nil {
		return nil,err
	}
	if! ok {return nil,errors.News("frequently called")}// Perform third-party OCR calls (pseudocode)
	ocrRes,err := doOcrByThird()
	iferr ! =nil {
		return nil,err
	}
	// The incr operation is executed if the call succeeds
	iferr := o.redis.Incr(ctx,buildUserOcrCountKey(uid)); err! =nil{
	   return nil,err
	}
	return ocrRes,nil
}

func (b *baiduOcrSvc) incrCount(ctx context.Context, uid int64) error {
   /* This lua script does the following: Local current = redis. Call ('incr',KEYS[1]); Redis. call('expire',KEYS[1],ARGV[1]) end; redis.call('expire',KEYS[1],ARGV[1]) end * /
	script := redis.NewScript(
		`local current = redis.call('incr',KEYS[1]); local t = redis.call('ttl',KEYS[1]); if t == -1 then redis.call('expire',KEYS[1],ARGV[1]) end; return current `)
	var (
		expireTime = 60 / / 60 seconds
	)
	_, err := script.Run(ctx, b.redis.Client(), []string{buildUserOcrCountKey(uid)}, expireTime).Result()
	iferr ! =nil {
		return err
	}
	return nil
}

// Check whether the number of calls per minute is exceeded
func (o *ocrSvc)checkMinute (ctx context.Context,uid int) (bool, error) {
	minuteCount, err := o.redis.Get(ctx, buildUserOcrCountKey(uid))
	iferr ! =nil && !errors.Is(err, eredis.Nil) {
		elog.Error("checkMinute: redis.Get failed", zap.Error(err))
		return false, constx.ErrServer
	}
	if errors.Is(err, eredis.Nil) {
	    // The second version of the code does not initialize the check
		// Expired, or there is no record of the number of calls made by the user (set initial value to 0, expiration time to 1 minute)
		// o.redis.Set(ctx, buildUserOcrCountKey(uid),0,time.Minute)
		return true.nil
	}
	// The number of calls per minute has exceeded
	if cast.ToInt(minuteCount) >= config.UserOcrMinuteCount() {
		elog.Warn("checkMinute: user FrequentlyCalled", zap.Int64("uid", uid), zap.String("minuteCount", minuteCount))
		return false.nil
	}
	return true.nil
}
Copy the code

conclusion

After some twists and turns, it seems to have solved the most difficult problem. I’m going to leave you with a question, what do you think are the problems with version 2 code? Leave a comment in the comments section

Writing is not easy, please give it a thumbs up