diff --git a/go.mod b/go.mod index ee31d58..2eefae5 100644 --- a/go.mod +++ b/go.mod @@ -12,6 +12,8 @@ require ( github.com/golang-jwt/jwt/v5 v5.3.0 github.com/google/uuid v1.6.0 github.com/gorilla/websocket v1.5.3 + github.com/sirupsen/logrus v1.9.3 + go.uber.org/ratelimit v0.3.1 gopkg.in/yaml.v3 v3.0.1 gorm.io/driver/mysql v1.6.0 gorm.io/gorm v1.31.0 @@ -20,7 +22,7 @@ require ( require ( filippo.io/edwards25519 v1.1.0 // indirect github.com/RoaringBitmap/roaring/v2 v2.10.0 // indirect - github.com/benbjohnson/clock v1.3.0 // indirect + github.com/benbjohnson/clock v1.3.5 // indirect github.com/bits-and-blooms/bitset v1.24.0 // indirect github.com/blevesearch/bleve_index_api v1.2.9 // indirect github.com/blevesearch/geo v0.2.4 // indirect @@ -77,7 +79,6 @@ require ( github.com/philhofer/fwd v1.2.0 // indirect github.com/pkg/errors v0.9.1 // indirect github.com/rs/xid v1.6.0 // indirect - github.com/sirupsen/logrus v1.9.3 // indirect github.com/tidwall/gjson v1.18.0 // indirect github.com/tidwall/match v1.2.0 // indirect github.com/tidwall/pretty v1.2.1 // indirect @@ -86,7 +87,6 @@ require ( github.com/ugorji/go/codec v1.3.0 // indirect go.etcd.io/bbolt v1.4.3 // indirect go.uber.org/atomic v1.11.0 // indirect - go.uber.org/ratelimit v0.3.1 // indirect golang.org/x/arch v0.21.0 // indirect golang.org/x/crypto v0.42.0 // indirect golang.org/x/lint v0.0.0-20241112194109-818c5a804067 // indirect diff --git a/kills/kill.go b/kills/kill.go index cf89f11..5e4c85f 100644 --- a/kills/kill.go +++ b/kills/kill.go @@ -1,14 +1,15 @@ package kills import ( + "context" "encoding/base64" "fmt" "github.com/gin-gonic/gin" - "gorm.io/gorm" "log" "math/rand" "time" - "toutoukan/init/databaseInit" + "toutoukan/init/redisInit" + "toutoukan/utill/scripts" ) const ( @@ -40,103 +41,103 @@ func (Goods) TableName() string { func (Order) TableName() string { return "oders_list" } - -// 生成25位随机订单编号(string类型) func GenerateOrderID() string { - // 1. 取当前时间戳(精确到毫秒,8位数字) - timestamp := time.Now().Format("20060102150405") // 年月日时分秒(14位) + timestamp := time.Now().Format("20060102150405") - // 2. 生成随机字符串(11位,补足25位) - randomBytes := make([]byte, 8) // 8字节随机数经base64编码后约11字符 + randomBytes := make([]byte, 8) _, err := rand.Read(randomBytes) if err != nil { - // 出错时降级为伪随机数 return fmt.Sprintf("ORD%s%011d", timestamp, time.Now().UnixNano()%100000000000) } - // 3. 拼接前缀+时间戳+随机字符串(确保25位) - randomStr := base64.URLEncoding.EncodeToString(randomBytes)[:11] // 截取前11位 + randomStr := base64.URLEncoding.EncodeToString(randomBytes)[:11] return fmt.Sprintf("ORD%s%s", timestamp, randomStr) } func Userkill(c *gin.Context) { - // 直接使用全局初始化好的 UserDB 连接池 - db := databaseInit.UserDB - if db == nil { - log.Printf("%sUserDB 未初始化,请先调用 InitUserDB()%s\n", colorRed, colorReset) - c.JSON(500, gin.H{"error": "系统数据库未初始化"}) + + result, err := redisInit.RedisClient.Eval(context.Background(), scripts.Luascript_forkill, []string{"stock:10000", "mayiming"}, 1, GenerateOrderID()).Int() + if err != nil { + log.Printf("Redis 脚本执行错误: %v", err) + c.JSON(500, gin.H{"error": "库存操作失败", "detail": err.Error()}) return } - fmt.Printf("%s用户请求处理完成%s\n", colorBlue, colorReset) - - // 定义要查询的商品ID - var targetGID uint = 1 - var goods Goods - - tx := db.Begin() - if tx.Error != nil { - log.Printf("%s开启事务失败: %v%s\n", colorRed, tx.Error, colorReset) - c.JSON(500, gin.H{"body": "事务开启失败"}) - return - } - result := tx.Set("gorm:query_option", "FOR UPDATE").Where("gid = ?", targetGID).First(&goods) - if result.Error != nil { - tx.Rollback() // 失败回滚事务 - log.Printf("%s查询商品失败: %v%s\n", colorRed, result.Error, colorReset) - c.JSON(404, gin.H{"error": "商品不存在"}) - return + if result == 1 { + c.JSON(200, gin.H{"result": "库存扣减成功"}) + } else { + c.JSON(200, gin.H{"result": "库存不足,扣减失败"}) } - // 2. 检查库存是否充足 - if goods.Stock <= 0 { - tx.Rollback() - log.Printf("%s商品库存不足%s\n", colorYellow, colorReset) - c.JSON(400, gin.H{"error": "商品库存不足"}) - return - } - - // 3. 生成订单 - order := Order{ - OrderId: GenerateOrderID(), - TradeTime: time.Now(), - } - if err := tx.Create(&order).Error; err != nil { - tx.Rollback() - log.Printf("%s创建订单失败: %v%s\n", colorRed, err, colorReset) - c.JSON(500, gin.H{"error": "创建订单失败"}) - return - } - - updateResult := tx.Model(&Goods{}). - Where("gid = ? AND stock > 0", targetGID). - Update("stock", gorm.Expr("stock - 1")) - if updateResult.Error != nil { - tx.Rollback() - log.Printf("%s扣减库存失败: %v%s\n", colorRed, updateResult.Error, colorReset) - c.JSON(500, gin.H{"error": "库存更新失败"}) - return - } - - // 5. 检查库存是否实际被扣减(防止并发下库存已被其他事务耗尽) - if updateResult.RowsAffected == 0 { - tx.Rollback() - log.Printf("%s库存扣减失败:并发冲突,库存已不足%s\n", colorRed, colorReset) - c.JSON(400, gin.H{"error": "商品库存不足"}) - return - } - - // 5. 提交事务 - if err := tx.Commit().Error; err != nil { - tx.Rollback() - log.Printf("%s提交事务失败: %v%s\n", colorRed, err, colorReset) - c.JSON(500, gin.H{"error": "系统错误"}) - return - } - - log.Printf("%s订单创建成功,订单ID: %s,剩余库存: %d%s\n", colorGreen, order.OrderId, goods.Stock-1, colorReset) - c.JSON(200, gin.H{ - "message": "下单成功", - "order_id": order.OrderId, - "stock": goods.Stock - 1, - }) } + +//func Userkill(c *gin.Context) { +// // 直接使用全局初始化好的 UserDB 连接池 +// db := databaseInit.UserDB +// if db == nil { +// log.Printf("%sUserDB 未初始化,请先调用 InitUserDB()%s\n", colorRed, colorReset) +// c.JSON(500, gin.H{"error": "系统数据库未初始化"}) +// return +// } +// fmt.Printf("%s用户请求处理完成%s\n", colorBlue, colorReset) +// +// // 定义要查询的商品ID +// var targetGID uint = 1 +// var goods Goods +// +// tx := db.Begin() +// if tx.Error != nil { +// log.Printf("%s开启事务失败: %v%s\n", colorRed, tx.Error, colorReset) +// c.JSON(500, gin.H{"body": "事务开启失败"}) +// return +// } +// result := tx.Set("gorm:query_option", "FOR UPDATE").Where("gid = ?", targetGID).First(&goods) +// if result.Error != nil { +// tx.Rollback() // 失败回滚事务 +// log.Printf("%s查询商品失败: %v%s\n", colorRed, result.Error, colorReset) +// c.JSON(404, gin.H{"error": "商品不存在"}) +// return +// } +// +// // 2. 检查库存是否充足 +// if goods.Stock <= 0 { +// tx.Rollback() +// log.Printf("%s商品库存不足%s\n", colorYellow, colorReset) +// c.JSON(400, gin.H{"error": "商品库存不足"}) +// return +// } +// +// // 3. 生成订单 +// userorder := Order{ +// OrderId: GenerateOrderID(), +// TradeTime: time.Now(), +// } +// if err := tx.Create(&userorder).Error; err != nil { +// tx.Rollback() +// log.Printf("%s创建订单失败: %v%s\n", colorRed, err, colorReset) +// c.JSON(500, gin.H{"error": "创建订单失败"}) +// return +// } +// +// // 4. 扣减库存(库存-1) +// if err := tx.Model(&Goods{}).Where("gid = ?", targetGID).Update("stock", goods.Stock-1).Error; err != nil { +// tx.Rollback() +// log.Printf("%s扣减库存失败: %v%s\n", colorRed, err, colorReset) +// c.JSON(500, gin.H{"error": "库存更新失败"}) +// return +// } +// +// // 5. 提交事务 +// if err := tx.Commit().Error; err != nil { +// tx.Rollback() +// log.Printf("%s提交事务失败: %v%s\n", colorRed, err, colorReset) +// c.JSON(500, gin.H{"error": "系统错误"}) +// return +// } +// +// log.Printf("%s订单创建成功,订单ID: %s,剩余库存: %d%s\n", colorGreen, userorder.OrderId, goods.Stock-1, colorReset) +// c.JSON(200, gin.H{ +// "message": "下单成功", +// "order_id": userorder.OrderId, +// "stock": goods.Stock - 1, +// }) +//} diff --git a/model/usermodel/userOrder/userorder.go b/model/usermodel/userOrder/userorder.go new file mode 100644 index 0000000..49022e4 --- /dev/null +++ b/model/usermodel/userOrder/userorder.go @@ -0,0 +1,6 @@ +package userOrder + +type UserRequest struct { + UserID int `json:"user_id"` + Order string `json:"userorder"` // 对应商品标识:stock10000 ~ stock10004 +} diff --git a/oneKUsers/mian.go b/oneKUsers/mian.go index 23b6a26..005b02e 100644 --- a/oneKUsers/mian.go +++ b/oneKUsers/mian.go @@ -7,120 +7,121 @@ import ( "net/http" "sync" "time" + "toutoukan/model/usermodel/userOrder" ) -// 请求体结构 -type UserRequest struct { - UserID int `json:"user_id"` -} +// 请求体结构(Order字段对应目标商品标识) // 响应体结构(根据实际接口响应调整) type Response struct { Success bool `json:"success"` Message string `json:"message"` + // 可根据实际接口返回补充字段,如订单号、库存状态等 } func main() { - const totalUsers = 1000 // 总用户数 - const batchSize = 130 // 每批发送的请求数 - const interval = 5 * time.Second // 每批请求的时间间隔 - const duration = 20 * time.Second // 总持续时间 - - // 计算总批次数(确保在duration时间内发送完所有请求) - totalBatches := totalUsers / batchSize - if totalUsers%batchSize != 0 { - totalBatches++ - } - - // 检查总耗时是否符合预期 - expectedDuration := time.Duration(totalBatches-1) * interval - if expectedDuration > duration { - fmt.Printf("警告:按当前设置将耗时 %v,超过预期的 %v\n", expectedDuration, duration) - } - + const totalBatches = 5 // 固定发送5批请求 + const batchSize = 150 // 每批固定150个请求 + const interval = 5 * time.Second // 每批请求的时间间隔(可按需调整) var wg sync.WaitGroup - userID := 1 // 用户ID计数器 - // 按批次发送请求 + userID := 1 // 用户ID计数器,从1开始递增(确保每个请求用户ID唯一) + + // 循环发送5批请求,每批对应不同的商品order(stock10000 ~ stock10004) for batch := 1; batch <= totalBatches; batch++ { - // 计算当前批次的请求数量(最后一批可能不足batchSize) - currentBatchSize := batchSize - if userID+currentBatchSize-1 > totalUsers { - currentBatchSize = totalUsers - userID + 1 - } + // 计算当前批次的目标商品order:第1批→stock10000,第2批→stock10001...第5批→stock10004 + targetOrder := fmt.Sprintf("stock1000%d", batch-1) + fmt.Printf("=== 开始第 %d 批请求 ===\n", batch) + fmt.Printf("当前批次目标商品: %s | 请求数量: %d 个 | 开始时间: %v\n", + targetOrder, batchSize, time.Now().Format("15:04:05")) - fmt.Printf("开始第 %d 批请求,共 %d 个,当前时间: %v\n", - batch, currentBatchSize, time.Now().Format("15:04:05")) - - // 发送当前批次的请求 - for i := 0; i < currentBatchSize; i++ { + // 并发发送当前批次的150个请求 + for i := 0; i < batchSize; i++ { wg.Add(1) - go func(uid int) { + // 捕获当前循环的uid和order(避免goroutine闭包引用循环变量问题) + currentUID := userID + currentOrder := targetOrder + go func(uid int, order string) { defer wg.Done() - // 发送POST请求 - result, err := sendPostRequest(uid) + // 发送单个POST请求 + result, err := sendPostRequest(uid, order) if err != nil { - fmt.Printf("用户 %d 请求失败: %v\n", uid, err) + // 请求失败:输出错误日志(包含用户ID和商品标识) + fmt.Printf("❌ 用户 %d (商品%s) 请求失败: %v\n", uid, order, err) return } - // 输出结果(可以根据需要调整输出频率,避免日志过多) - fmt.Printf("用户 %d 响应: %+v\n", uid, result) - }(userID) - userID++ + // 请求成功:输出响应结果(可按需调整日志粒度,避免刷屏) + fmt.Printf("✅ 用户 %d (商品%s) 响应: 成功=%v, 消息=%s\n", + uid, order, result.Success, result.Message) + }(currentUID, currentOrder) + userID++ // 每生成一个请求,用户ID递增1 } - // 最后一批不需要等待 + // 等待当前批次所有请求完成(确保批次内请求全部处理后,再进入下一批) + wg.Wait() + fmt.Printf("=== 第 %d 批请求全部完成 | 完成时间: %v ===\n\n", + batch, time.Now().Format("15:04:05")) + + // 非最后一批请求,等待指定间隔后再发送下一批(避免请求集中压测) if batch < totalBatches { - // 等待指定间隔后再发送下一批 + fmt.Printf("等待 %v 后发送下一批请求...\n\n", interval) time.Sleep(interval) } } - // 等待所有请求完成 - wg.Wait() - fmt.Println("所有请求已完成") + fmt.Println("🎉 所有5批请求已全部完成!") } -// 发送POST请求 -func sendPostRequest(userID int) (*Response, error) { - url := "http://localhost:9096/user/kill" +type UserRequest1 struct { + UserID int `json:"user_id"` + Order string `json:"userorder"` // 对应商品标识:stock10000 ~ stock10004 +} - // 创建请求体 - requestBody := UserRequest{ +// 发送POST请求(接收用户ID和商品order,构造请求体) +func sendPostRequest(userID int, order string) (*Response, error) { + // 目标接口地址(根据实际部署调整) + targetURL := "http://localhost:9096/user/kill" + // 1. 构造请求体(绑定当前用户ID和目标商品order) + requestBody := userOrder.UserRequest{ UserID: userID, + Order: order, } - // 转换为JSON + // 2. 将请求体序列化为JSON格式 jsonBody, err := json.Marshal(requestBody) if err != nil { return nil, fmt.Errorf("JSON序列化失败: %v", err) } - // 发送POST请求 - resp, err := http.Post(url, "application/json", bytes.NewBuffer(jsonBody)) + // 3. 发送POST请求(设置Content-Type为application/json) + resp, err := http.Post( + targetURL, + "application/json", + bytes.NewBuffer(jsonBody), + ) if err != nil { - return nil, fmt.Errorf("请求发送失败: %v", err) + return nil, fmt.Errorf("请求发送失败(网络/连接问题): %v", err) } - defer resp.Body.Close() + defer resp.Body.Close() // 确保响应体关闭,避免资源泄漏 - // 解析响应(无论状态码如何都尝试解析,以便获取详细信息) + // 4. 解析接口响应(无论状态码是否为200,均尝试解析,便于排查问题) var response Response decoder := json.NewDecoder(resp.Body) if err := decoder.Decode(&response); err != nil { - return nil, fmt.Errorf("响应解析失败: %v", err) + return nil, fmt.Errorf("响应解析失败(格式不匹配): %v", err) } - // 检查响应状态码 - // 状态码异常时,返回包含完整响应的错误信息 + // 5. 检查HTTP状态码(非200视为异常,返回详细信息) if resp.StatusCode != http.StatusOK { - // 将完整响应转为JSON字符串,便于查看 - responseJSON, _ := json.MarshalIndent(response, "", " ") + // 将响应内容转为格式化JSON,便于查看完整错误信息 + responseDetail, _ := json.MarshalIndent(response, "", " ") return &response, fmt.Errorf( - "状态码异常: %d, 响应内容: %s", + "HTTP状态码异常: %d | 接口响应详情: %s", resp.StatusCode, - responseJSON, + responseDetail, ) } + // 6. 请求成功,返回解析后的响应 return &response, nil } diff --git a/utill/scripts/killscripts.go b/utill/scripts/killscripts.go new file mode 100644 index 0000000..ecee7e5 --- /dev/null +++ b/utill/scripts/killscripts.go @@ -0,0 +1,15 @@ +package scripts + +var Luascript_forkill = ` + local key = KEYS[1] + local jian = tonumber(ARGV[1]) + local dingdan = ARGV[2] + local nums = tonumber(redis.call("get", KEYS[1]) or "0") + if nums > 0 then + redis.call("DECRBY", KEYS[1], jian) + redis.call("LPUSH", KEYS[2], dingdan) + return 1 + else + return 0 + end + `