In this paper, the content

  • Optimizes interface performance in high concurrency scenarios using Redis
  • Optimistic database locking

With the approach of Double 11, various promotional activities are becoming popular, such as seckill, snatching coupons and group shopping.

The main scenarios involving high concurrency for the same resource are seckilling and coupon grabbing.

The premise

Activity rules

  • A limited number of prizes, say 100
  • There is no limit to the number of participants
  • Each user can participate only once

Activities require

  • No more, no less, 100 prizes to send out
  • 1 user can grab 1 prize at most
  • On a first-come, first-served basis, the first person to come will get a prize

Database implementation

This article will not discuss the poor performance of pessimistic locks, but discuss the advantages and disadvantages of using optimistic locks to solve high concurrency problems.

Database structure

ID Code UserId CreatedAt RewardAt
The prize ID The prize code The user ID Creation time The winning time
  • UserId is 0 and RewardAt is NULL if you do not win
  • When winning, UserId is the winning UserId and RewardAt is the winning time

Optimistic lock implementation

Optimistic locks do not actually exist, and they are done using a field of data, such as the UserId used in this example.

The implementation process is as follows:

  1. Query the prize whose UserId is 0. If no prize is found, no prize is displayed

    SELECT * FROM envelope WHERE user_id=0 LIMIT 1
    Copy the code
  2. Update the user ID of the prize and the winning time (suppose the prize ID is 1, the winning user ID is 100, the current time is 0), where user_id=0 is optimistic lock.

    UPDATE envelope SET user_id=100, reward_at='the 2019-10-29 12:00:00' WHERE user_id=0 AND id=1
    Copy the code
  3. Check the return value of the UPDATE statement. If 1 is returned, the prize has been won. Otherwise, someone else has stolen the prize

Why add optimistic locks

It is normally ok to take the prize and then update it to the specified user. If user_id=0 is not added, the following problems may occur in high concurrency scenarios:

  1. Two users at the same time found a prize that did not win (concurrency problem)
  2. Update the winning user to user 1 with ID= prize ID
  3. If the SQL execution is successful and the number of rows affected is 1, the interface will return user 1 winning
  4. Next, update the winning user to user 2 with only ID= prize ID
  5. Because the prize is the same, the prize that has been sent to user 1 will be sent to user 2 again. In this case, the number of affected rows is 1, and the interface returns that user 2 also wins the prize
  6. So the end result of the prize is given to user 2
  7. User 1 will come and complain to the campaign because the lottery interface returns that user 1 won, but his prize was stolen, and the campaign can only lose money

Lucky draw process after adding optimistic lock

  1. The condition for updating user 1 isId = red envelope ID AND user_id=0Because the red envelope is not allocated to anyone at this time, the update is successful for user 1, and the interface returns user 1 winning
  2. When user 2 is updated, the update condition isId = red envelope ID AND user_id=0Since the red envelope has already been allocated to user 1, this condition does not update any records and the interface returns user 2 winning

Advantages and disadvantages of optimistic locking

advantages

  • The performance is ok because there is no lock
  • Not more

disadvantages

  • Usually does not meet the “first come, first served” rule of the event, once a concurrency occurs, will not win the situation, at this time there are prizes in the prize library

Pressure test

Performance on MacBook Pro 2018 (Golang implemented HTTP server,MySQL connection pool size 100, Jmeter pressure) :

  • 500 Concurrent 500 Total requests Average response time 331ms Number of successful requests 31 Throughput 458.7/s

Redis implementation

It can be seen that the optimistic lock implementation under the scrambling ratio is too high, not the recommended implementation method, the following through Redis to optimize the second kill business.

Reasons for high performance of Redis

  • Single threading eliminates thread switching overhead
  • Memory-based operations Although persistent operations involve hard disk access, they are asynchronous and do not affect Redis business
  • IO multiplexing is used

The implementation process

  1. Write the code of the prize in the database to the Redis queue before the event starts

  2. Use LPOP to pop the elements in the queue as the activity proceeds

  3. If successful, the prize is issued using the UPDATE syntax

    UPDATE reward SETUser_id = userID,reward_at= Current timeWHERE code='Prize code'
    Copy the code
  4. If you fail to get the prize, there is no prize available at present

In the case of Redis, concurrent access is guaranteed by Redis lPOP (), which is an atomic method that can be guaranteed to pop up one by one in the case of concurrency.

Pressure test

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

  • 500 concurrent requests 500 total requests avg. Response time 48ms number of successful requests 100 throughput 497.0/s

conclusion

You can see that The performance of Redis is stable, there is no overfire, and the access latency is about 8 times less, the throughput has not reached the bottleneck, you can see that Redis for high concurrency system performance is very large! Access cost is not high, worth learning!

The 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(a) string {
	return "envelope"
}

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

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

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

func init(a) {
	var err error
	db, err = gorm.Open("mysql"."root:root@tcp(localhost:3306)/test? charset=utf8&parseTime=True&loc=Local")
	iferr ! =nil {
		log.Fatal(err)
	}
	iferr = db.DB().Ping(); err ! =nil {
		log.Fatal(err)
	}
	db.DB().SetMaxOpenConns(100)
	fmt.Println("database connected. pool size 10")}func init(a) {
	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(a) {
	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 {
		iferr := redisClient.LPush(QueueEnvelope, envelopes[i].Code).Err(); err ! =nil {
			log.Fatal(err)
		}
	}
	fmt.Println("load 100 envelopes")}func main(a) {
	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)
		iferr ! =nil {
			w.WriteHeader(400)
			_, _ = fmt.Fprint(w, "Bad Request")
			return
		}
		// Check if the user grabs 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()
		iferr ! =nil {
			w.WriteHeader(200)
			_, _ = fmt.Fprint(w, "No Envelope")
			return
		}
		// Give out red envelopes
		envelope := &Envelope{}
		err = db.Where("code=?", code).Take(&envelope).Error
		if err == gorm.ErrRecordNotFound {
			w.WriteHeader(200)
			_, _ = fmt.Fprint(w, "No Envelope")
			return
		}
		iferr ! =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 that Redis really solves the scramble problem
		if rowsAffected == 0 {
			fmt.Printf("Scuffle occurred. Id =%d\n", envelope.Id)
			w.WriteHeader(500)
			_, _ = fmt.Fprintf(w, "Scuffle occurred. Id =%d\n", envelope.Id)
			return
		}
		_, _ = fmt.Fprint(w, envelope.Code)
	})

	fmt.Println("listen on 8080")
	fmt.Println(http.ListenAndServe(": 8080".nil))}Copy the code