Redis article takes you understand spike optimize performance under high concurrency

In this article

Use Redis optimized interface performance under high concurrency scenarios
database optimistic locking

With near double-12, a variety of promotional activities became the rage, there are more mainstream spike, grab coupons, to fight groups and so on.
The main scenario involves a high concurrent competition for the same resources and have a spike grab coupons.

premise

rule of activity

  • A limited number of prizes, such as 100
  • Does not limit the number of users involved
  • Each user can only participate once spike

Activity Requirements

  • No more hair, not less fat, 100 prizes to be issued to all
  • A user up to grab a prize
  • Follow the principle of first-come, first-come, users have prizes

Database implementation

Pessimistic locking poor performance, this article will not discuss, discuss the advantages and disadvantages of using optimistic locking to solve the problem of high concurrency.
Database structure

Redis article takes you understand spike optimize performance under high concurrency

  • UserId is 0, RewardAt to NULL when not winning
  • When winning is winning UserId User ID, RewardAt is winning time

Optimistic locking achieve
optimistic locking does not really exist a real lock, optimistic locking is to use the data to do a field, such as the examples of this paper is to UserId to achieve.
Implementation process is as follows:
1, the query for the prize UserId 0 if not found then prompt no prizes

SELECT * FROM envelope WHERE user_id=0 LIMIT 1`

2, updating user ID and prize winning time (assuming that ID is 1 prize, the winning user ID 100, the current time is 2019-10-29 12:00:00), user_id = 0 here is our optimistic lock.

UPDATE envelope SET user_id=100, reward_at='2019-10-29 12:00:00' WHERE user_id=0 AND id=1`

3, detection is performed UPDATE statement returns the value 1 if the returns prove the success of winning, or other proof that the prize was robbed

Why add optimistic locking

Under normal circumstances get the prize, then the prize to update the specified user is not the problem. If you do not add the user_id = 0, the following problems occur under high concurrency scenarios:

  • Two users simultaneously query to a non-winning prizes (concurrency problems occur)
  • The prize winning users to update to a user, only the update condition ID = prizes ID
  • Above SQL execution is successful, the impact of the number of rows is 1, then the interface will return to winning user 1
  • Next, the winning user is updated to 2, only the update condition ID = user ID prizes
  • Because it is the same prize, the prize has been issued to the user 1 will be re-issued to the user 2, this time affecting the number of rows 1, 2 user interfaces also winning return
  • So the final result of this prize is issued to the user 2
  • 1 user activity will come complainant party, because the user interface has returned 1 lottery jackpot, but his prize was robbed, this time can only lose money party activities

After the lottery process to add optimistic locking

1 1. The update condition when red id = user ID AND user_id = 0, since this time is not assigned to anyone red, a user update is successful, the user interfaces to return a winning
2. When updating the update condition 2 user id = red ID AND user_id = 0, since at this time the user has been allocated to the red 1, so that the condition does not update any records, the user interface has returned winning 2

Optimistic locking the advantages and disadvantages
advantages

  • Performance is acceptable, because no lock
  • Not Super

Shortcoming

  • Usually it does not meet the "first come first served" campaign rules, once the concurrent occurrence will occur without winning, this time there are prizes prizes library

Pressure measured
in the pressure measurement MacBook Pro 2018 is expressed as follows (HTTP server implemented Golang, MySQL connection pool size 100, Jmeter pressure test):

  • 500 500 Total number of successes number of concurrent requests for the issuance of the average response time 331ms certain 31 458.7 / s

Redis achieve

You can see the competition ratio is too high, is not a recommended method for implementing the following optimizing business by Redis under this spike achieve optimistic locking.

Redis performance reasons

  • Eliminating the need for single-threaded thread switching overhead
  • Although persistence operations related to disk access, but that is asynchronous and does not affect the Redis memory-based business operations
  • Use IO multiplexing

Implementation process

  • Before the event in prizes will be written to the database code Redis queue
  • Use the pop-up elements in the queue when lpop activities
  • If the acquisition is successful, the payment of prizes using the syntax UPDATE
UPDATE reward SET user_id=用户ID,reward_at=当前时间 WHERE code='奖品码'

If the acquisition fails, no current available prizes, winners can not prompt

Redis case where concurrent access is through lpop (Redis) is ensured, this method is a method atom, can also ensure that the concurrency of a pop-up.

Pressure measurement

Pressure measurement performance on MacBook Pro 2018 is as follows (golang implemented HTTP server, MySQL connection pool size 100, Redis connection pool consignment 100, Jmeter pressure test):

  • 500 500 Total number of concurrent requests average response time of 48ms grant a certain number of successes 100 497.0 / s

in conclusion

Redis can see the performance is stable and will not appear super-fat, and less access latency is about eight times the throughput bottleneck not reached, it can be seen Redis for high concurrency system performance is very big! Access cost is not too high, it is worth learning!

Experiment Code

// main.go
package main

import (
    "fmt"
    "github.com/go-redis/redis"
    _ "github.com/go-sql-driver/mysql"
    "github.com/jinzhu/gorm"
    "log"
    "net/http"
    "strconv"
    "time"
)

type Envelope struct {
    Id        int `gorm:"primary_key"`
    Code      string
    UserId    int
    CreatedAt time.Time
    RewardAt  *time.Time
}

func (Envelope) TableName() string {
    return "envelope"
}

func (p *Envelope) BeforeCreate() error {
    p.CreatedAt = time.Now()
    return nil
}

const (
    QueueEnvelope = "envelope"
    QueueUser     = "user"
)

var (
    db          *gorm.DB
    redisClient *redis.Client
)

func init() {
    var err error
    db, err = gorm.Open("mysql", "root:root@tcp(localhost:3306)/test?charset=utf8&parseTime=True&loc=Local")
    if err != nil {
        log.Fatal(err)
    }
    if err = db.DB().Ping(); err != nil {
        log.Fatal(err)
    }
    db.DB().SetMaxOpenConns(100)
    fmt.Println("database connected. pool size 10")
}

func init() {
    redisClient = redis.NewClient(&redis.Options{
        Addr:     "localhost:6379",
        DB:       0,
        PoolSize: 100,
    })
    if _, err := redisClient.Ping().Result(); err != nil {
        log.Fatal(err)
    }
    fmt.Println("redis connected. pool size 100")
}

// 读取Code写入Queue
func init() {
    envelopes := make([]Envelope, 0, 100)
    if err := db.Debug().Where("user_id=0").Limit(100).Find(&envelopes).Error; err != nil {
        log.Fatal(err)
    }
    if len(envelopes) != 100 {
        log.Fatal("不足100个奖品")
    }
    for i := range envelopes {
        if err := redisClient.LPush(QueueEnvelope, envelopes[i].Code).Err(); err != nil {
            log.Fatal(err)
        }
    }
    fmt.Println("load 100 envelopes")
}

func main() {
    http.HandleFunc("/envelope", func(w http.ResponseWriter, r *http.Request) {
        uid := r.Header.Get("x-user-id")
        if uid == "" {
            w.WriteHeader(401)
            _, _ = fmt.Fprint(w, "UnAuthorized")
            return
        }
        uidValue, err := strconv.Atoi(uid)
        if err != nil {
            w.WriteHeader(400)
            _, _ = fmt.Fprint(w, "Bad Request")
            return
        }
        // 检测用户是否抢过了
        if result, err := redisClient.HIncrBy(QueueUser, uid, 1).Result(); err != nil || result != 1 {
            w.WriteHeader(429)
            _, _ = fmt.Fprint(w, "Too Many Request")
            return
        }
        // 检测是否在队列中
        code, err := redisClient.LPop(QueueEnvelope).Result()
        if err != nil {
            w.WriteHeader(200)
            _, _ = fmt.Fprint(w, "No Envelope")
            return
        }
        // 发放红包
        envelope := &Envelope{}
        err = db.Where("code=?", code).Take(&envelope).Error
        if err == gorm.ErrRecordNotFound {
            w.WriteHeader(200)
            _, _ = fmt.Fprint(w, "No Envelope")
            return
        }
        if err != nil {
            w.WriteHeader(500)
            _, _ = fmt.Fprint(w, err)
            return
        }
        now := time.Now()
        envelope.UserId = uidValue
        envelope.RewardAt = &now
        rowsAffected := db.Where("user_id=0").Save(&envelope).RowsAffected // 添加user_id=0来验证Redis是否真的解决争抢问题
        if rowsAffected == 0 {
            fmt.Printf("发生争抢. id=%d\n", envelope.Id)
            w.WriteHeader(500)
            _, _ = fmt.Fprintf(w, "发生争抢. id=%d\n", envelope.Id)
            return
        }
        _, _ = fmt.Fprint(w, envelope.Code)
    })

    fmt.Println("listen on 8080")
    fmt.Println(http.ListenAndServe(":8080", nil))
}

At last

Like I can focus on my public number: java small Guage sharing platform. Thanks for your support!

Guess you like

Origin blog.51cto.com/14611538/2453108