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:
- Assume that the number of OCR calls by the current user does not exceed. But the TTL in Redis has 1 second left
- The third party OCR is then called for identification
- 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)
- 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