秒杀要不要用锁?五种方案的代码实践和超卖演示

https://www.jianshu.com/p/a2bd89e0d24b

工欲善其事必先利其器,我们先来装一下相关工具

jmeter

  • 我是在mac演示的,所以我先安装一下brew
ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)" 2> /dev/null
  • 使用brew 安装jmeter
brew install jmeter
  • 启动jmeter
 /usr/local/Cellar/jmeter/5.4.2/bin/jmeter 

使用go做代码演示(安装忽略)

  • 新建一个项目/Users/zhangguofu/website/goproject
  • 使用go mod 模式
 go mod init acurd.com/m

使用MySQL作为数据存储

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 '名称',
    `count`   int(11)          NOT NULL COMMENT '库存',
    `sale`    int(11)          NOT NULL COMMENT '已售',
    `version` int(11)          NOT NULL COMMENT '乐观锁,版本号',
    PRIMARY KEY (`id`)
) ENGINE = InnoDB
  DEFAULT CHARSET = utf8 COMMENT '商品表';

drop table if exists goods_order;
CREATE TABLE `goods_order`
(
    `id`          int(11) unsigned NOT NULL AUTO_INCREMENT,
    `gid`         int(11)          NOT NULL COMMENT '库存ID',
    `name`        varchar(30)      NOT NULL DEFAULT '' COMMENT '商品名称',
    `create_time` timestamp        NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '创建时间',
    PRIMARY KEY (`id`)
) ENGINE = InnoDB
  DEFAULT CHARSET = utf8 COMMENT '订单表';

insert into goods (`id`,`name`,`count`,`sale`,`version`) values (1,'华为p40',10,0,0);

相关代码[有点粗糙]

package main

import (
    "fmt"
    "github.com/jinzhu/gorm"
    _ "github.com/jinzhu/gorm/dialects/mysql"
    "log"
    "net/http"
    "strconv"
    "time"
)

// 商品表
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"`   // 名称
    Count   int    `gorm:"column:count;type:int(11);NOT NULL" json:"count"`     // 库存
    Sale    int    `gorm:"column:sale;type:int(11);NOT NULL" json:"sale"`       // 已售
    Version int    `gorm:"column:version;type:int(11);NOT NULL" json:"version"` // 乐观锁,版本号
}
// 订单表
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"`                                             // 库存ID
    Name       string    `gorm:"column:name;type:varchar(30);NOT NULL" json:"name"`                                       // 商品名称
    CreateTime time.Time `gorm:"column:create_time;type:timestamp;default:CURRENT_TIMESTAMP;NOT NULL" json:"create_time"` // 创建时间
}
//实际表名
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) //打印sql语句
    //开启连接池
    db.DB().SetMaxIdleConns(100)   //最大空闲连接
    db.DB().SetMaxOpenConns(100)   //最大连接数
    db.DB().SetConnMaxLifetime(30) //最大生存时间(s)
    return db
}

func addOrder(w http.ResponseWriter, r *http.Request) {
    db := getDb()
    defer db.Close()

    // 先去查看商品表还有没有库存
    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
        //更新数据库
        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("我啥子都么抢到"))

    }

    //如果有库存插入到订单表
}

  • 运行jmeter,100个线程,每个线程10c


    在这里插入图片描述
  • 我们查看一下数据库,发现10个库存是没了。而且订单下了902个,老板哭晕在厕所


    在这里插入图片描述

    在这里插入图片描述
  • 而且我们通过结果树发现,有好几个请求返回的结果都是the count i read is 7 说明在同一时刻,有很多线程都读到了库存有7个,那就是说,在同一个时刻,很多进程读到了一样的数据,然后开始了下订单,并最终导致超卖问题。

  • 我们看一下jmeter的聚合报告,对比后面的结果使用


    在这里插入图片描述

解决超卖

  • 出现了超卖,肯定是不行的,那么我们怎么优化这种问题呢?

悲观锁

  • 先上代码,主要是改了addOder部分,另外为了演示悲观锁和乐观锁对接口性能的影响,我们让库存为1000
func addOrder(w http.ResponseWriter, r *http.Request) {
    db := getDb()
    defer db.Close()

    // 先去查看商品表还有没有库存
    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
        //更新数据库
        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("我啥子都么抢到"))
    }

    //如果有库存插入到订单表
}
  • 发现这次没有超卖


    在这里插入图片描述
  • 但是这样每个线程更新请求的时候都会先去锁表的这一行(悲观锁),更新完库存后再释放锁。导致处理请求速度变慢,接下来我们来看看乐观锁,我们来看一下jmeter的聚合报告


    在这里插入图片描述

乐观锁

  • 乐观锁其实并不是真正的锁,而是一种数据更新机制,比如我们根据版本号判断数据是否被篡改过
  • 看一下修改的代码
func addOrder(w http.ResponseWriter, r *http.Request) {
    db := getDb()
    defer db.Close()

    // 先去查看商品表还有没有库存
    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
        //更新数据库
        column:=tx.Model(&goods).Where("version=?",oldVerson).Updates(&goods)
        if column.RowsAffected==0 {//没有更新成功
            tx.Rollback()
            w.Write([]byte("我没有抢过别人"))
            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("创建订单失败"))
            return
        }
        tx.Commit()
        w.Write([]byte(fmt.Sprintf("the count i read is %d",goods.Count)))
    }else{
        tx.Rollback()
        w.Write([]byte("我啥子都么抢到"))
    }

    //如果有库存插入到订单表
}
  • 查看数据库,发现下单正常


    在这里插入图片描述
  • 查看jmeter的聚合结果,发现速度的提升了不少的
在这里插入图片描述

redis 锁

  • 前面写的都是同步方案,每次的请求都需要经过MySQL处理,如果请求量过大,可能造成MySQL服务宕机,导致服务终端,那么我们能不能使用异步的方式去处理呢?
  • 削峰:对于秒杀系统瞬时会有大量用户涌入,所以在抢购一开始会有很高的瞬间峰值。高峰值流量是压垮系统很重要的原因,所以如何把瞬间的高流量变成一段时间平稳的流量也是设计秒杀系统很重要的思路。实现削峰的常用的方法有利用缓存和消息中间件等技术。

那么下面的案例,我们就基于Redis来实现秒杀的功能

  • 使用队列来实现,我把商品放进一个队列里面,谁抢到了就可以继续下单,抢不到就返回
  • 同时我们为了减轻MySQL的压力,我们把请求放入队列(redis rabbitmq均可实现)里面去,通过脚本将订单从队列跑入到数据库中
配置Redis的相关数据
  • 配置队列数据,一共1000个,也就是说只有1000个用户可以抢到
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) {
    //从redis里面读取数据,如果读取到了,就进入下单环节
    var list = "goodslist"
    var orderList="orderlist"
    client := getRedis()
    /** 用于初始化一个1000库存的队列
    client.LTrim(list, 1, 0)     //先初始化一个空队列
    for i := 1; i <= 1000; i++ { //队列里面放1000个库存
        client.LPush(list, i)
    }
    return
    **/

    var res = client.RPop(list)
    val := res.Val()
    if len(val) > 0 {
        //抢到后把用户的id 存入 另外一个队列,用于创建订单
        r.ParseForm()
        uid:=r.FormValue("uid")

        //fmt.Println(uid)
        //return
        client.LPush(orderList,uid)
        msg:=fmt.Sprintf("我抢到了,我是第%v抢到的 我的用户id是 %v \n", val,uid)
        _, _ = w.Write([]byte(msg))
        fmt.Print(msg)
    } else {
        msg:="我啥子都没得抢到\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
}
  • 查看执行结果,这里面可能有人问,不是队列吗?怎么打印出来的没按顺序啊?这是因为go 对每一个请求都是开启一个协程去处理的,n个协程,不一定谁先回来


    在这里插入图片描述
  • 我们来看一下redis


    在这里插入图片描述

    在这里插入图片描述
  • 我们看一下请求的聚合结果,比MySQL的性能提升100倍左右


    在这里插入图片描述
使用redis incrby decrby来控制下单人数
  • 先来看一下代码
func addOrder(w http.ResponseWriter, r *http.Request) {
    //从redis里面读取数据,如果读取到了,就进入下单环节
    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("我的值现在是",val);
    //return
    if val <= total {
        //抢到后把用户的id 存入 另外一个队列,用于创建订单
        r.ParseForm()
        uid:=r.FormValue("uid")
        client.LPush(orderList,uid)
        msg:=fmt.Sprintf("我抢到了,我是第%d抢到的 我的用户id是 %v \n", val,uid)
        _, _ = w.Write([]byte(msg))
        fmt.Print(msg)
    } else {
        msg:="我啥子都没得抢到\n"
        _, _ = w.Write([]byte(msg))
    }
    return
}
  • 我们来看一下Redis


    在这里插入图片描述
  • 再来看一下请求聚合


    在这里插入图片描述
插曲
  • 在我使用incrby的过程中发生了一些小插曲,先来看截图,单次请求没有问题,多线程访问的时候就返回0,打印报错ERR max number of clients reached 原因是我使用完Redis并没有关闭资源
在这里插入图片描述
  • 修改完毕上面一个问题之后,又出现问题了connect: can't assign requested addressdial tcp,这个是因为response没有关闭导致的,所以,小伙伴们一定要记得关闭资源啊!

redis分布式锁

在开始之前,我们先来熟悉一下这几个命令(从 Redis 2.6.12 版本开始支持)

  • EX second :设置键的过期时间为 second 秒。 SET key value EX second 效果等同于 SETEX key second value 。

  • PX millisecond :设置键的过期时间为 millisecond 毫秒。 SET key value PX millisecond 效果等同于 PSETEX key millisecond value 。

  • NX :只在键不存在时,才对键进行设置操作。 SET key value NX 效果等同于 SETNX key value 。

  • XX :只在键已经存在时,才对键进行设置操作。

  • 我们来看一下代码

package main

import (
    "fmt"
    "github.com/go-redis/redis"
    "github.com/jinzhu/gorm"
    _ "github.com/jinzhu/gorm/dialects/mysql"
    "log"
    "net/http"
    "strconv"
    "time"
)

// 商品表
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"`   // 名称
    Count   int    `gorm:"column:count;type:int(11);NOT NULL" json:"count"`     // 库存
    Sale    int    `gorm:"column:sale;type:int(11);NOT NULL" json:"sale"`       // 已售
    Version int    `gorm:"column:version;type:int(11);NOT NULL" json:"version"` // 乐观锁,版本号
}

// 订单表
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"`                                             // 库存ID
    Name       string    `gorm:"column:name;type:varchar(30);NOT NULL" json:"name"`                                       // 商品名称
    CreateTime time.Time `gorm:"column:create_time;type:timestamp;default:CURRENT_TIMESTAMP;NOT NULL" json:"create_time"` // 创建时间
}

//实际表名
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) //打印sql语句
    //开启连接池
    db.DB().SetMaxIdleConns(100)   //最大空闲连接
    db.DB().SetMaxOpenConns(100)   //最大连接数
    db.DB().SetConnMaxLifetime(30) //最大生存时间(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)//这里会有一个问题,就是 我里面的程序执行过长,导致锁释放,那么程序末尾的删除锁 就会删除其他请求的锁,导致不可用
    if cmd.Val() == true {

        db := getDb()
        defer db.Close()

        // 先去查看商品表还有没有库存
        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
            //更新数据库
            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("我啥子都么抢到"))

        }
        client.Del(key)
    }

}

func getRedis() *redis.Client {
    client := redis.NewClient(&redis.Options{
        Addr: "127.0.0.1:6379",
    })
    return client
}
  • 但是这里会有一个问题,就是 我里面的程序执行过长,导致锁释放,那么程序末尾的删除锁 就会删除其他请求的锁,导致不可用,我们优化一下,给value赋值一个随机数,每次你删除之前,判断这个value和你的value是否一致,如果一致就删除,如果不一致就不删
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"
)

// 商品表
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"`   // 名称
    Count   int    `gorm:"column:count;type:int(11);NOT NULL" json:"count"`     // 库存
    Sale    int    `gorm:"column:sale;type:int(11);NOT NULL" json:"sale"`       // 已售
    Version int    `gorm:"column:version;type:int(11);NOT NULL" json:"version"` // 乐观锁,版本号
}

// 订单表
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"`                                             // 库存ID
    Name       string    `gorm:"column:name;type:varchar(30);NOT NULL" json:"name"`                                       // 商品名称
    CreateTime time.Time `gorm:"column:create_time;type:timestamp;default:CURRENT_TIMESTAMP;NOT NULL" json:"create_time"` // 创建时间
}

//实际表名
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) //打印sql语句
    //开启连接池
    db.DB().SetMaxIdleConns(100)   //最大空闲连接
    db.DB().SetMaxOpenConns(100)   //最大连接数
    db.DB().SetConnMaxLifetime(30) //最大生存时间(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)//这里会有一个问题,就是 我里面的程序执行过长,导致锁释放,那么程序末尾的删除锁 就会删除其他请求的锁,导致不可用
    if cmd.Val() == true {

        db := getDb()
        defer db.Close()

        // 先去查看商品表还有没有库存
        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
            //更新数据库
            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("我啥子都么抢到"))

        }
        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 实现分布式锁

为什么使用lua
  • 从redis2.6.0版本开始,通过内置的Lua解释器,可以使用EVAL命令对Lua脚本进行求值。
  • 减少网络开销。可以将多个请求通过脚本的形式一次发送,减少网络时延。
  • 原子操作。Redis会将整个脚本作为一个整体执行,中间不会被其他请求插入。因此在脚本运行过程中无需担心会出现竞态条件,无需使用事务,并且Redis会保证脚本会以原子性(atomic)的方式执行(要么都成功,要么都失败),当某个脚本正在执行的时候,不会有其他脚本或者Redis命令被执行。
  • 复用。客户端发送的脚本会永久存在redis中,这样其他客户端可以复用这一脚本,而不需要使用代码完成相同的逻辑。
学习使用lua
  • 参考文章
    参考文章
    在Redis中使用lua,我们先来看一下常用的命令

  • EVAL
    命令格式:EVAL script numkeys key [key …] arg [arg …]
    我们举个栗子

#   ------------------------------------------  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"

其中的 引号内的部分是脚本, 1代表key的数量,就是只有1个key ,这个key是谁呢?就是age 后面跟了两个参数 argv[1] 是18 argv[2]是60
上面的这个命令的意思和set age 18 EX 60是一样的,设置key1的值是10 过期时间是60秒

SCRIPT LOAD
  • 命令格式 SCRIPT LOAD script
  • SCRIPT LOAD 将脚本 script 添加到Redis服务器的脚本缓存中,并不立即执行这个脚本,而是会立即对输入的脚本进行求值。并返回给定脚本的 SHA1 校验和。如果给定的脚本已经在缓存里面了,那么不执行任何操作。其实就是将脚本缓存在Redis服务器中,如果已经缓存过了就不再缓存
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

命令格式:EVALSHA sha1 numkeys key [key …] arg [arg …]
刚才脚本不是被缓存了吗,怎么执行呢?在任何客户端通过EVALSHA命令,可以使用脚本的 SHA1 校验和来调用这个脚本。脚本可以在缓存中保留无限长的时间,直到执行SCRIPT FLUSH为止。

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

命令格式:SCRIPT FLUSH
清除Redis服务端所有 Lua 脚本缓存,注意是所有

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

命令格式:SCRIPT KILL
杀死当前正在运行的 Lua 脚本,当且仅当这个脚本没有执行过任何写操作时,这个命令才生效。 这个命令主要用于终止运行时间过长的脚本,比如一个因为 BUG 而发生无限 loop 的脚本,或者因为读取的key过大导致阻塞等等。
假如当前正在运行的脚本已经执行过写操作,那么即使执行SCRIPT KILL,也无法将它杀死,因为这是违反 Lua 脚本的原子性执行原则的。在这种情况下,唯一可行的办法是使用SHUTDOWN NOSAVE命令,通过停止整个 Redis 进程来停止脚本的运行,并防止不完整(half-written)的信息被写入数据库中。

使用redis-cli客户端执行lua文件
  • 完整命令
    redis-cli -h host -p port -a password -n db -–eval demo.lua k1 k2 , a1 a2
    解释:-h 后面跟远程Redis的ip; -p后面跟远程Redis端口号; -a后面跟密码; -n 后接的参数为选择的redis db;k1 k2 , a1 a2” 的在lua 脚本中获取的方式是使用全局变量KEYS 和ARGV。

  • 我们来举个栗子验证一下

  • lua 文件如下

-- 这个命令等价于 set key1 argv1 EX argv2
-- 比如下面这个栗子,设置age是18过期时间是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
  • 执行命令(注意 在key和arg中间有 一个逗号,逗号前后要空一格)
  • 第二个要注意的点就是 我们在go里面套用lua的时候,可以先使用redis-cli进行调试,比放入go里面调试要方便
zhangguofu@zhangguofudeMacBook-Pro bin $ ./redis-cli  --eval /Users/zhangguofu/website/goproject/script.lua name age , jimy  18 60
(integer) 1
  • 查看Redis结果
在这里插入图片描述
在go中使用lua
  • 来看一下lua脚本中的内容
--用户id
local userId    = tostring(KEYS[1])
--订单集合
local orderSet=tostring(KEYS[2])
-- 商品库存key
local goodsTotal=tostring(ARGV[1])
--订单队列
local orderList=tostring(ARGV[2])

-- 是否已经抢购到了,如果是返回
local hasBuy = tonumber(redis.call("sIsMember", orderSet, userId))
if hasBuy ~= 0 then
    return 0
end

-- 库存的数量
local total=tonumber(redis.call("GET", goodsTotal))
--return total
-- 是否已经没有库存了,如果是返回
if total <= 0 then
    return 0
end

-- 可以下单
local flag

-- 增加至订单队列
flag = redis.call("LPUSH", orderList, userId)

-- 增加至用户集合
flag = redis.call("SADD", orderSet, userId)

-- 库存数减1
flag = redis.call("DECR", goodsTotal)
-- 返回当时缓存的数量
return total

--[[


--  多行注释
]]
  • 看一下执行结果
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
  • 看一下go的代码
package main

import (
    "fmt"
    "github.com/go-redis/redis/v8"
    "io/ioutil"
    "log"
    "net/http"
    "sync"
)

const orderSet = "orderSet"     //用户id的集合
const goodsTotal = "goodsTotal" //商品库存的key
const orderList = "orderList"   //订单队列
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, 什么都没抢到 \n", userId)
        }else{
            fmt.Printf("userid: %s 抢到了, 库存: %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
}

  • 查看代码运行结果


    在这里插入图片描述
  • 查看Redis结果
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>  
  • 查看jmeter的聚合报告


    在这里插入图片描述
  • 相关代码已上传至github仓库
  • 如不能点击请选择查看原文

你可能感兴趣的:(秒杀要不要用锁?五种方案的代码实践和超卖演示)