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:
-
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
-
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
-
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:
- Two users at the same time found a prize that did not win (concurrency problem)
- Update the winning user to user 1 with ID= prize ID
- If the SQL execution is successful and the number of rows affected is 1, the interface will return user 1 winning
- Next, update the winning user to user 2 with only ID= prize ID
- 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
- So the end result of the prize is given to user 2
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
- The condition for updating user 1 is
Id = red envelope ID AND user_id=0
Because 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 - When user 2 is updated, the update condition is
Id = red envelope ID AND user_id=0
Since 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
-
Write the code of the prize in the database to the Redis queue before the event starts
-
Use LPOP to pop the elements in the queue as the activity proceeds
-
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
-
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