Code practice of several schemes often used in second kill anti oversold

Posted by Pethlehemm on Sun, 06 Mar 2022 10:26:19 +0100

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>  

Topics: Go MySQL Back-end Distribution Middleware