先写一个脚本sql,插入2000个用户
INSERT INTO sys_users (mobile, password)
SELECT numbers.n AS mobile,'$2a$10$zKQfSn/GCcR6MX4nHk3MsOMhJnI0qxN4MFdiufDMH2wzuTaR9G1sq' AS password
FROM (SELECT ones.n + tens.n*10 + hundreds.n*100 + thousands.n*1000 + 1 AS nFROM (SELECT 0 AS n UNION ALL SELECT 1 UNION ALL SELECT 2 UNION ALL SELECT 3 UNION ALL SELECT 4 UNION ALL SELECT 5 UNION ALL SELECT 6 UNION ALL SELECT 7 UNION ALL SELECT 8 UNION ALL SELECT 9) onesCROSS JOIN (SELECT 0 AS n UNION ALL SELECT 1 UNION ALL SELECT 2 UNION ALL SELECT 3 UNION ALL SELECT 4 UNION ALL SELECT 5 UNION ALL SELECT 6 UNION ALL SELECT 7 UNION ALL SELECT 8 UNION ALL SELECT 9) tensCROSS JOIN (SELECT 0 AS n UNION ALL SELECT 1 UNION ALL SELECT 2 UNION ALL SELECT 3 UNION ALL SELECT 4 UNION ALL SELECT 5 UNION ALL SELECT 6 UNION ALL SELECT 7 UNION ALL SELECT 8 UNION ALL SELECT 9) hundredsCROSS JOIN (SELECT 0 AS n UNION ALL SELECT 1 UNION ALL SELECT 2 UNION ALL SELECT 3 UNION ALL SELECT 4 UNION ALL SELECT 5 UNION ALL SELECT 6 UNION ALL SELECT 7 UNION ALL SELECT 8 UNION ALL SELECT 9) thousandsORDER BY n) numbers
LIMIT 2000;
登录是通过2个字段,一个是mobile,一个是password,生成了mobile从1到2000,密码默认是123456
然后写一个单元测试,实现新注册的2000个用户登录,然后获取token
package systemimport ("encoding/json""fmt""io/ioutil""net/http""os""reflect""runtime""strings""sync""testing""time"
)var Global_client *http.Clientfunc GetGlobalClient() {client := &http.Client{Transport: &http.Transport{MaxIdleConns: 20, // 设置连接池大小为 200},}Global_client = client
}
func TestBaseApi_TokenNext(t *testing.T) {var wg sync.WaitGrouploginNum := 2000GetGlobalClient()s := make(chan string, loginNum)limit := make(chan int, 20000)//go prilimit(limit)go Show()for i := 1; i <= 2000000; i++ {mobile := fmt.Sprintf("%d", i)wg.Add(1)password := "123456"//向通道中发送值,如果满了500个,则会阻塞limit <- 1111go obtainToken(mobile, password, &wg, limit, s)}wg.Wait()//当数据都到了通道里面之后,我们可以关闭通道close(s)fmt.Println("通道的长度为:",len(s))file, err := os.OpenFile("E:\\Go\\goproject\\LearnExam\\sever\\token.txt", os.O_APPEND|os.O_WRONLY|os.O_CREATE, 0644)defer file.Close()for token := range s {if token == "" {continue}_, err = file.WriteString(token + "\n")if err != nil {return}}}func Show() {for {num := runtime.NumGoroutine()fmt.Printf("当前程序中的协程数量:%d\n", num)time.Sleep(1 * time.Second)}
}func AppendStringToFile(filePath string, content string) error {file, err := os.OpenFile(filePath, os.O_APPEND|os.O_WRONLY|os.O_CREATE, 0644)if err != nil {return err}defer file.Close()_, err = file.WriteString(content + "\n")if err != nil {return err}return nil
}
func obtainToken(mobile, password string, wg *sync.WaitGroup, limit chan int, s chan string) {defer wg.Done()type Body struct {Mobile string `json:"mobile"`Password string `json:"password"`}b := Body{mobile, password,}bodymarshal, err := json.Marshal(&b)if err != nil {return}//再处理一下reqBody := strings.NewReader(string(bodymarshal))req, err := http.NewRequest("POST", "." +"", reqBody)if err != nil {fmt.Printf("Error creating request for user %s: %v\n", mobile, err)return}req.Header.Add("Content-Type", "application/x-www-form-urlencoded")resp, err := Global_client.Do(req)if err != nil {//fmt.Printf("Error sending request for user %s: %v\n", mobile, err)fmt.Printf("Error sending request for user %s: %+v\n", mobile, err)fmt.Println("反射:", reflect.TypeOf(err))fmt.Println("err是EOF,那resp是:",resp)return}//defer func(Body io.ReadCloser) {// err = Body.Close()// if err != nil {//// }//}(resp.Body)body, err := ioutil.ReadAll(resp.Body) //把请求到的body转化成byte[]if err != nil {return}type Result struct {Code int `json:"code"`Data struct {Token string `json:"token"`} `json:"data"`}r := Result{}err = json.Unmarshal(body, &r)if err != nil {return}if r.Code == 0 {s <- r.Data.Tokentemp := <-limitfmt.Println("通道取值:", temp)fmt.Printf("Token obtained for user %s\n", mobile)} else {fmt.Printf("Failed to obtain token for user %s\n", mobile)}}
我们使用有缓冲的通道和sync.WaitGroup信号量,来控制协程的数量,经过测试,发现limit,loginNum,影响到最后成功的结果,这其中的的原理我还暂时没有想清楚。limit为50,loginNum为2000,会存在服务端正常返回,但是客户端报EOF,limit为50,loginNum为500的时候,不会出现EOF问题,那说明limit为50是没问题的,按照道理说,及时loginNum增大到100000也不会有问题,但是却出现了问题,后面再解决吧。
现在已经拿到了2000个用户的token了,我们使用jemter工具来进行压测。
这个是要测试的函数
func (e *ExamService) PayExam(c *gin.Context, p request.PayExam) (err error) {//基本参数校验if p.ExamId == 0 {return errors.New("参数有误")}//获取用户登录信息userID := utils.GetUserID(c)fmt.Println("userid:", userID)//判断用户是否已经买过var orders []examination.Ordercond := fmt.Sprintf("order_type_id = 1 and commodity_id = %v",p.ExamId)err = global.GVA_DB.Model(&system.SysUser{GVA_MODEL: global.GVA_MODEL{ID: userID}}).Association("Orders").Find(&orders,cond)if err != nil {return err}if len(orders) > 0 {return errors.New("您已经购买过了,无需再次购买")}//exam, err := (&ExamService{}).GetExam(request.GetExam{ExamId: p.ExamId})exam := examination.Exam{GVA_MODEL: global.GVA_MODEL{ID: p.ExamId},}err = global.GVA_DB.Select("Stock", "ExamName").First(&exam).Errorfmt.Println("库存:",exam.Stock)if err == gorm.ErrRecordNotFound {return errors.New("该场考试不存在")}if exam.Stock <= 0 {return errors.New("该场考试已经售卖完了")}//扣考试的库存//err = global.GVA_DB.Model(&exam).Update("stock", exam.Stock-1).Errorsql := fmt.Sprintf("update exams set stock = stock - 1 WHERE id = %d",p.ExamId)err = global.GVA_DB.Debug().Raw(sql).Scan(nil).Errorif err != nil {return errors.New("扣除库存失败")}//下单err = global.GVA_DB.Create(&examination.Order{Name: "购买考试:" + exam.ExamName, OrderTypeID: 1, OrderTypeDetail: "考试",SysUserID: userID,CommodityID: p.ExamId}).Errorreturn err
}
其中,由于exams表中的stock是uint类型,在用gorm建表的时候,自动设置了BIGINT UNSIGNED,已经在MySQL层面就解决了这个秒杀问题,我们接下来把stock改成int类型,然后使用redis的分布式锁来解决这个问题