https://www.jianshu.com/p/a2bd89e0d24b
If you want to do well, you must sharpen your tools first. Let's install relevant tools first
jmeter
- I demonstrated it on mac, so I'll install brew first
ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)" 2> /dev/null
- Using brew to install jmeter
brew install jmeter
- Start jmeter
/usr/local/Cellar/jmeter/5.4.2/bin/jmeter
Use go for code demonstration (installation ignored)
- Create a new project / Users/zhangguofu/website/goproject
- Use go mod mode
go mod init acurd.com/m
Using MySQL as data store
create database `go-project`; use `go-project`; drop table if exists goods; CREATE TABLE `goods` ( `id` int(11) unsigned NOT NULL AUTO_INCREMENT, `name` varchar(50) NOT NULL DEFAULT '' COMMENT 'name', `count` int(11) NOT NULL COMMENT 'stock', `sale` int(11) NOT NULL COMMENT 'Sold ', `version` int(11) NOT NULL COMMENT 'Optimistic lock, version number', PRIMARY KEY (`id`) ) ENGINE = InnoDB DEFAULT CHARSET = utf8 COMMENT 'Commodity list'; drop table if exists goods_order; CREATE TABLE `goods_order` ( `id` int(11) unsigned NOT NULL AUTO_INCREMENT, `gid` int(11) NOT NULL COMMENT 'stock ID', `name` varchar(30) NOT NULL DEFAULT '' COMMENT 'Trade name', `create_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT 'Creation time', PRIMARY KEY (`id`) ) ENGINE = InnoDB DEFAULT CHARSET = utf8 COMMENT 'Order form'; insert into goods (`id`,`name`,`count`,`sale`,`version`) values (1,'Huawei p40',10,0,0);
Relevant code [a little rough]
package main import ( "fmt" "github.com/jinzhu/gorm" _ "github.com/jinzhu/gorm/dialects/mysql" "log" "net/http" "strconv" "time" ) // Commodity list type Goods struct { Id uint `gorm:"column:id;type:int(11) unsigned;primary_key;AUTO_INCREMENT" json:"id"` Name string `gorm:"column:name;type:varchar(50);NOT NULL" json:"name"` // name Count int `gorm:"column:count;type:int(11);NOT NULL" json:"count"` // stock Sale int `gorm:"column:sale;type:int(11);NOT NULL" json:"sale"` // Sold Version int `gorm:"column:version;type:int(11);NOT NULL" json:"version"` // Optimistic lock, version number } // Order form type GoodsOrder struct { Id uint `gorm:"column:id;type:int(11) unsigned;primary_key;AUTO_INCREMENT" json:"id"` Gid int `gorm:"column:gid;type:int(11);NOT NULL" json:"gid"` // Inventory ID Name string `gorm:"column:name;type:varchar(30);NOT NULL" json:"name"` // Trade name CreateTime time.Time `gorm:"column:create_time;type:timestamp;default:CURRENT_TIMESTAMP;NOT NULL" json:"create_time"` // Creation time } //Actual table name func (m *GoodsOrder) TableName() string { return "goods_order" } func main() { http.HandleFunc("/", addOrder) log.Fatal(http.ListenAndServe(":8082", nil)) } func getDb() *gorm.DB { connArgs := fmt.Sprintf("%s:%s@(%s:%d)/%s?charset=utf8&parseTime=True&loc=Local", "guofu", "guofu", "localhost", 13306, "go-project") db, err := gorm.Open("mysql", connArgs) if err != nil { panic(err) } db.LogMode(true) //Print sql statement //Open connection pool db.DB().SetMaxIdleConns(100) //Maximum idle connections db.DB().SetMaxOpenConns(100) //maximum connection db.DB().SetConnMaxLifetime(30) //Maximum survival time (s) return db } func addOrder(w http.ResponseWriter, r *http.Request) { db := getDb() defer db.Close() // First, check the goods list to see if there is any inventory var goods Goods db.Where("id = ?", "1").First(&goods) //fmt.Printf("%+v", goods) if goods.Count >0 { tx := db.Begin() defer func() { if r := recover() r != nil { tx.Rollback() } }() goods.Sale+=1 goods.Count-=1 //Update database if err := tx.Save(&goods).Error; err != nil { tx.Rollback() panic(err) } order:= GoodsOrder{ Gid: 1, Name:strconv.Itoa(int(time.Now().Unix())), } if err := tx.Create(&order).Error; err != nil { tx.Rollback() panic(err) } tx.Commit() w.Write([]byte(fmt.Sprintf("the count i read is %d",goods.Count))) }else{ w.Write([]byte("I didn't get anything")) } //If there is inventory, insert it into the order table }
-
Run jmeter,100 threads, 10c per thread
-
We checked the database and found that 10 stocks were gone. And 902 orders were placed, and the boss fainted in the toilet
-
Moreover, we found through the result tree that the count i read is 7 returned by several requests, which means that at the same time, many threads read 7 of the inventory, that is, at the same time, many processes read the same data, then began to place orders, which eventually led to oversold.
-
Let's take a look at jmeter's aggregation report and compare the following results
Solve oversold
- If there is oversold, it must not work. So how can we optimize this problem?
Pessimistic lock
- First, the code is changed, mainly the addOder part. In addition, in order to demonstrate the impact of pessimistic lock and optimistic lock on interface performance, we set the inventory to 1000
func addOrder(w http.ResponseWriter, r *http.Request) { db := getDb() defer db.Close() // First, check the goods list to see if there is any inventory var goods Goods tx := db.Begin() if err := tx.Set("gorm:query_option", "FOR UPDATE").First(&goods, 1).Error; err != nil { tx.Rollback() panic(err) } defer func() { if r := recover() r != nil { tx.Rollback() } }() //fmt.Printf("%+v", goods) if goods.Count >0 { goods.Sale+=1 goods.Count-=1 //Update database if err := tx.Save(&goods).Error; err != nil { tx.Rollback() panic(err) } order:= GoodsOrder{ Gid: 1, Name:strconv.Itoa(int(time.Now().Unix())), } if err := tx.Create(&order).Error; err != nil { tx.Rollback() panic(err) } tx.Commit() w.Write([]byte(fmt.Sprintf("the count i read is %d",goods.Count))) }else{ tx.Rollback() w.Write([]byte("I didn't get anything")) } //If there is inventory, insert it into the order table }
-
Found no oversold this time
-
But in this way, when each thread updates the request, it will first lock this row of the table (sad lock), and then release the lock after updating the inventory. This slows down the processing of requests. Next, let's look at optimistic locks. Let's take a look at jmeter's aggregation report
Optimistic lock
- Optimistic lock is not a real lock, but a data update mechanism. For example, we judge whether the data has been tampered according to the version number
- Take a look at the modified code
func addOrder(w http.ResponseWriter, r *http.Request) { db := getDb() defer db.Close() // First, check the goods list to see if there is any inventory var goods Goods tx := db.Begin() if err := tx.Where("ID=?", "1").First(&goods).Error; err != nil { tx.Rollback() return } defer func() { if r := recover() r != nil { tx.Rollback() return } }() //fmt.Printf("%+v", goods) if goods.Count >0 { goods.Sale+=1 goods.Count-=1 oldVerson:=goods.Version goods.Version+=1 //Update database column:=tx.Model(&goods).Where("version=?",oldVerson).Updates(&goods) if column.RowsAffected==0 {//No update succeeded tx.Rollback() w.Write([]byte("I didn't rob anyone")) return } order:= GoodsOrder{ Gid: 1, Name:strconv.Itoa(int(time.Now().Unix())), } if err := tx.Create(&order).Error; err != nil { tx.Rollback() w.Write([]byte("Failed to create order")) return } tx.Commit() w.Write([]byte(fmt.Sprintf("the count i read is %d",goods.Count))) }else{ tx.Rollback() w.Write([]byte("I didn't get anything")) } //If there is inventory, insert it into the order table }
- Check the database and find that the order is normal
- Check the aggregation results of jmeter and find that the speed has been improved a lot
redis lock
- All the above are synchronous schemes. Every request needs to be processed by mysql. If the number of requests is too large, MySQL service may be down, resulting in service terminals. Can we use asynchronous processing?
- Peak clipping: for the seckill system, there will be a large influx of users, so there will be a high instantaneous peak at the beginning of rush buying. High peak flow is a very important reason for crushing the system, so how to turn the instantaneous high flow into a stable flow for a period of time is also a very important idea for designing the second kill system. The common methods to realize peak shaving are caching and message middleware.
In the following case, we will implement the second kill function based on Redis
- Using the queue, I put the goods into a queue. Whoever grabs them can continue to place orders, and if they can't, they will return
- At the same time, in order to reduce the pressure on MySQL, we put the request into the queue (redis rabbitmq can be implemented), and run the order from the queue to the database through the script
Configure Redis related data
- Configure the queue data, a total of 1000, that is, only 1000 users can grab it
package main import ( "fmt" "github.com/go-redis/redis" "log" "net/http" ) func main() { http.HandleFunc("/", addOrder) log.Fatal(http.ListenAndServe(":8082", nil)) } func addOrder(w http.ResponseWriter, r *http.Request) { //Read the data from redis var list = "goodslist" var orderList="orderlist" client := getRedis() /** Used to initialize a 1000 stock queue client.LTrim(list, 1, 0) //Initialize an empty queue first for i := 1; i <= 1000; i++ { //Put 1000 stocks in the queue client.LPush(list, i) } return **/ var res = client.RPop(list) val := res.Val() if len(val) > 0 { //After grabbing, the user's id is stored in another queue for creating orders r.ParseForm() uid:=r.FormValue("uid") //fmt.Println(uid) //return client.LPush(orderList,uid) msg:=fmt.Sprintf("I got it,I'm number one%v Grab my users id yes %v \n", val,uid) _, _ = w.Write([]byte(msg)) fmt.Print(msg) } else { msg:="I didn't get anything\n" _, _ = w.Write([]byte(msg)) fmt.Print(msg) } return } func getRedis() *redis.Client { client := redis.NewClient(&redis.Options{ Addr: "127.0.0.1:6379", }) return client }
- Check the execution results. Someone may ask, isn't it a queue? How did it print out out out of order? This is because go starts a process to process every request. There are n processes, and it is not necessarily who comes back first
- Let's take a look at redis
- Let's take a look at the aggregation result of requests, which is about 100 times higher than that of MySQL
Use redis incrby decrby to control the number of people placing orders
- Let's look at the code first
func addOrder(w http.ResponseWriter, r *http.Request) { //Read the data from redis. If you read it, you will enter the order placing link var inckey="inc-count" var orderList="inc-orderlist" var total int64=1000 client := getRedis() //defer client.Close() //defer r.Body.Close() var res = client.IncrBy(inckey,1) val := res.Val() if res.Err()!=nil{ fmt.Print(res.Err()) return } fmt.Println("My value is now",val); //return if val <= total { //After grabbing, the user's id is stored in another queue for creating orders r.ParseForm() uid:=r.FormValue("uid") client.LPush(orderList,uid) msg:=fmt.Sprintf("I got it,I'm number one%d Grab my users id yes %v \n", val,uid) _, _ = w.Write([]byte(msg)) fmt.Print(msg) } else { msg:="I didn't get anything\n" _, _ = w.Write([]byte(msg)) } return }
- Let's take a look at Redis
- Let's look at request aggregation
episode
- There are some small episodes in the process of using incrby. Let's take a look at the screenshot first. There is no problem with a single request. When accessing multiple threads, it returns 0. The print error ERR max number of clients reached is because I didn't close the resources after using Redis
- After modifying the above problem, there is a problem again. connect: can't assign requested addressdial tcp. This is because the response is not closed. Therefore, partners must remember to close resources!
redis distributed lock
Before starting, let's familiarize ourselves with these commands (supported from redis version 2.6.12)
-
EX second: set the expiration time of the key to second seconds. The effect of SET key value EX second is equivalent to that of set key second value.
-
PX millisecond: set the expiration time of the key to millisecond. SET key value PX millisecond effect is equivalent to PSETEX key millisecond value.
-
Nx: set the key only when the key does not exist. SET key value NX has the same effect as SETNX key value.
-
20: Set the key only when it already exists.
-
Let's look at the code
package main import ( "fmt" "github.com/go-redis/redis" "github.com/jinzhu/gorm" _ "github.com/jinzhu/gorm/dialects/mysql" "log" "net/http" "strconv" "time" ) // Commodity list type Goods struct { Id uint `gorm:"column:id;type:int(11) unsigned;primary_key;AUTO_INCREMENT" json:"id"` Name string `gorm:"column:name;type:varchar(50);NOT NULL" json:"name"` // name Count int `gorm:"column:count;type:int(11);NOT NULL" json:"count"` // stock Sale int `gorm:"column:sale;type:int(11);NOT NULL" json:"sale"` // Sold Version int `gorm:"column:version;type:int(11);NOT NULL" json:"version"` // Optimistic lock, version number } // Order form type GoodsOrder struct { Id uint `gorm:"column:id;type:int(11) unsigned;primary_key;AUTO_INCREMENT" json:"id"` Gid int `gorm:"column:gid;type:int(11);NOT NULL" json:"gid"` // Inventory ID Name string `gorm:"column:name;type:varchar(30);NOT NULL" json:"name"` // Trade name CreateTime time.Time `gorm:"column:create_time;type:timestamp;default:CURRENT_TIMESTAMP;NOT NULL" json:"create_time"` // Creation time } //Actual table name func (m *GoodsOrder) TableName() string { return "goods_order" } func main() { http.HandleFunc("/", addOrder) log.Fatal(http.ListenAndServe(":8082", nil)) } func getDb() *gorm.DB { connArgs := fmt.Sprintf("%s:%s@(%s:%d)/%s?charset=utf8&parseTime=True&loc=Local", "guofu", "guofu", "localhost", 13306, "go-project") db, err := gorm.Open("mysql", connArgs) if err != nil { panic(err) } db.LogMode(false) //Print sql statement //Open connection pool db.DB().SetMaxIdleConns(100) //Maximum idle connections db.DB().SetMaxOpenConns(100) //maximum connection db.DB().SetConnMaxLifetime(30) //Maximum survival time (s) return db } func addOrder(w http.ResponseWriter, r *http.Request) { key := "order" client := getRedis() defer client.Close() cmd := client.SetNX(key, "1", time.Second*30)//There will be a problem here, that is, the execution of the program in me is too long, resulting in the release of the lock. Then the delete lock at the end of the program will delete other requested locks, resulting in unavailability if cmd.Val() == true { db := getDb() defer db.Close() // First, check the goods list to see if there is any inventory var goods Goods db.Where("id = ?", "1").First(&goods) fmt.Println(goods.Count) if goods.Count > 0 { tx := db.Begin() defer func() { if r := recover() r != nil { tx.Rollback() } }() goods.Sale += 1 goods.Count -= 1 //Update database if err := tx.Save(&goods).Error; err != nil { tx.Rollback() panic(err) } order := GoodsOrder{ Gid: 1, Name: strconv.Itoa(int(time.Now().Unix())), } if err := tx.Create(&order).Error; err != nil { tx.Rollback() panic(err) } tx.Commit() w.Write([]byte(fmt.Sprintf("the count i read is %d", goods.Count))) } else { w.Write([]byte("I didn't get anything")) } client.Del(key) } } func getRedis() *redis.Client { client := redis.NewClient(&redis.Options{ Addr: "127.0.0.1:6379", }) return client }
- However, there will be a problem here, that is, the execution of the program in me is too long, resulting in the release of the lock. Then the deletion lock at the end of the program will delete other requested locks, resulting in unavailability. Let's optimize and assign a random number to value. Before each deletion, judge whether this value is consistent with your value. If it is consistent, delete it. If it is inconsistent, do not delete it
package main import ( "fmt" "github.com/go-redis/redis" "github.com/jinzhu/gorm" _ "github.com/jinzhu/gorm/dialects/mysql" "github.com/satori/go.uuid" "log" "net/http" "strconv" "time" ) // Commodity list type Goods struct { Id uint `gorm:"column:id;type:int(11) unsigned;primary_key;AUTO_INCREMENT" json:"id"` Name string `gorm:"column:name;type:varchar(50);NOT NULL" json:"name"` // name Count int `gorm:"column:count;type:int(11);NOT NULL" json:"count"` // stock Sale int `gorm:"column:sale;type:int(11);NOT NULL" json:"sale"` // Sold Version int `gorm:"column:version;type:int(11);NOT NULL" json:"version"` // Optimistic lock, version number } // Order form type GoodsOrder struct { Id uint `gorm:"column:id;type:int(11) unsigned;primary_key;AUTO_INCREMENT" json:"id"` Gid int `gorm:"column:gid;type:int(11);NOT NULL" json:"gid"` // Inventory ID Name string `gorm:"column:name;type:varchar(30);NOT NULL" json:"name"` // Trade name CreateTime time.Time `gorm:"column:create_time;type:timestamp;default:CURRENT_TIMESTAMP;NOT NULL" json:"create_time"` // Creation time } //Actual table name func (m *GoodsOrder) TableName() string { return "goods_order" } func main() { http.HandleFunc("/", addOrder) log.Fatal(http.ListenAndServe(":8082", nil)) } func getDb() *gorm.DB { connArgs := fmt.Sprintf("%s:%s@(%s:%d)/%s?charset=utf8&parseTime=True&loc=Local", "guofu", "guofu", "localhost", 13306, "go-project") db, err := gorm.Open("mysql", connArgs) if err != nil { panic(err) } db.LogMode(false) //Print sql statement //Open connection pool db.DB().SetMaxIdleConns(100) //Maximum idle connections db.DB().SetMaxOpenConns(100) //maximum connection db.DB().SetConnMaxLifetime(30) //Maximum survival time (s) return db } func addOrder(w http.ResponseWriter, r *http.Request) { value:=GetUUID() key := "order" client := getRedis() defer client.Close() cmd := client.SetNX(key,value , time.Second*30)//There will be a problem here, that is, the execution of the program in me is too long, resulting in the release of the lock. Then the delete lock at the end of the program will delete other requested locks, resulting in unavailability if cmd.Val() == true { db := getDb() defer db.Close() // First, check the goods list to see if there is any inventory var goods Goods db.Where("id = ?", "1").First(&goods) fmt.Println(goods.Count) if goods.Count > 0 { tx := db.Begin() defer func() { if r := recover() r != nil { tx.Rollback() } }() goods.Sale += 1 goods.Count -= 1 //Update database if err := tx.Save(&goods).Error; err != nil { tx.Rollback() panic(err) } order := GoodsOrder{ Gid: 1, Name: strconv.Itoa(int(time.Now().Unix())), } if err := tx.Create(&order).Error; err != nil { tx.Rollback() panic(err) } tx.Commit() w.Write([]byte(fmt.Sprintf("the count i read is %d", goods.Count))) } else { w.Write([]byte("I didn't get anything")) } if client.Get(key).Val()==value { client.Del(key) } } } func GetUUID() (string) { u2 := uuid.NewV4() return u2.String() } func getRedis() *redis.Client { client := redis.NewClient(&redis.Options{ Addr: "127.0.0.1:6379", }) return client }
lua+redis to realize distributed lock
Why lua
- From redis2 Starting from version 6.0, through the built-in Lua interpreter, you can use EVAL command to evaluate Lua script.
- Reduce network overhead. Multiple requests can be sent at one time in the form of script to reduce the network delay.
- Atomic operation. Redis will execute the whole script as a whole and will not be inserted by other requests. Therefore, there is no need to worry about race conditions and transactions during script running. Redis will ensure that the scripts will be executed in an atomic way (either successful or failed). When a script is being executed, no other scripts or redis commands will be executed.
- Reuse. The script sent by the client will be permanently stored in redis, so that other clients can reuse this script without using code to complete the same logic.
Learn to use lua
-
Reference articles
Reference articles
To use lua in Redis, let's take a look at the common commands -
EVAL
Command format: EVAL script numkeys key [key...] arg [arg...]
Let's take a chestnut
# ------------------------------------------ script-----------------------------numkeys--key-----arg--arg EVAL "redis.call('SET',KEYS[1],ARGV[1]);redis.call('EXPIRE',KEYS[1],ARGV[2]);return 1;" 1 age 18 60 127.0.0.1:6379> EVAL "redis.call('SET',KEYS[1],ARGV[1]);redis.call('EXPIRE',KEYS[1],ARGV[2]);return 1;" 1 age 18 60 (integer) 1 127.0.0.1:6379> get age "18"
The part within the quotation marks is the script. 1 represents the number of keys, that is, there is only one key. Who is this key? age is followed by two parameters. argv[1] is 18 and argv [2] is 60
The meaning of the above command is the same as that of set age 18 EX 60. Set the value of key1 to 10 and the expiration time to 60 seconds
SCRIPT LOAD
- Command format SCRIPT LOAD script
- SCRIPT LOAD adds the script script to the script cache of Redis server. Instead of executing the script immediately, it will immediately evaluate the input script. And returns the SHA1 checksum of the given script. If the given script is already in the cache, no operation is performed. In fact, the script is cached in the Redis server. If it has been cached, it will not be cached
127.0.0.1:6379> SCRIPT LOAD "redis.call('SET',KEYS[1],ARGV[1]);redis.call('EXPIRE',KEYS[1],ARGV[2]);return 1;" "6cc501292668ceef3dd487b3e4e889dc08d07587"
EVALSHA
Command format: EVALSHA sha1 numkeys key [key...] arg [arg...]
The script was cached just now. Why didn't it execute? On any client side, through the EVALSHA command, you can use the SHA1 checksum of the script to call the script. Scripts can remain in the cache for an unlimited period of time until SCRIPT FLUSH is executed.
127.0.0.1:6379> EVALSHA 6cc501292668ceef3dd487b3e4e889dc08d07587 1 name jimy 10 (integer) 1 127.0.0.1:6379> get name "jimy" 127.0.0.1:6379> get name (nil) 127.0.0.1:6379> EVALSHA 6cc501292668ceef3dd487b3e4e889dc08d07587 1 name jimy 10 (integer) 1 127.0.0.1:6379> ttl name (integer) 7 127.0.0.1:6379> ttl name (integer) 6 127.0.0.1:6379> ttl name
SCRIPT FLUSH
Command format: SCRIPT FLUSH
Clear all Lua script caches on Redis server. Note that all
127.0.0.1:6379> SCRIPT EXISTS 6cc501292668ceef3dd487b3e4e889dc08d07587 1) (integer) 1 127.0.0.1:6379> SCRIPT FLUSH OK 127.0.0.1:6379> SCRIPT EXISTS 6cc501292668ceef3dd487b3e4e889dc08d07587 1) (integer) 0
SCRIPT KILL
Command format: SCRIPT KILL
Kill the currently running Lua script. This command takes effect only if and only if the script has not performed any write operation. This command is mainly used to terminate a script that runs too long, such as a script with infinite loop due to a BUG, or blocking due to too large read key, etc.
If the script has been written and executed, Lua will not be able to execute it even if it violates the atomic principle. In this case, the only feasible way is to use the SHUTDOWN NOSAVE command to stop the script by stopping the whole Redis process and prevent incomplete information from being written to the database.
Using redis cli client to execute lua file
-
Full command
redis-cli -h host -p port -a password -n db -–eval demo.lua k1 k2 , a1 a2
Explanation: - h followed by the IP of remote Redis- P followed by the remote Redis port number- a followed by password- n the following parameter is the selected Redis db; K1, K2, a1, a2 "are obtained in lua script by using global variables KEYS and ARGV. -
Let's take a chestnut to verify it
-
lua files are as follows
-- This command is equivalent to set key1 argv1 EX argv2 -- For example, the chestnut below,set up age Yes, 18. The expiration time is 60 -- set age 18 EX 60 redis.call('SET',KEYS[1],ARGV[1]) redis.call('EXPIRE',KEYS[1],ARGV[3]) redis.call('SET',KEYS[2],ARGV[2]) redis.call('EXPIRE',KEYS[2],ARGV[3]) return 1
- Execute the command (note that there is a comma between key and arg, and one space should be left before and after the comma)
- The second point to note is that when we apply lua in go, we can use redis cli for debugging first, which is more convenient than putting it into go for debugging
zhangguofu@zhangguofudeMacBook-Pro bin $ ./redis-cli --eval /Users/zhangguofu/website/goproject/script.lua name age , jimy 18 60 (integer) 1
- View Redis results
Using lua in go
- Take a look at the lua script
--user id local userId = tostring(KEYS[1]) --Order set local orderSet=tostring(KEYS[2]) -- Commodity inventory key local goodsTotal=tostring(ARGV[1]) --Order queue local orderList=tostring(ARGV[2]) -- Has it been snapped up,If yes, return local hasBuy = tonumber(redis.call("sIsMember", orderSet, userId)) if hasBuy ~= 0 then return 0 end -- Quantity in stock local total=tonumber(redis.call("GET", goodsTotal)) --return total -- Is it out of stock,If yes, return if total <= 0 then return 0 end -- Can place an order local flag -- Add to order queue flag = redis.call("LPUSH", orderList, userId) -- Add to user set flag = redis.call("SADD", orderSet, userId) -- Inventory minus 1 flag = redis.call("DECR", goodsTotal) -- Returns the number of caches at that time return total --[[ -- multiline comment ]]
- Take a look at the execution results
zhangguofu@zhangguofudeMacBook-Pro bin $ ./redis-cli --eval /Users/zhangguofu/website/goproject/lua-case/script.lua sxxxxxx orderSet , goodsTotal orderList (integer) 100 zhangguofu@zhangguofudeMacBook-Pro bin $ ./redis-cli --eval /Users/zhangguofu/website/goproject/lua-case/script.lua sxxxxx1 orderSet , goodsTotal orderList (integer) 99 zhangguofu@zhangguofudeMacBook-Pro bin $ ./redis-cli --eval /Users/zhangguofu/website/goproject/lua-case/script.lua sxxxxx2 orderSet , goodsTotal orderList (integer) 98
- Take a look at the code of go
package main import ( "fmt" "github.com/go-redis/redis/v8" "io/ioutil" "log" "net/http" "sync" ) const orderSet = "orderSet" //Collection of user IDs const goodsTotal = "goodsTotal" //Item inventory key const orderList = "orderList" //Order queue func createScript() *redis.Script { str, err := ioutil.ReadFile("./lua-case/script.lua") if err != nil { fmt.Println("Script read error", err) log.Println(err) } scriptStr := fmt.Sprintf("%s", str) script := redis.NewScript(scriptStr) return script } func evalScript(client *redis.Client, userId string, wg *sync.WaitGroup) { defer wg.Done() script := createScript() //fmt.Printf("%+v",script) //return sha, err := script.Load(client.Context(), client).Result() if err != nil { log.Fatalln(err) } ret := client.EvalSha(client.Context(), sha, []string{ userId, orderSet, }, []string{ goodsTotal, orderList, }) if result, err := ret.Result(); err != nil { log.Fatalf("Execute Redis fail: %v", err.Error()) } else { total:=result.(int64) if total==0{ fmt.Printf("userid: %s, Nothing \n", userId) }else{ fmt.Printf("userid: %s Got it, stock: %d \n", userId, total) } } } func main() { http.HandleFunc("/", addOrder) log.Fatal(http.ListenAndServe(":8082", nil)) } func addOrder(w http.ResponseWriter, r *http.Request) { var wg sync.WaitGroup wg.Add(1) client := getRedis() defer r.Body.Close() defer client.Close() r.ParseForm() uid := r.FormValue("uid") go evalScript(client, uid, &wg) wg.Wait() } func getRedis() *redis.Client { client := redis.NewClient(&redis.Options{ Addr: "127.0.0.1:6379", }) return client }
-
View code running results
-
View Redis results
127.0.0.1:6379> set goodsTotal 100 OK 127.0.0.1:6379> get goodsTotal "0" 127.0.0.1:6379> keys * 1) "goodsTotal" 2) "orderSet" 3) "orderList" 127.0.0.1:6379> llen orderList (integer) 100 127.0.0.1:6379> scard orderSet (integer) 100 127.0.0.1:6379>
- View jmeter's aggregation Report
- Relevant codes have been uploaded to github warehouse
- If you can't click, please select to view the original text