gin+vue实战
后端:
- 用户管理
- 用户列表
- 登录/登出
- 商品管理
- 商品的增上改查
- 活动管理
- 商品关联
- 成功率
- redis队列,不成功的回到队列继续,成功的从队列删除
结束难点:
- 代码和部署完全隔离
- 怎么避免雪崩
- 根据后端承载能力,进行限流和过载保护
- 使用redis承载海量QPS
- mysql性能瓶颈
- 可扩展行(机器的扩展等)
- 长连接
秒杀业务逻辑:
接入层功能:
实战技术选型
一、实战名称:gin+vue3+微服务打造秒杀商城
二、技术选型
1.前端:vue3,antdv,vue-router,axios
2.后端
- gin框架
- gorm
- 微服务:集群部署
- web
- srv
gin+vue+微服务打造秒杀商城
一、流程
二、技术难点
- 代码和部署完全隔离(微服务)
- 怎么避免雪崩(微服务)
- 根据后端承载能力,进行限流和过载保护(限流熔断)
- 使用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 | 主键 |
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("[email protected]") // 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 := "[email protected]" // 发送者的邮箱地址
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}
四、压测指标
- 压测前要明确压测功能和压测指标,一般需要确定的几个问题:
- 固定接口参数进行压测还是进行接口参数随机化压测?
- 要求支持多少并发数?
- 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:
- sudo rabbitmqctl add_vhost /myvhost
- 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
出现如下说明安装成功了
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 "[email protected]"
- 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