Redis optimizes high and fast performance

Content of this article

  • Using Redis to optimize interface performance in high concurrency scenarios
  • Database optimistic lock

With the approaching of the double 11, all kinds of promotional activities began to become popular, and the mainstream ones are seckill, coupons, and so on.

The main scenarios involving high concurrency competing for the same resource are seconds killing and coupon grabbing.

premise

Activity rules

  • Limited number of prizes, such as 100
  • Unlimited number of users
  • Each user can only participate in one second kill

Activity requirements

  • No more, no less. All 100 prizes should be sent out.
  • 1 user grabs 1 prize at most
  • Follow the first come, first served principle, first come users have prizes

Database implementation

Pessimistic lock performance is too poor, this article will not discuss the advantages and disadvantages of using optimistic lock to solve high concurrency problems.

database structure

ID Code UserId CreatedAt RewardAt
Prize ID Prize code User ID Creation time Winning time
  • UserId 0, RewardAt NULL when not winning
  • When winning, UserId is the winning user ID, and RewardAt is the winning time.

Optimistic lock implementation

In fact, there is no real optimistic lock. Optimistic lock is made by using a certain field of data. For example, the example in this article is implemented by UserId.

The implementation process is as follows:

  1. Query the prize with UserId 0, if not found, it will prompt no prize.

    SELECT * FROM envelope WHERE user_id=0 LIMIT 1
  2. Update the user ID and winning time of the prize (assuming that the prize ID is 1, winning user ID is 100, and the current time is 2019-10-29 12:00:00). The user Ou 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. Check the execution return value of the UPDATE statement. If the return value is 1, it indicates that the winning prize is successful. Otherwise, it indicates that the prize is robbed by others.

Why add optimistic lock

Under normal circumstances, it is OK to get the prize and then update the prize to the designated user. If the user id = 0 is not added, the following problems will occur in the high concurrency scenario:

  1. Two users simultaneously query 1 prize not won (concurrent problem occurs)
  2. Update the winning user of the prize to user 1, and the update condition is only id = prize ID
  3. The above SQL execution is successful, and the number of rows affected is also 1. At this time, the interface will return to user 1 for winning.
  4. Next, update the winning user to user 2, and the update condition is only id = prize ID
  5. Since it is the same prize, the prize already given to user 1 will be reissued to user 2. At this time, the number of affected lines is 1, and the interface returns to user 2 and wins the prize.
  6. So the final result of the prize is to distribute it to user 2.
  7. User 1 will come to complain about the active party, because the lottery interface returns to user 1 to win the prize, but his prize is robbed, at this time, the active party can only lose money

Lottery process after adding optimistic lock

  1. When updating user 1, the condition is id = red packet ID and user? Id = 0. Because the red packet is not assigned to anyone at this time, user 1 is updated successfully, and the interface returns user 1 winning prize.
  2. When user 2 is updated, the update condition is id = red packet ID AND user_id=0. Since the red packet has been assigned to user 1 at this time, no record will be updated for this condition. The interface returns user 2's winning prize.

Advantages and disadvantages of optimistic lock

Advantage

  • Good performance, no lock
  • Not super.

shortcoming

  • Generally, it does not meet the "first come, first served" activity rule. Once there is a concurrence, there will be a failure to win the prize. At this time, there are prizes in the prize library.

Pressure measurement

The performance of compression test on MacBook Pro 2018 is as follows (HTTP server implemented by Golang, MySQL connection pool size 100, Jmeter compression test):

  • 500 concurrent 500 total requests average response time 331ms number of successful distribution is 31 throughput 458.7/s

Redis implementation

It can be seen that the contention ratio is too high under the implementation of optimistic lock, which is not the recommended implementation method. Next, Redis is used to optimize this seckill business.

Why Redis has high performance

  • Single thread saves thread switching cost
  • Although memory based operations involve hard disk access, they are asynchronous and will not affect Redis business.
  • IO multiplexing used

Implementation process

  1. Write the code of the prize in the database to the Redis queue before the activity starts
  2. Use lpop to pop up the elements in the queue while the activity is in progress
  3. If successful, use the UPDATE syntax to distribute the prize

    Update reward set user id = user ID, reward at = current time WHERE code = 'prize code'
  4. If you fail to obtain the prize, there is no prize available at present, and you will be prompted that you have not won the prize.

In the case of Redis, the concurrent access is guaranteed by Redis's lpop(). This method is an atomic method, which can ensure that the concurrent access will pop up one by one.

Pressure measurement

The performance of compression test on MacBook Pro 2018 is as follows (HTTP server implemented by Golang, MySQL connection pool size 100, Redis connection pool consignment 100, Jmeter compression test):

  • 500 concurrent 500 total requests average response time 48ms number of successful distribution 100 throughput 497.0/s

conclusion

It can be seen that the performance of Redis is stable, there will be no over delivery, and the access delay is about 8 times less, and the throughput has not reached the bottleneck. It can be seen that Redis's performance improvement for high concurrency system is very large! Access cost is not high, it is worth learning!

Experimental 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")
}

// Read Code and write to 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("Less than 100 prizes")
    }
    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
        }
        // Check whether the user has robbed it
        if result, err := redisClient.HIncrBy(QueueUser, uid, 1).Result(); err != nil || result != 1 {
            w.WriteHeader(429)
            _, _ = fmt.Fprint(w, "Too Many Request")
            return
        }
        // Check whether it is in the queue
        code, err := redisClient.LPop(QueueEnvelope).Result()
        if err != nil {
            w.WriteHeader(200)
            _, _ = fmt.Fprint(w, "No Envelope")
            return
        }
        // Issue red packets
        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 // Add user? Id = 0 to verify whether Redis really solves the contention problem
        if rowsAffected == 0 {
            fmt.Printf("Scramble for. id=%d\n", envelope.Id)
            w.WriteHeader(500)
            _, _ = fmt.Fprintf(w, "Scramble for. id=%d\n", envelope.Id)
            return
        }
        _, _ = fmt.Fprint(w, envelope.Code)
    })

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

Keywords: Go Redis Database MySQL less

Added by nOw on Tue, 29 Oct 2019 12:06:26 +0200