Gin+Vue+微服务打造秒杀商城,打造高并发抢购平台

发布时间 2023-11-07 15:51:01作者: 天使angl

gin+vue实战

后端:

  • 用户管理
    • 用户列表
    • 登录/登出
  • 商品管理
    • 商品的增上改查
  • 活动管理
    • 商品关联
    • 成功率
    • redis队列,不成功的回到队列继续,成功的从队列删除

结束难点:

  • 代码和部署完全隔离
  • 怎么避免雪崩
  • 根据后端承载能力,进行限流和过载保护
  • 使用redis承载海量QPS
  • mysql性能瓶颈
  • 可扩展行(机器的扩展等)
  • 长连接

秒杀业务逻辑:

img

接入层功能:

img

实战技术选型

一、实战名称:gin+vue3+微服务打造秒杀商城

二、技术选型

1.前端:vue3,antdv,vue-router,axios

2.后端

  • gin框架
  • gorm
  • 微服务:集群部署
    • web
    • srv

gin+vue+微服务打造秒杀商城

一、流程

img

二、技术难点

  • 代码和部署完全隔离(微服务)
  • 怎么避免雪崩(微服务)
  • 根据后端承载能力,进行限流和过载保护(限流熔断)
  • 使用redis承载海量QPS(redis队列)
  • mysql性能瓶颈(sql优化,拆库,拆表)
  • 可扩展性(机器的扩展等)
  • 长连接(先不管)

三、项目分类

1.前端:zhiliao_vue_gin

2.micro-web:zhiliao_web

3.用户管理服务:zhiliao_user_srv

4.商品和活动管理服务:zhiliao_product_srv

5.秒杀服务:zhiliao_seckill_srv

表设计

一、管理员表

表名:sys_admin

字段名称类型长度说明
Id int 11 主键
UserName varchar 64 用户名
Password varchar 64 密码,md5加密
Desc varchar 255 用户描述
Status int 2 用户状态
CreateTime datetime 0 用户创建时间

二、用户表(注册)

表名:sys_user

字段名称类型长度说明
Id int 11 主键
Email varchar 64 邮箱地址
Password varchar 64 密码,md5加密
Desc varchar 255 用户描述
Status int 2 用户状态
CreateTime datetime 0 用户创建时间

三、商品表

表名:sys_product

字段名称类型长度说明
Id int 11 主键
Name varchar 64 商品名称
Price decimal 11,2 价格,保留两位小数
Num int 11 商品数量
Unit varchar 32 商品单位
Pic varchar 255 商品图片
Desc varchar 255 商品描述
CreateTime datetime 0 用户创建时间

四、活动表

表名:sys_product_seckill

字段名称类型长度说明
Id int 11 主键
Name varchar 64 活动名称
Price decimal 11,2 活动价格,保留两位小数,小于等于商品价格
Num int 11 参与秒杀的数量,小于等于商品数量
PId int 11 商品外键
StartTime datetime 0 秒杀开始时间
EndTime datetime 0 秒杀结束时间
CreateTime datetime 0 活动创建时间

五、订单表

表名:sys_orders

字段名称类型长度说明
Id int 11 主键
OrderNum varchar 64 订单编号
Uid int 11 用户外键,关联sys_user
SId int 11 活动的外键,关联sys_product_seckill
PayStatus int 2 支付状态
CreateTime datetime 0 订单创建时间

数据校验

一、vue中使用rules校验数据

1.使用

1.form中绑定rules
     <a-form :model="form" :rules="rules">

2.在要校验的item上设置prop属性,这里的prop是第三中的key(高版本的antdv用name代替了prop)
     <a-form-item label="邮箱地址" name="mail">    // 这里和下面的mail必须一致,不然获取不到值
           <a-input v-model:value="form.mail" placeholder="请输入正确的邮箱地址">

   注意:prop对应的不单单是rules规则里面的验证项,同时应该对应着我们form-item下的v-model的值  

3.在data中定义rules
     rules:{    // 这里的rules就是前面绑定的rules
        email:[{required: true,message: "必填",trigger: "blur"}]
      }
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13

2.使用自定义验证器

1.定义验证器:新版的antdv返回Promise

let validateEMail = async(rule, value) => {
      const reg = /^([a-zA-Z0-9]+[-_.]?)+@[a-zA-Z0-9]+\.[a-z]+$/;
      if (value == "" || value == undefined || value == null) {
        // callback(new Error("请输入邮箱"));
        return Promise.reject((new Error("请输入邮箱")));
      } else {
        if (!reg.test(value)) {
          return Promise.reject((new Error("请输入正确的邮箱")));
        } else {
          return Promise.resolve();
        }
      }
    };

如果报错:Warning: callback is deprecated. Please return a promise instead则使用下面的



2.使用自定义验证器
  {required: true,validator: validateEMail,trigger: "blur"}


正则中的符号含义:
  + :匹配前面的子表达式一次或多次。要匹配 + 字符,请使用 \+。
  ?:匹配前面的子表达式零次或一次,或指明一个非贪婪限定符。要匹配 ? 字符,请使用 \?
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27

3.表单提交的时候校验

1.form中加ref属性
    <a-form :model="form" :rules="rules" ref="form">
2.提交按钮传参,参数和前面的ref中的值一致
    <a-button type="primary" @click="onSubmit('form')">提交</a-button>

3.函数中校验
    sendMail(ruleForm) {
      alert(this.form.mail);
      this.$refs[ruleForm].validate().then(()=>{
        alert("校验通过");
      }).catch(() => {
        alert("校验不通过");
      })
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13

常用校验正则:https://www.cnblogs.com/lieone/p/11856330.html

二、golang中验证邮箱

func VerifyEmailFormat(email string) bool {
      pattern := `\w+([-+.]\w+)*@\w+([-.]\w+)*\.\w+([-.]\w+)*` //匹配电子邮箱
      reg := regexp.MustCompile(pattern)
      return reg.MatchString(email)
 }


 VerifyEmailFormat("12345@126.com")  // true
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

发送邮件

一、生成随机数

import (
    "fmt"
    "math/rand"
    "strings"
    "time"
)
func GenEmailCode(width int) string {
    numeric := [10]byte{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}
    r := len(numeric)
    rand.Seed(time.Now().UnixNano())

    var sb strings.Builder
    for i := 0; i < width; i++ {
        fmt.Fprintf(&sb, "%d", numeric[ rand.Intn(r) ])
    }
    return sb.String()
}
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17

二、发送邮件

// 使用beego下utils下的NewEMail
func SendEmail(to_email, msg string) {
        username := "2713058923@qq.com"    // 发送者的邮箱地址
        password := "xxx"      // 授权密码
        host := "smtp.qq.com"  // 邮件协议
        port := "587"          // 端口号

        emailConfig := fmt.Sprintf(`{"username":"%s","password":"%s","host":"%s","port":%s}`, username, password,host,port)
        fmt.Println("emailConfig", emailConfig)
        emailConn := utils.NewEMail(emailConfig)  // beego下的
        emailConn.From = strings.TrimSpace(from_email)
        emailConn.To = []string{strings.TrimSpace(to_email)}
        emailConn.Subject = "知了传课注册验证码"
        //注意这里我们发送给用户的是激活请求地址
        emailConn.Text = msg
        err := emailConn.Send()
        fmt.Println(err)
    }


使用:
// 生成六位随机数
session_email_code := utils.GenEmailCode(6)
// 消息内容
email_msg := fmt.Sprintf("您的注册验证码为:%s",session_email_code)
// 发送邮件
utils.EmailSend(mail, email_msg)


// 缓存邮件和随机数
"github.com/patrickmn/go-cache"

//初始化
c := cache.New(30*time.Second, 10*time.Second)
//使用
c.Set("Title", "Spring Festival", cache.DefaultExpiration)
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36

jwt-token认证

一、什么是JWT?

JSON Web Token:是一种跨域认证解决方案,它规定了一种Token实现方式,多用于前后端分离等场景

二、为什么需要JWT?

1.前后端不分离的验证逻辑

  • 前端提交数据
  • 后端校验存session,通过后保存session,生成session_id标识
  • 服务端返回响应时将上一步的session_id写入用户浏览器的Cookie
  • 前端每次请求都会自动携带包含session_id的Cookie
  • 服务端通过请求中的session_id就能找到之前保存的该用户那份session数据

2.使用jwt认证

服务端完成登录校验后,会生成一个令牌(就是token)再发回给用户,用户后续请求只需要带上这个Token,服务端解密之后就能获取该用户的相关信息了。

三、使用JWT

使用jwt-go完成生成JWT和解析JWT的功能

go get github.com/dgrijalva/jwt-go

1.生成jwt

  • 定义结构体,这个就是要返回给前端的

        type FrontUserToken struct { 
          // jwt的匿名字段 
          jwt.StandardClaims
          // 要返回给前端的用户信息
          Username string `json:"user_name"`
          UserId int `json:"user_id"`
        }
     
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
  • 定义JWT过期时间

// 过期时间1小时,n小时的话 * n
const TokenExpireDuration = time.Hour
 
  • 1
  • 2
  • 定义加密的盐,生成token和解析的时候都需要用到,使用同一个
var TokenSecret = []byte("gin_vue_token")
 
  • 1
  • 生成token
type UserToken struct {
    jwt.StandardClaims
    // 自定义的用户信息
    UserName string `json:"user_name"`

}

// 前端用户token过期时间
var FrontUserExpireDuration = time.Hour
var FrontUserSecretKey = []byte("front_user_token")


// 管理端用户token过期时间
var AdminUserExpireDuration = time.Hour * 2
var AdminUserSecretKey = []byte("admin_user_token")


// 生成token
func GenToken(UserName string,expireDuration time.Duration,secret_key []byte)  (string,error){

    user := UserToken{
        jwt.StandardClaims{
            // 现在 + 加上传的过期时间
            ExpiresAt:time.Now().Add(expireDuration).Unix(),
            Issuer:"micro_gin_vue",
        },
        UserName,

    }

    token := jwt.NewWithClaims(jwt.SigningMethodHS256,user)
    return token.SignedString(secret_key)

}
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34

2.解析jwt

// 认证token
func AuthToken(tokenString string,secretKey []byte) (*UserToken, error){

    // 解析token
    token,err := jwt.ParseWithClaims(tokenString,&UserToken{}, func(token *jwt.Token) (key interface{}, err error) {
        return secretKey,nil
    })

    if err != nil {
        return nil,err
    }

    clasims,is_ok := token.Claims.(*UserToken)

    // 验证token
    if is_ok && token.Valid { // 正常的
        return clasims,nil
    }

    return nil,errors.New("token valid err")

}
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22

3.中间件,在所有需要验证的路由中加上该解析,如果没有token则不让访问

func JwtTokenValid(ctx *gin.Context)  {

    auth_header := ctx.Request.Header.Get("Authorization")

    if auth_header == "" {
        ctx.JSON(http.StatusOK,gin.H{
            "code":401,
            "msg":"请携带token",
        })
        ctx.Abort()
        return
    }

    auths := strings.Split(auth_header," ")

    bearer := auths[0]
    token := auths[1]

    if len(token) == 0 || len(bearer) == 0 {
        ctx.JSON(http.StatusOK,gin.H{
            "code":401,
            "msg":"请携带正确格式的token",
        })
        ctx.Abort()
        return
    }

    user, err := utils.AuthToken(token,utils.AdminUserSecretKey)

    if err != nil {
        ctx.JSON(http.StatusOK,gin.H{
            "code":401,
            "msg":"无效的token",
        })
        ctx.Abort()
        return
    }

    ctx.Set("user_name",user.UserName)
    ctx.Next()

}
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42

key:Authorization

value:token值

前端状态管理

一、自定义状态管理

const front_token_key = "front_token"
const front_user_key = "front_user_name"

const admin_token_key = "admin_token"
const admin_user_name_key = "admin_user_name"


class Auth {
    // 构造函数浏览器刷新的情况下从localStorage加载
    constructor(){
        this.token = localStorage.getItem(front_token_key)
        this.username = localStorage.getItem(front_user_key)
        this.admin_token = localStorage.getItem(admin_token_key)
        this.admin_user_name = localStorage.getItem(admin_user_name_key)

    }

    // 用户端存储信息
    setFrontAuth(token,username){
        this.token = token
        this.username = username

        // 用户端的管理端需要区分开
        localStorage.setItem(front_token_key,token)
        localStorage.setItem(front_user_key,username)


    }

    // 管理端存储信息
    setAdminAuth(admin_token,admin_user_name){
        this.admin_token = admin_token
        this.admin_user_name = admin_user_name

        localStorage.setItem(admin_token_key,admin_token)
        localStorage.setItem(admin_user_name_key,admin_user_name)

    }

    // 用户端清空缓存信息
    delFrontAuth(){
        this.token = null,
        this.username = null

    }

    // 管理端清空缓存信息
    delAdminAuth(){
        this.admin_token = null
        this.admin_user_name = null

    }
    // 也可以定义导航守卫
    is_authed(){
        if(this.token && this.username){
            return true
        }else{
            return false
        }
    }
}

// 导出,单例
const auth = new Auth()
export default auth


在main.js中定义全局变量


router.js中定义导航守卫
    // 定义导航守卫
    router.beforeEach((to, from, next) => {
      if (to.path === '/login' || to.path === '/register') {
        next('/');
      } else {
        let token = localStorage.getItem('token');

        if (token === 'null' || token === '') {
          next('/login');
        } else {
          next();
        }
      }
    });
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82
  • 83
  • 84
  • 85

二、使用vuex第三方库,适用于中大型项目

vue3.0不支持vuex,参考:https://www.codingsky.com/doc/2020/7/31/1024.html

1.vue2中使用vuex示例代码

npm install vuex --save

vuex/vuex.js中:
    import Vuex from 'vuex';

    const store = new Vuex.Store({
        state: {
            // 没有获取到设置为null
            token:localStorage.getItem('token')?localStorage.getItem('token') : null
            user:localStorage.getItem('user')?localStorage.getItem('user') : null

        },
        mutations: {
            setAuth(state,token,user){
                state.token = token;
                localStorage.setItem('token', user.Authorization);
            },
            delAuth(state){
                state.token = null;
                state.user = null;
                localStorage.removeItem("token")
                localStorage.removeItem("user")

            }

        }

    export default store



main.js中use下
    import vuex from './vuex/vuex.js'

    app.use(vuex);

    设置全局变量:
    app.config.globalProperties.$vuex = vuex


router.js中:
    // 定义导航守卫
    router.beforeEach((to, from, next) => {
      if (to.path === '/login') {
        next();
      } else {
        let token = localStorage.getItem('token');

        if (token === 'null' || token === '') {
          next('/login');
        } else {
          next();
        }
      }
    });
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53

state:数据仓库,主要存储共享的数据,不是存储的过程,是存储的结果,

getters:获取数据

mutactions:存数据

actions:对数据先进行处理,再存储到仓库,也可以不用进行处理

*执行流程:*后端返回数据,使用actions先进行数据处理(也可以不处理),然后通过

mutation 把处理后的数据放入数据仓库state中,想使用数据就通过
getters从数据仓库state中取。

antdv分页

结合table使用分页

一、在table标签上加属性

 <a-table :pagination="users_pagenation">
 
  • 1

二、在data中指定users_pagenation

front_users:[],
columns,
position:top,
users_pagenation:{
    current:1,
    pageSize:5,
    pageSizeOptions:['10','20','30'], // 可选每页显示几条
    showSizeChanger:true,
    total:11
}
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

三、绑定change事件

<a-table @change="chanPage">
 
  • 1

四、定义chanPage事件

chanPage(pagination){
    this.users_pagenation.current = pagination.current
    this.users_pagenation.pageSize = pagination.pageSize
},
 
  • 1
  • 2
  • 3
  • 4

jmeter压测工具

一、介绍

1.免费的

2.使用简单

3.能满足大多数压测要求

二、环境准备及软件安装

1.jdk环境搭建

  • 环境配置:
    • Java_Home:jdk安装路径
    • %Java_Home%\bin;%Java_Home%\jre\bin;
  • 验证:
    • java -version

2.软件安装

  • 下载地址:http://jmeter.apache.org/download_jmeter.cgi,注意jdk版本对应上
  • 解压即可,不需要安装

3.启动:双击解压文件夹bin目录下的jmeter.bat,启动之后会有两个窗口,一个cmd窗口,一个JMeter的 GUI,不要使用GUI运行压力测试,GUI仅用于压力测试的创建和调试;执行压力测试请不要使用GUI。使用下面的命令来执行测试:

jmeter -n -t [jmx file] -l [results file] -e -o [Path to web report folder]

4.通过 【Options】->【Choose Language】变更为简体中文

5.修改字体大小

选项—>外观—>windows

  • 编辑bin目录下的jmeter.properties文件,修改jsyntaxtextarea.font.size的值,并将注释取消
  • 在jmeter.bat文件中添加如下代码
set JVM_ARGS=%JVM_ARGS% -Dswing.plaf.metal.controlFont=Dialog-20
set JVM_ARGS=%JVM_ARGS% -Dswing.plaf.metal.systemFont=Dialog-20
set JVM_ARGS=%JVM_ARGS% -Dswing.plaf.metal.userFont=SansSerif-20
set JVM_ARGS=%JVM_ARGS% -Dswing.plaf.metal.smallFont=SansSerif-20
 
  • 1
  • 2
  • 3
  • 4

三、创建测试

Jmeter-http接口测试添加步骤:

1.创建线程组

在左侧的"TestPlan"上右键 【添加】–>【Threads(Users)】–>【线程组】,设置线程数和循环次数。只设置这两个即可,比如1000的线程数,1次循环

2.配置元件

在我们刚刚创建的线程组上右键 【添加】–>【配置元件】–>【HTTP请求默认值】。只需要配置协议、地址和端口这三项即可,这样后面所有的请求都是基于现在的这个进行的,比如http://127.0.0.1:8080,后面的的请求只需要使用path即可

3.http请求

在“线程组”右键 【添加-】->【samlper:取样器】–>【HTTP 请求】设置我们需要测试的API的请求路径和数据。我这里是用的json

4.添加请求头

线程组上右键 【添加】–>【配置元件】–>【HTTP信息头管理器】

5.添加断言

线程组上右键 【添加】–>【断言】–>【响应断言】,根据响应的数据来判断请求是否正常。比如只根据状态码判断是否正常。

要测试的响应字段:响应代码

模式匹配规则:Equales

要测试的模式:200

错误提示信息:“出错啦!”

6.添加察看结果树

线程组上右键 【添加】–>【监听器】–>【察看结果树】。点击工具栏上的运行按钮就可以看到结果了

7.添加Summary Report

线程组上右键 【添加】–>【监听器】–>【Summary Report:汇总报告】。点击工具栏上的运行按钮就可以看到结果了

以上的测试计划已构建完整,点击左上角的报错按钮保存下

8.执行测试计划

cmd中执行:进入jmeter的bin目录,执行下面的命令

jmeter -n -t [jmx file] -l [results file] -e -o [Path to web report folder]

  • jmx file:测试计划文件路径
  • results file:测试结果文件路径
  • Path to web report folder:web报告保存路径

e.g.:jmeter -n -t [testplan/RedisLock.jmx] -l [testplan/result/result.txt] -e -o [testplan/webreport]

秒杀接口压测

一、需要的添加及使用的jmeter技术

1.需要满足的条件:

  • 第一步使用邮箱地址、密码登录获取到返回的token
  • 第二步携带token请求秒杀接口

需要邮箱地址、密码及返回的token

2.jmeter测试需要用到的技术

  • jmeter操作数据库,读取邮箱地址和密码
  • jmeter关联,使用第一步返回的token作为参数执行第二步,jmeter关联就可以保存这个token信息

二、jmeter操作数据库

1.下载mysql-connector-java-5.1.7-bin.jar,地址:https://dev.mysql.com/downloads/connector/j/,

  • 选择Platform Independent
  • 选择ZIP文件进行下载

2.解压,把里面的jar包放到jmeter的lib目录下

3.配置连接信息

  • 线程组右键添加“配置原件”–“JDBC Connection Configuration”
  • 线程组右键添加“samlper:取样器” – “JDBC Request”
  • 在TestPlan页面,点击浏览 ,将目录或jar添加到类路径 Add directory or jar to classpath。此处选择我们刚刚放在lib下的jar即可
  • JDBC Connection Configuration页面配置连接信息
    • 数据库:mysql
    • DriverName–>com.mysql.jdbc.Driver
    • URL–>jdbc:mysql://host:port/{dbname}?allowMultiQueries=true&serverTimezone=UTC
    • 用户名、密码

4.使用

在JDBC Request 页面

三、jmeter关联

1.添加关联

  • 在某个请求上右键添加”后置处理器“ – ”json提取器“
  • 设置
    • 响应字段:主体
    • 引用名称:token
    • 正则表达式:$.key1.key2

2.获取关联数据

  • parameters中获取:${token}

四、压测指标

  1. 压测前要明确压测功能和压测指标,一般需要确定的几个问题:
  2. 固定接口参数进行压测还是进行接口参数随机化压测?
  3. 要求支持多少并发数?
  4. TPS(每秒钟处理事务数)目标多少?响应时间要达到多少?
jdbc request:设置结果集存储的变量:user

BeanShell 后置处理器:

var email = vars.getObject("user").get(0).get("email");
vars.put("email",email.toString());
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

测试问题记录:

  • 并发查询怎么确保每个线程一个用户?使用计数器,不勾选“与每用户独立的跟踪计数器”选项
select email from front_user WHERE email not in (SELECT uemail from orders) limit ${user_offset},1
 
  • 1
  • 相隔时间很小的时候,可能一个用户会下单两次都成功

五、需要明确的问题

有错误率同开发确认,确定是否允许错误的发生或者错误率允许在多大的范围内;

5.1、Throughput吞吐量每秒请求的数大于并发数,则可以慢慢的往上面增加;

若在压测的机器性能很好的情况下,出现吞吐量小于并发数,说明并发数不能再增加了,可以慢慢的往下减,找到最佳的并发数;压测结束,登陆相应的web服务器查看CPU等性能指标,进行数据的分析;

5.2、最大的tps:不断的增加并发数,加到tps达到一定值开始出现下降,那么那个值就是最大的tps。

5.3、最大的并发数:最大的并发数和最大的tps是不同的概率,一般不断增加并发数,达到一个值后,服务器出现请求超时,则可认为该值为最大的并发数。

**5.4、**压测过程出现性能瓶颈,若压力机任务管理器查看到的cpu、网络和cpu都正常,未达到90%以上,则可以说明服务器有问题,压力机没有问题。

5.5、影响性能考虑点包括:数据库、应用程序、中间件(tomact、Nginx)、网络和操作系统等方面。

秒杀优化

一、前端优化

可以使用静态化的方式,把常用的资源加载到缓存中,不要经常访问后端,减轻一部分服务器压力

二、后端优化

1.秒杀功能独立,不受其他接口的影响,这里我们使用的微服务,秒杀是单独的服务

2.秒杀数量限制(限流):每隔一段时间发放一个有效的抢购

3.消息队列:减小数据库压力,生产者生产任务存放在队列中,消费者从队列中取任务消费,可以有效降低数据库压力

消息队列的几种模式:

简单模式:生产者生产任务放到队列中,消费者从队列中取任务消费

工作模式:生产者生产任务放到队列中,多个消费者从队列中取任务消费

订阅模式:

路由模式:

话题模式:

RPC模式:

rabbitmq

一、环境安装

1.windows安装:https://www.cnblogs.com/JustinLau/p/11738511.html

2.linux安装

安装:sudo apt-get install rabbitmq-server

查看状态:service rabbitmq-server status / systemctl status rabbitmq-server

开放端口:

  • 15672:管理后台
  • 5672:连接服务的端口

3.web可视化配置:

  • sudo rabbitmq-plugins enable rabbitmq_management
  • service rabbitmq-server restart # 重启服务
  • 访问:
    • 本地:localhost:15672 用户名和密码都是guest,只适用于本地访问
    • 其他机器访问: ip:15672 必须得创建用户并授权

4.配置用户及授权

查看用户:sudo rabbitmqctl list_users

添加用户:sudo rabbitmqctl add_user admin(用户名) admin(密码)

授权:sudo rabbitmqctl set_user_tags admin(用户名) administrator(角色)

  • administrator(超级管理员)
  • monitoring(监控者):可以查看rabbitmq节点的相关信息
  • policymaker(策略制定者):无法查看节点的相关信息
  • management(普通管理者):无法看到节点信息,也无法对策略进行管理
  • none(其他):无法登陆管理控制台,通常就是普通的生产者和消费者

5.rabbitmqctl服务的使用

查看当前用户列表:sudo rabbitmqctl list_users

添加用户:sudo rabbitmqctl add_user admin(用户名) admin(密码)

删除用户:sudo rabbitmqctl delete_user admin(用户名)

修改用户密码:sudo rabbitmqctl change_password admin(用户名) admin1(新密码)

设置vhost:

  1. sudo rabbitmqctl add_vhost /myvhost
  2. sudo rabbitmqctl set_permissions -p /myvhost myuser “." ".” “.*”

vhost说明:virtual host相当于一个单独的rabbitmq服务器,每个virtual是独立的,不可互通的,相当于mysql中的数据库,都是独立的,可以单独设置权限,Virtual Name一般以/开头

6.几种交换机

  • direct:直连,通过routingKey和exchange决定的那个唯一的queue可以接收消息
    • routing_key和exchange对应起来
  • fanout:发布/订阅,所有bind到此exchange的queue都可以接收消息
    • routing_key可以省略
  • topic:和direct类似,但是在匹配规则上进行了扩展,支持通配符的方式
  • headers:通过headers 来决定把消息发给哪些queue

二、go使用rabbitmq

1.下载第三方库:go get github.com/streadway/amqp

2.连接

// 连接rabbitmq
conn,_ := amqp.Dial("amqp://用户名:密码@IP:端口号")   // 端口号:5672
defer conn.close

// 打开通道
ch, err := conn.Channel()
defer ch.Close()

// 声明队列
queue,err_q := ch.QueueDeclare("mysql_queue",false,false,false,false,nil)
fmt.Println(err_q)

// 生产任务:生产者
ch.Publish("",queue.Name,false,false,amqp.Publishing{
    ContentType:"text/plain",
    Body:[]byte("hello world"),
})

// 消费者
msgs,err_c := ch.Consume("mysql_queue","my_consumer",false,false,false,false,nil)
fmt.Println(err_c)

for msg := range msgs{  // chan类型
    // DeliveryTag:唯一标识
    fmt.Println(msg.DeliveryTag,string(msg.Body))
}
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26

3.流程

  • 生产者创建channel发送消息到交换机
  • 交换机根据bind队列将消息发送到队列
  • 队列存储消息并负责分发消息到消费者
  • 消费者使用channel获取消息并消费,也可以选择拒收消息,消息重新打回队列,在规定时间后重试

4.确保服务器重启不会清空队列

// 1.创建队列设置持久化:durable表示是否持久化
queue,err_q := ch.QueueDeclare("my_queue",true,false,false,false,nil)

// 2.生产者设置持久化,DeliveryMode

// 3.消费者持久化:如果生产者这边设置了持久化,那么消费者同样也需要设置成持久化。
amqp.Publishing{
        ContentType:"text/plain",
        Body:[]byte("hello world"),
        DeliveryMode:amqp.Persistent,
    }
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11

5.消息确认机制

// 消费者消费的时候会出现两种情况,消费完成和消费失败,消费失败的要再次回到队列,重新分配

// 1.在消费的时候autoAck设置未false,表示不自动确认,当消息消费失败会再次放到队列

// 2.消费成功后手动确认,这样就会从队列中删减掉改任务,不会重复执行
deliveries,err_c := ch.Consume("my_queue","my_consumer",false,false,false,false,nil)
fmt.Println(err_c)

for delivery := range deliveries{
    delivery.Ack(true)
    // delivery.Ack(false)  // 失败重新放入队列中,注意这里需要加延迟时间,不然接收到的消息还是这个失败的,
        // 可以加重试次数,使用缓存计数的方式
    // delivery.Ack(true)  // 拒绝接收并且重新放回队列

}
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15

6.使用交换机

// 创建两个队列
queue1,err_q1 := ch.QueueDeclare("first_queue",true,false,false,false,nil)
queue2,err_q2 := ch.QueueDeclare("second_queue",true,false,false,false,nil)

// 创建两个交换机
err1 := ch.ExchangeDeclarePassive("frist_exchange","direct",true,false,false,false,nil)
err2 := ch.ExchangeDeclarePassive("second_exchange","direct",true,false,false,false,nil)

// queue和交换机绑定
err3 := ch.QueueBind(queue.Name,"frist_routingKey","frist_exchange",false,nil)
err4 := ch.QueueBind(queue.Name,"second_routingKey","second_exchange",false,nil)
// 第一个参数是队列名称,第二个参数是routingKey,第三个参数是交换机名称

// 生产者:第一个参数是交换机名称,第二个参数routingKey
err_p := ch.Publish("frist_exchange","frist_routingKey",false,false,amqp.Publishing{
        ContentType:"text/plain",
        Body:[]byte("hello world"),
        DeliveryMode:amqp.Persistent,
})

// 消费者:使用queue name
deliveries,err_c := ch.Consume("my_queue","my_consumer",false,false,false,false,nil)
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22

7.限流:确保消费者每次只能消费一个任务,消费完成后再分配任务,ack后再继续接收任务

第一种方式:

//设置每次从消息队列获取任务的数量
err = ch.Qos(
    1,    //预取任务数量,这里可以理解为线程的数量
    0,    //预取大小
    false,    //全局设置
)

if err != nil {
    //无法设置Qos
    return err
}
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11

8.notify确认

生产者的任务是否成功入列

// 在publish之前
ret := <- ch.NotifyReturn(make(chan amqp.Return))
if (string(ret.Body) != ""){
    // 获得body重新发,注意这里得需要异步执行,不然会卡死:ret.Body
}
 
  • 1
  • 2
  • 3
  • 4
  • 5

参数说明:

创建队列:

 
  • 1

创建交换机:

 
  • 1

生产者:

exchange:交换机名称
key:routingKey
mandatory:如果生产者生产的任务没有正常进入队列中,设置为true会返还给生产者,设置为false会直接丢弃
immediate:
msg:发送的消息,amqp.Publishing类型的数据
 
  • 1
  • 2
  • 3
  • 4
  • 5

消费者:

queue:队列名称
consumer:消费者名称
autoAck:自动确认
exclusive:
noLocal:
noWait:
args:参数
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

redis的使用

下载第三方库:github.com/garyburd/redigo/redis

一、连接redis

conn,err := redis.Dial("tcp","10.1.210.69:6379")

defer conn.Close()
 
  • 1
  • 2
  • 3

二、设置过期时间

设置key过期时间
conn.Do("expire", "name", 10) //10秒过期
 
  • 1
  • 2

二、设置key、value

conn.Do("SET", "name", "hallen")
 
  • 1

三、获取key对应的值

redis.String(conn.Do("GET", "name"))
 
  • 1

git版本控制

gitlab

一、介绍

二、安装服务

1.安装依赖包

sudo apt-get update
sudo apt-get install -y curl openssh-server ca-certificates
 
  • 1
  • 2

2.邮件配置

sudo apt-get install -y postfix
 
  • 1

选择Internet site

3.添加镜像

curl https://packages.gitlab.com/install/repositories/gitlab/gitlab-ce/script.deb.sh | sudo bash
 
  • 1

4.安装gitlab

sudo apt-get install gitlab-ce   /gitlab-ee


如果报错:
    Unable to fetch some archives, maybe run apt-get update or try with --fix-missing?
执行:
    sudo apt-get update
    sudo apt-get upgrade
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

出现如下说明安装成功了img

5.修改访问路径及端口号,端口号必须开放,否则访问不了

sudo vim /etc/gitlab/gitlab.rb

external_url 修改为:http://ip:端口号
unicorn['port'] = 8080修改为自己的备用端口号,不能和上面的重复
 
  • 1
  • 2
  • 3
  • 4

6.更新配置

sudo gitlab-ctl reconfigure
 
  • 1

7.重启服务

sudo gitlab-ctl restart
 
  • 1

8.打开 sshd 和 postfix 服务

service sshd start
service postfix start
 
  • 1
  • 2

9.查看状态

sudo gitlab-ctl status
 
  • 1

访问:http://ip:端口号

第一次访问会默认以root管理员用户登陆,需要输入两遍密码

10.本地设置git的用户名密码

git config --global user.name "hallen"
git config --global user.email "1277405413@qq.com"
 
  • 1
  • 2

11.访问502错误

  • 内存不够,至少需要2G内存
  • 端口被占用:netstat -ntpl
  • gitlab占用内存太多,导致服务器崩溃。尤其是使用阿里云服务器最容易出现502
    • swap

查看日志:sudo gitlab-ctl tail -f unicorn

初始化项目

一、仓库已有项目,本地怎么拉取

git clone 地址
 
  • 1

二、本地已有项目,怎么提交到远程

初始化仓库:git init
// 手动的为你的远程仓库的地址在本地起一个别名:
git remote add origin 仓库地址
// 从远程分支拉取master分支并与本地master分支合并
git pull origin master:master
//提交本地分支到远程分支
git push -u origin master
// 提交本地代码:
git add -A
git commit -m ''
git push --set-upstream origin master
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11

gin项目部署

windows部署

window上部署:bee pack -be GOOS=windows

  • 进入到项目目录,执行:bee pack -be GOOS=windows
    • 如果发生错误:
      • SET CGO_ENABLED=0
      • SET GOOS=windows
      • SET GOARCH=amd64
      • bee pack -be GOOS=windows
    • 将打包好的项目包拷贝到要存放的路径下,解压
    • 安装nssm服务管理工具: 支持Windows 7, Windows 8 and Windows 10
      • 管理员身份打开cmd,进入到nssm软件存放exe文件的目录
      • nssm install servicename就是你要添加的服务的名称
      • 然后在弹出的框中选择第二步解压文件夹中的exe文件
    • 开始中搜服务,找到 ,启动即可
    • 修改服务的指向路径:
      • 1.进入服务,查看路径,【开始】=>【运行】=>【services.msc】
      • 2.进入注册表,修改服务路径【开始】=>【运行】=>【regedit】,打开HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\【服务名称】,找到要修改的服务名称,然后修改【ImagePath】中数据值即可

vue项目部署

linux上部署

一、独立部署

1.进入项目目录使用bee工具打包

bee pack -be GOOS=linux
 
  • 1

2.将打包文件放在linux中

安装:sudo apt-get install lrzsz
 
  • 1

3.修改权限

chmod 777 -R /home/go
 
  • 1

4.进入到该目录

nohup 命令启动:nohup ./项目名称 &
指定日志路径:nohup ./项目名称 >run.log 2>&1 &
 
  • 1
  • 2

二、supervisor部署

1.安装supervisor

sudo apt-get install supervisor
 
  • 1

2.创建配置文件

1.在/etc/supervisor/conf.d目录下新建文件:supervisord.conf
2.配置内容如下:
    [program:zhiliao_web]
    # 项目文件夹
    directory = /home/go/src/zhiliao_web
    # 项目可执行文件位置
    command = /home/go/src/zhiliao_web/zhiliao_web
    autostart = true
    startsecs = 5
    user = root
    redirect_stderr = true
    # 输出日志文件的位置
    stdout_logfile = /home/go/src/zhiliao_web/supervisor.log

    port=127.0.0.1:9000
    #登录web用的用户名和密码
    username=user
    password=admin
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18

3.supervisor命令

  • 启动supervisor服务:supervisord -c /etc/supervisor/supervisord.conf
  • 如果提示有程序已经在运行,先把服务停了:systemctl stop supervisor.service
  • 重启supervisord:supervisorctl reload
  • 进入supervisor客户端:supervisorctlstart program_namestop xxxrestart xxx

三、vue项目部署

1.下载node文件

uname -a查看系统位数(x86_64表示64位系统, i686 i386表示32位系统)

https://nodejs.org/en/download/ 下载对应位数的编译好的文件

将文件放到linux服务器并解压

解压:tar -xvf   node-v6.10.0-linux-x64.tar.xz  
重命名(软连接要用):mv node-v6.10.0-linux-x64  nodejs
 
  • 1
  • 2

2.建立软连接

npm:ln -s /home/hallen4/go/node/node-v14.15.1/bin/npm /usr/local/bin/
node:ln -s /home/hallen4/go/node/node-v14.15.1/bin/node /usr/local/bin/
 
  • 1
  • 2

3.验证

node -v
 
  • 1

4.linux文件监听限额
cd /proc/sys/fs/inotify/

临时限额:

sudo sysctl fs.inotify.max_user_watches = 524288 
sudo sysctl -p
 
  • 1
  • 2

永久限额

echo fs.inotify.max_user_watches = 524288 | sudo tee -a /etc/sysctl.conf 
sudo sysctl -p
 
  • 1
  • 2

1.在/etc/supervisor/conf.d目录下新建文件:supervisord.conf
2.配置内容如下:
[program:zhiliao_web]
# 项目文件夹
directory = /home/go/src/zhiliao_web
# 项目可执行文件位置
command = /home/go/src/zhiliao_web/zhiliao_web
autostart = true
startsecs = 5
user = root
redirect_stderr = true
# 输出日志文件的位置
stdout_logfile = /home/go/src/zhiliao_web/supervisor.log

port=127.0.0.1:9000
#登录web用的用户名和密码
username=user
password=admin
 
  • 1
  • 2
  • 3
  • 4

3.supervisor命令

- 启动supervisor服务:supervisord -c /etc/supervisor/supervisord.conf
- 如果提示有程序已经在运行,先把服务停了:systemctl stop supervisor.service
- 重启supervisord:supervisorctl reload
- 进入supervisor客户端:supervisorctlstart program_namestop xxxrestart xxx

## 三、vue项目部署

1.下载node文件

uname -a查看系统位数(x86_64表示64位系统, i686 i386表示32位系统)

[https://nodejs.org/en/download/ ](https://nodejs.org/en/download/下载对应位数的编译好的文件)下载对应位数的编译好的文件

将文件放到linux服务器并解压
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18

解压:tar -xvf node-v6.10.0-linux-x64.tar.xz
重命名(软连接要用):mv node-v6.10.0-linux-x64 nodejs


2.建立软连接
 
  • 1
  • 2
  • 3

npm:ln -s /home/hallen4/go/node/node-v14.15.1/bin/npm /usr/local/bin/
node:ln -s /home/hallen4/go/node/node-v14.15.1/bin/node /usr/local/bin/


3.验证
 
  • 1
  • 2
  • 3

node -v


4.linux文件监听限额
cd /proc/sys/fs/inotify/

临时限额:
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

sudo sysctl fs.inotify.max_user_watches = 524288
sudo sysctl -p


永久限额
 
  • 1
  • 2
  • 3

echo fs.inotify.max_user_watches = 524288 | sudo tee -a /etc/sysctl.conf
sudo sysctl -p