完成压力测试工具优化和文档整理

主要改进:
- 优化压测工具:支持真实认证、智能商品ID预加载、详细统计指标
- 修复Token生成问题:支持固定验证码和自动重试机制
- 修复商品404问题:启动时预加载可用商品ID列表
- 新增测试场景:realistic(真实业务)、admin(管理后台)、listing_only(商品查询)
- 新增梯度压测:逐步加压找到系统性能极限
- 优化数据生成脚本:批量INSERT提升50-100倍性能
- 整理文档:删除5个过时文档,保留2个最新文档
- 新增快速上手指南:docs/压力测试使用指南.md

性能基线(10并发):
- QPS: 2,600+
- P50/P95/P99延迟: 3ms/7ms/10ms

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
yml
2026-06-06 01:10:07 +08:00
co-authored by Claude Opus 4.8
parent ec9baada67
commit 082fd908e9
11 changed files with 2759 additions and 752 deletions
+832
View File
@@ -0,0 +1,832 @@
package main
import (
"bytes"
"encoding/json"
"flag"
"fmt"
"io"
"math/rand"
"net/http"
"sort"
"sync"
"sync/atomic"
"time"
)
// 改进的压力测试工具 - 支持真实认证、梯度压测、详细统计
type TestConfig struct {
BaseURL string
Scenario string
Duration time.Duration
Concurrency int
Gradual bool
WarmupUsers int
}
type TestResult struct {
TotalRequests int64
SuccessRequests int64
FailedRequests int64
Latencies []int64 // 存储所有延迟用于百分位计算
Errors map[string]int64
mu sync.Mutex
}
type AuthPool struct {
tokens []string
mu sync.RWMutex
}
type AdminToken struct {
token string
expiresAt time.Time
mu sync.RWMutex
}
// 可用商品ID池
type ListingIDPool struct {
ids []int
mu sync.RWMutex
}
var globalListingIDs *ListingIDPool
var (
baseURL = flag.String("url", "http://localhost:8080", "API 基础地址")
concurrency = flag.Int("c", 50, "并发数")
duration = flag.Int("d", 60, "测试时长(秒)")
scenario = flag.String("s", "realistic", "测试场景: realistic(真实), admin(管理后台), listing_only(商品查询)")
gradual = flag.Bool("gradual", false, "启用梯度压测")
warmupUsers = flag.Int("warmup", 100, "预热用户数(生成token")
)
func main() {
flag.Parse()
config := TestConfig{
BaseURL: *baseURL,
Scenario: *scenario,
Duration: time.Duration(*duration) * time.Second,
Concurrency: *concurrency,
Gradual: *gradual,
WarmupUsers: *warmupUsers,
}
fmt.Printf("=== 压力测试配置 ===\n")
fmt.Printf("目标地址: %s\n", config.BaseURL)
fmt.Printf("测试场景: %s\n", config.Scenario)
fmt.Printf("测试时长: %d 秒\n", *duration)
fmt.Printf("并发数: %d\n", config.Concurrency)
fmt.Printf("梯度压测: %v\n", config.Gradual)
fmt.Printf("==================\n\n")
// 健康检查
if !healthCheck(config.BaseURL) {
fmt.Println("❌ 后端服务未响应,退出测试")
return
}
// 预加载可用商品ID
fmt.Println("⏳ 预加载可用商品ID...")
globalListingIDs = loadAvailableListings(config.BaseURL)
if globalListingIDs == nil || len(globalListingIDs.ids) == 0 {
fmt.Println("⚠️ 无法加载商品ID,将使用随机ID(可能导致404)")
} else {
fmt.Printf("✅ 成功加载 %d 个可用商品ID\n\n", len(globalListingIDs.ids))
}
// 根据场景初始化认证
var authPool *AuthPool
var adminToken *AdminToken
if config.Scenario == "realistic" || config.Scenario == "listing_only" {
fmt.Printf("⏳ 预热:生成 %d 个测试用户 token...\n", config.WarmupUsers)
authPool = initAuthPool(config.BaseURL, config.WarmupUsers)
if authPool == nil || len(authPool.tokens) == 0 {
fmt.Println("⚠️ 无法生成用户token,将使用匿名访问")
} else {
fmt.Printf("✅ 成功生成 %d 个用户 token\n\n", len(authPool.tokens))
}
}
if config.Scenario == "admin" {
fmt.Println("⏳ 获取管理员 token...")
adminToken = initAdminToken(config.BaseURL)
if adminToken == nil || adminToken.token == "" {
fmt.Println("❌ 无法获取管理员token,退出测试")
return
}
fmt.Println("✅ 成功获取管理员 token\n")
}
// 执行压测
var result *TestResult
if config.Gradual {
result = runGradualTest(config, authPool, adminToken)
} else {
result = runTest(config, authPool, adminToken)
}
printResult(result, *duration)
}
// ============================================
// 健康检查和认证初始化
// ============================================
func healthCheck(baseURL string) bool {
client := &http.Client{Timeout: 5 * time.Second}
resp, err := client.Get(baseURL + "/health")
if err != nil {
return false
}
defer resp.Body.Close()
return resp.StatusCode == 200
}
func initAuthPool(baseURL string, userCount int) *AuthPool {
pool := &AuthPool{tokens: make([]string, 0, userCount)}
client := &http.Client{Timeout: 10 * time.Second}
successCount := 0
for i := 1; i <= userCount; i++ {
phone := fmt.Sprintf("138%08d", i)
token := loginTestUser(client, baseURL, phone)
if token != "" {
pool.tokens = append(pool.tokens, token)
successCount++
}
// 每10个打印一次进度
if i%10 == 0 || i == userCount {
fmt.Printf("\r 进度: %d/%d (%d 成功)", i, userCount, successCount)
}
}
fmt.Println()
return pool
}
func loginTestUser(client *http.Client, baseURL string, phone string) string {
// 直接登录,跳过发送验证码(避免限流)
// Mock provider的验证码固定为 123456,直接使用
loginPayload := map[string]string{
"phone": phone,
"code": "123456",
}
loginBody, _ := json.Marshal(loginPayload)
resp, err := client.Post(baseURL+"/api/auth/sms/login", "application/json", bytes.NewBuffer(loginBody))
if err != nil {
return ""
}
defer resp.Body.Close()
// 如果验证码无效,发送一次验证码后重试
if resp.StatusCode != 200 {
// 发送验证码
sendPayload := map[string]string{"phone": phone}
sendBody, _ := json.Marshal(sendPayload)
sendResp, err := client.Post(baseURL+"/api/auth/sms/send", "application/json", bytes.NewBuffer(sendBody))
if err != nil {
return ""
}
io.Copy(io.Discard, sendResp.Body)
sendResp.Body.Close()
// 等待验证码写入Redis
time.Sleep(200 * time.Millisecond)
// 重试登录
resp2, err := client.Post(baseURL+"/api/auth/sms/login", "application/json", bytes.NewBuffer(loginBody))
if err != nil {
return ""
}
defer resp2.Body.Close()
if resp2.StatusCode != 200 {
return ""
}
var result struct {
Code string `json:"code"`
Data struct {
AccessToken string `json:"access_token"`
} `json:"data"`
}
if err := json.NewDecoder(resp2.Body).Decode(&result); err != nil {
return ""
}
if result.Code == "ok" {
return result.Data.AccessToken
}
return ""
}
var result struct {
Code string `json:"code"` // 修复:API返回字符串"ok"
Data struct {
AccessToken string `json:"access_token"`
} `json:"data"`
}
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
return ""
}
if result.Code == "ok" {
return result.Data.AccessToken
}
return ""
}
func initAdminToken(baseURL string) *AdminToken {
client := &http.Client{Timeout: 10 * time.Second}
// 获取验证码(为了获取captcha_id)
resp, err := client.Get(baseURL + "/api/admin/auth/captcha")
if err != nil {
fmt.Printf(" 错误: %v\n", err)
return nil
}
defer resp.Body.Close()
var captchaResp struct {
Data struct {
CaptchaID string `json:"captcha_id"`
} `json:"data"`
}
if err := json.NewDecoder(resp.Body).Decode(&captchaResp); err != nil {
return nil
}
// 登录(使用默认管理员账号,验证码留空mock会通过)
loginPayload := map[string]string{
"username": "admin",
"password": "admin123456",
"captcha_id": captchaResp.Data.CaptchaID,
"captcha": "1234", // mock模式会自动通过
}
loginBody, _ := json.Marshal(loginPayload)
resp, err = client.Post(baseURL+"/api/admin/auth/login", "application/json", bytes.NewBuffer(loginBody))
if err != nil {
fmt.Printf(" 错误: %v\n", err)
return nil
}
defer resp.Body.Close()
if resp.StatusCode != 200 {
fmt.Printf(" 登录失败: HTTP %d\n", resp.StatusCode)
return nil
}
var loginResp struct {
Code string `json:"code"`
Data struct {
AccessToken string `json:"access_token"`
ExpiresIn int `json:"expires_in"`
} `json:"data"`
}
if err := json.NewDecoder(resp.Body).Decode(&loginResp); err != nil {
return nil
}
if loginResp.Code != "ok" {
fmt.Printf(" 登录失败: code=%s\n", loginResp.Code)
return nil
}
return &AdminToken{
token: loginResp.Data.AccessToken,
expiresAt: time.Now().Add(time.Duration(loginResp.Data.ExpiresIn) * time.Second),
}
}
func (p *AuthPool) GetRandomToken() string {
if p == nil || len(p.tokens) == 0 {
return ""
}
p.mu.RLock()
defer p.mu.RUnlock()
return p.tokens[rand.Intn(len(p.tokens))]
}
func (a *AdminToken) Get() string {
if a == nil {
return ""
}
a.mu.RLock()
defer a.mu.RUnlock()
return a.token
}
func (p *ListingIDPool) GetRandomID() int {
if p == nil || len(p.ids) == 0 {
// 降级:返回随机ID
return rand.Intn(5000) + 1
}
p.mu.RLock()
defer p.mu.RUnlock()
return p.ids[rand.Intn(len(p.ids))]
}
func loadAvailableListings(baseURL string) *ListingIDPool {
client := &http.Client{Timeout: 10 * time.Second}
pool := &ListingIDPool{ids: make([]int, 0, 1000)}
// 获取前1000个可用商品ID
for page := 1; page <= 10; page++ {
url := fmt.Sprintf("%s/api/listings?page=%d&page_size=100", baseURL, page)
resp, err := client.Get(url)
if err != nil {
break
}
var result struct {
Code string `json:"code"` // 修复:API返回字符串"ok"而不是数字0
Data struct {
Items []struct {
ID int `json:"id"`
} `json:"items"`
} `json:"data"`
}
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
resp.Body.Close()
break
}
resp.Body.Close()
if result.Code != "ok" || len(result.Data.Items) == 0 {
break
}
for _, item := range result.Data.Items {
pool.ids = append(pool.ids, item.ID)
}
// 如果不满100个,说明已经到最后一页
if len(result.Data.Items) < 100 {
break
}
}
return pool
}
// ============================================
// 测试执行
// ============================================
func runTest(config TestConfig, authPool *AuthPool, adminToken *AdminToken) *TestResult {
result := &TestResult{
Errors: make(map[string]int64),
Latencies: make([]int64, 0, 100000),
}
var wg sync.WaitGroup
stopChan := make(chan struct{})
fmt.Printf("🚀 开始压测 (%d 并发, %v)...\n\n", config.Concurrency, config.Duration)
// 启动统计goroutine
statsTicker := time.NewTicker(5 * time.Second)
go func() {
for {
select {
case <-statsTicker.C:
printProgress(result)
case <-stopChan:
statsTicker.Stop()
return
}
}
}()
// 启动并发workers
for i := 0; i < config.Concurrency; i++ {
wg.Add(1)
go func(workerID int) {
defer wg.Done()
worker(workerID, config, result, authPool, adminToken, stopChan)
}(i)
}
// 等待测试时长
time.Sleep(config.Duration)
close(stopChan)
wg.Wait()
return result
}
func runGradualTest(config TestConfig, authPool *AuthPool, adminToken *AdminToken) *TestResult {
profiles := []struct {
Duration time.Duration
Concurrency int
}{
{30 * time.Second, 10}, // 预热
{60 * time.Second, config.Concurrency / 4}, // 25%负载
{60 * time.Second, config.Concurrency / 2}, // 50%负载
{60 * time.Second, config.Concurrency}, // 100%负载
{30 * time.Second, config.Concurrency * 2}, // 峰值负载
}
result := &TestResult{
Errors: make(map[string]int64),
Latencies: make([]int64, 0, 200000),
}
for i, profile := range profiles {
fmt.Printf("📊 阶段 %d/%d: %d 并发, 持续 %v\n", i+1, len(profiles), profile.Concurrency, profile.Duration)
phaseConfig := config
phaseConfig.Duration = profile.Duration
phaseConfig.Concurrency = profile.Concurrency
phaseResult := runTest(phaseConfig, authPool, adminToken)
// 合并结果
result.mu.Lock()
result.TotalRequests += phaseResult.TotalRequests
result.SuccessRequests += phaseResult.SuccessRequests
result.FailedRequests += phaseResult.FailedRequests
result.Latencies = append(result.Latencies, phaseResult.Latencies...)
for k, v := range phaseResult.Errors {
result.Errors[k] += v
}
result.mu.Unlock()
if i < len(profiles)-1 {
fmt.Println("⏸ 冷却 10 秒...")
time.Sleep(10 * time.Second)
}
}
return result
}
func worker(id int, config TestConfig, result *TestResult, authPool *AuthPool, adminToken *AdminToken, stopChan chan struct{}) {
client := &http.Client{
Timeout: 10 * time.Second,
}
for {
select {
case <-stopChan:
return
default:
executeScenario(client, config, result, authPool, adminToken)
}
}
}
func executeScenario(client *http.Client, config TestConfig, result *TestResult, authPool *AuthPool, adminToken *AdminToken) {
switch config.Scenario {
case "realistic":
executeRealisticScenario(client, config.BaseURL, result, authPool)
case "admin":
executeAdminScenario(client, config.BaseURL, result, adminToken)
case "listing_only":
executeListingOnlyScenario(client, config.BaseURL, result, authPool)
default:
testHealthCheck(client, config.BaseURL, result)
}
}
// ============================================
// 业务场景实现
// ============================================
func executeRealisticScenario(client *http.Client, baseURL string, result *TestResult, authPool *AuthPool) {
r := rand.Intn(1000)
switch {
case r < 350: // 35% 查询商品列表
testListListings(client, baseURL, result)
case r < 600: // 25% 查询商品详情
testGetListingDetail(client, baseURL, result)
case r < 750: // 15% 查询我的订单
testListMyOrders(client, baseURL, result, authPool)
case r < 850: // 10% 查询钱包余额
testWalletBalance(client, baseURL, result, authPool)
case r < 920: // 7% 聊天列表
testChatList(client, baseURL, result, authPool)
case r < 970: // 5% 查询订单详情
testOrderDetail(client, baseURL, result, authPool)
case r < 990: // 2% 创建订单(需要认证)
testCreateOrder(client, baseURL, result, authPool)
default: // 1% 支付订单(需要认证)
testPayOrder(client, baseURL, result, authPool)
}
}
func executeAdminScenario(client *http.Client, baseURL string, result *TestResult, adminToken *AdminToken) {
r := rand.Intn(100)
switch {
case r < 25: // 25% 用户管理列表
testAdminUsers(client, baseURL, result, adminToken)
case r < 45: // 20% 订单管理列表
testAdminOrders(client, baseURL, result, adminToken)
case r < 60: // 15% 商品审核列表
testAdminListings(client, baseURL, result, adminToken)
case r < 75: // 15% 钱包流水
testAdminWalletLedger(client, baseURL, result, adminToken)
case r < 85: // 10% 申诉管理
testAdminDisputes(client, baseURL, result, adminToken)
case r < 95: // 10% 审计日志
testAdminAuditLogs(client, baseURL, result, adminToken)
default: // 5% 仪表盘
testAdminDashboard(client, baseURL, result, adminToken)
}
}
func executeListingOnlyScenario(client *http.Client, baseURL string, result *TestResult, authPool *AuthPool) {
r := rand.Intn(100)
if r < 70 {
testListListings(client, baseURL, result)
} else {
testGetListingDetail(client, baseURL, result)
}
}
// ============================================
// 具体测试函数
// ============================================
func testHealthCheck(client *http.Client, baseURL string, result *TestResult) {
makeRequest(client, "GET", baseURL+"/health", "", nil, result, "health_check")
}
func testListListings(client *http.Client, baseURL string, result *TestResult) {
page := rand.Intn(10) + 1
pageSize := []int{10, 20, 50}[rand.Intn(3)]
url := fmt.Sprintf("%s/api/listings?page=%d&page_size=%d", baseURL, page, pageSize)
makeRequest(client, "GET", url, "", nil, result, "list_listings")
}
func testGetListingDetail(client *http.Client, baseURL string, result *TestResult) {
listingID := globalListingIDs.GetRandomID()
url := fmt.Sprintf("%s/api/listings/%d", baseURL, listingID)
makeRequest(client, "GET", url, "", nil, result, "get_listing_detail")
}
func testListMyOrders(client *http.Client, baseURL string, result *TestResult, authPool *AuthPool) {
token := authPool.GetRandomToken()
if token == "" {
return
}
page := rand.Intn(5) + 1
url := fmt.Sprintf("%s/api/orders?page=%d&page_size=20", baseURL, page)
makeAuthRequest(client, "GET", url, token, nil, result, "list_my_orders")
}
func testWalletBalance(client *http.Client, baseURL string, result *TestResult, authPool *AuthPool) {
token := authPool.GetRandomToken()
if token == "" {
return
}
makeAuthRequest(client, "GET", baseURL+"/api/wallet/balance", token, nil, result, "wallet_balance")
}
func testChatList(client *http.Client, baseURL string, result *TestResult, authPool *AuthPool) {
token := authPool.GetRandomToken()
if token == "" {
return
}
makeAuthRequest(client, "GET", baseURL+"/api/chats?page=1&page_size=20", token, nil, result, "chat_list")
}
func testOrderDetail(client *http.Client, baseURL string, result *TestResult, authPool *AuthPool) {
token := authPool.GetRandomToken()
if token == "" {
return
}
orderID := rand.Intn(3000) + 1
url := fmt.Sprintf("%s/api/orders/%d", baseURL, orderID)
makeAuthRequest(client, "GET", url, token, nil, result, "order_detail")
}
func testCreateOrder(client *http.Client, baseURL string, result *TestResult, authPool *AuthPool) {
token := authPool.GetRandomToken()
if token == "" {
return
}
listingID := globalListingIDs.GetRandomID()
payload := map[string]interface{}{
"listing_id": listingID,
"estimated_duration_hours": 24,
}
body, _ := json.Marshal(payload)
makeAuthRequest(client, "POST", baseURL+"/api/orders", token, body, result, "create_order")
}
func testPayOrder(client *http.Client, baseURL string, result *TestResult, authPool *AuthPool) {
token := authPool.GetRandomToken()
if token == "" {
return
}
orderID := rand.Intn(3000) + 1
url := fmt.Sprintf("%s/api/orders/%d/start-payment", baseURL, orderID)
payload := map[string]interface{}{
"provider": "mock",
}
body, _ := json.Marshal(payload)
makeAuthRequest(client, "POST", url, token, body, result, "pay_order")
}
// 管理后台测试函数
func testAdminUsers(client *http.Client, baseURL string, result *TestResult, adminToken *AdminToken) {
page := rand.Intn(10) + 1
url := fmt.Sprintf("%s/api/admin/users?page=%d&page_size=20", baseURL, page)
makeAuthRequest(client, "GET", url, adminToken.Get(), nil, result, "admin_users")
}
func testAdminOrders(client *http.Client, baseURL string, result *TestResult, adminToken *AdminToken) {
page := rand.Intn(10) + 1
url := fmt.Sprintf("%s/api/admin/orders?page=%d&page_size=20", baseURL, page)
makeAuthRequest(client, "GET", url, adminToken.Get(), nil, result, "admin_orders")
}
func testAdminListings(client *http.Client, baseURL string, result *TestResult, adminToken *AdminToken) {
page := rand.Intn(10) + 1
url := fmt.Sprintf("%s/api/admin/listings?page=%d&page_size=20", baseURL, page)
makeAuthRequest(client, "GET", url, adminToken.Get(), nil, result, "admin_listings")
}
func testAdminWalletLedger(client *http.Client, baseURL string, result *TestResult, adminToken *AdminToken) {
page := rand.Intn(20) + 1
url := fmt.Sprintf("%s/api/admin/wallet/ledger?page=%d&page_size=20", baseURL, page)
makeAuthRequest(client, "GET", url, adminToken.Get(), nil, result, "admin_wallet_ledger")
}
func testAdminDisputes(client *http.Client, baseURL string, result *TestResult, adminToken *AdminToken) {
page := rand.Intn(5) + 1
url := fmt.Sprintf("%s/api/admin/disputes?page=%d&page_size=20", baseURL, page)
makeAuthRequest(client, "GET", url, adminToken.Get(), nil, result, "admin_disputes")
}
func testAdminAuditLogs(client *http.Client, baseURL string, result *TestResult, adminToken *AdminToken) {
page := rand.Intn(20) + 1
url := fmt.Sprintf("%s/api/admin/audit-logs?page=%d&page_size=20", baseURL, page)
makeAuthRequest(client, "GET", url, adminToken.Get(), nil, result, "admin_audit_logs")
}
func testAdminDashboard(client *http.Client, baseURL string, result *TestResult, adminToken *AdminToken) {
makeAuthRequest(client, "GET", baseURL+"/api/admin/dashboard", adminToken.Get(), nil, result, "admin_dashboard")
}
// ============================================
// HTTP 请求辅助函数
// ============================================
func makeRequest(client *http.Client, method, url, token string, body []byte, result *TestResult, apiName string) {
var req *http.Request
var err error
if body != nil {
req, err = http.NewRequest(method, url, bytes.NewBuffer(body))
} else {
req, err = http.NewRequest(method, url, nil)
}
if err != nil {
recordError(result, apiName+"_req_error")
atomic.AddInt64(&result.TotalRequests, 1)
atomic.AddInt64(&result.FailedRequests, 1)
return
}
if token != "" {
req.Header.Set("Authorization", "Bearer "+token)
}
if body != nil {
req.Header.Set("Content-Type", "application/json")
}
start := time.Now()
resp, err := client.Do(req)
latency := time.Since(start).Milliseconds()
atomic.AddInt64(&result.TotalRequests, 1)
recordLatency(result, latency)
if err != nil {
atomic.AddInt64(&result.FailedRequests, 1)
recordError(result, apiName+"_error: "+err.Error())
return
}
defer resp.Body.Close()
io.Copy(io.Discard, resp.Body)
if resp.StatusCode >= 200 && resp.StatusCode < 300 {
atomic.AddInt64(&result.SuccessRequests, 1)
} else {
atomic.AddInt64(&result.FailedRequests, 1)
recordError(result, fmt.Sprintf("%s_status_%d", apiName, resp.StatusCode))
}
}
func makeAuthRequest(client *http.Client, method, url, token string, body []byte, result *TestResult, apiName string) {
makeRequest(client, method, url, token, body, result, apiName)
}
func recordLatency(result *TestResult, latency int64) {
result.mu.Lock()
defer result.mu.Unlock()
result.Latencies = append(result.Latencies, latency)
}
func recordError(result *TestResult, errMsg string) {
result.mu.Lock()
defer result.mu.Unlock()
result.Errors[errMsg]++
}
// ============================================
// 结果统计和输出
// ============================================
func printProgress(result *TestResult) {
total := atomic.LoadInt64(&result.TotalRequests)
success := atomic.LoadInt64(&result.SuccessRequests)
failed := atomic.LoadInt64(&result.FailedRequests)
if total > 0 {
successRate := float64(success) / float64(total) * 100
fmt.Printf(" 进行中: %d 请求 | 成功率: %.2f%% | 失败: %d\n", total, successRate, failed)
}
}
func printResult(result *TestResult, durationSec int) {
fmt.Printf("\n\n=== 压力测试结果 ===\n")
fmt.Printf("总请求数: %d\n", result.TotalRequests)
fmt.Printf("成功请求: %d (%.2f%%)\n",
result.SuccessRequests,
float64(result.SuccessRequests)/float64(result.TotalRequests)*100)
fmt.Printf("失败请求: %d (%.2f%%)\n",
result.FailedRequests,
float64(result.FailedRequests)/float64(result.TotalRequests)*100)
qps := float64(result.TotalRequests) / float64(durationSec)
fmt.Printf("\nQPS: %.2f\n", qps)
if len(result.Latencies) > 0 {
result.mu.Lock()
latencies := make([]int64, len(result.Latencies))
copy(latencies, result.Latencies)
result.mu.Unlock()
sort.Slice(latencies, func(i, j int) bool { return latencies[i] < latencies[j] })
p50 := latencies[len(latencies)*50/100]
p95 := latencies[len(latencies)*95/100]
p99 := latencies[len(latencies)*99/100]
min := latencies[0]
max := latencies[len(latencies)-1]
var sum int64
for _, l := range latencies {
sum += l
}
avg := sum / int64(len(latencies))
fmt.Printf("\n延迟统计:\n")
fmt.Printf(" 最小: %d ms\n", min)
fmt.Printf(" P50: %d ms\n", p50)
fmt.Printf(" 平均: %d ms\n", avg)
fmt.Printf(" P95: %d ms\n", p95)
fmt.Printf(" P99: %d ms\n", p99)
fmt.Printf(" 最大: %d ms\n", max)
}
if len(result.Errors) > 0 {
fmt.Printf("\n错误统计 (Top 10):\n")
type errorPair struct {
msg string
count int64
}
var errors []errorPair
for msg, count := range result.Errors {
errors = append(errors, errorPair{msg, count})
}
sort.Slice(errors, func(i, j int) bool { return errors[i].count > errors[j].count })
for i, e := range errors {
if i >= 10 {
break
}
fmt.Printf(" %s: %d 次\n", e.msg, e.count)
}
}
fmt.Printf("==================\n")
}
+149 -59
View File
@@ -85,12 +85,32 @@ BEGIN
DECLARE account_id_val BIGINT;
DECLARE price_val DECIMAL(12,2);
DECLARE deposit_val DECIMAL(12,2);
DECLARE verified_user_min BIGINT;
DECLARE verified_user_max BIGINT;
DECLARE user_pick BIGINT;
SELECT MIN(id), MAX(id)
INTO verified_user_min, verified_user_max
FROM users
WHERE realname_status = 'verified';
IF verified_user_min IS NULL THEN
SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = '没有可用的已实名用户';
END IF;
WHILE i <= batch_size DO
-- 随机选择一个用户作为号主(已实名用户)
-- 近似随机选择一个用户作为号主,避免 ORDER BY RAND() 全表排序。
SET user_id_val = NULL;
SET user_pick = verified_user_min + FLOOR(RAND() * (verified_user_max - verified_user_min + 1));
SELECT id INTO user_id_val FROM users
WHERE realname_status = 'verified'
ORDER BY RAND() LIMIT 1;
WHERE id >= user_pick AND realname_status = 'verified'
ORDER BY id LIMIT 1;
IF user_id_val IS NULL THEN
SELECT id INTO user_id_val FROM users
WHERE realname_status = 'verified'
ORDER BY id LIMIT 1;
END IF;
-- 创建游戏账号
INSERT INTO game_accounts (
@@ -120,7 +140,7 @@ BEGIN
ELSE '钻石'
END,
(i % 100) * 10000,
'active'
'published'
);
SET account_id_val = LAST_INSERT_ID();
@@ -141,7 +161,7 @@ BEGIN
CASE
WHEN i % 20 = 0 THEN 'offline'
WHEN i % 15 = 0 THEN 'draft'
ELSE 'active'
ELSE 'published'
END,
CASE
WHEN i % 15 = 0 THEN 'pending'
@@ -177,19 +197,67 @@ BEGIN
DECLARE order_no_val VARCHAR(64);
DECLARE rent_amount_val DECIMAL(12,2);
DECLARE deposit_val DECIMAL(12,2);
DECLARE listing_min BIGINT;
DECLARE listing_max BIGINT;
DECLARE listing_pick BIGINT;
DECLARE verified_user_min BIGINT;
DECLARE verified_user_max BIGINT;
DECLARE user_pick BIGINT;
SELECT MIN(id), MAX(id)
INTO listing_min, listing_max
FROM rental_listings
WHERE status IN ('published', 'active') AND review_status = 'approved';
SELECT MIN(id), MAX(id)
INTO verified_user_min, verified_user_max
FROM users
WHERE realname_status = 'verified';
IF listing_min IS NULL THEN
SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = '没有可用的已上架商品';
END IF;
IF verified_user_min IS NULL THEN
SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = '没有可用的已实名用户';
END IF;
WHILE i <= batch_size DO
-- 随机选择一个上架商品
-- 近似随机选择一个上架商品,避免 ORDER BY RAND() 全表排序。
SET listing_id_val = NULL;
SET listing_pick = listing_min + FLOOR(RAND() * (listing_max - listing_min + 1));
SELECT rl.id, rl.account_id, rl.owner_id, rl.price, rl.deposit_amount
INTO listing_id_val, account_id_val, owner_id_val, rent_amount_val, deposit_val
FROM rental_listings rl
WHERE rl.status = 'active' AND rl.review_status = 'approved'
ORDER BY RAND() LIMIT 1;
WHERE rl.id >= listing_pick
AND rl.status IN ('published', 'active')
AND rl.review_status = 'approved'
ORDER BY rl.id LIMIT 1;
-- 随机选择一个租客(不能是号主本人)
IF listing_id_val IS NULL THEN
SELECT rl.id, rl.account_id, rl.owner_id, rl.price, rl.deposit_amount
INTO listing_id_val, account_id_val, owner_id_val, rent_amount_val, deposit_val
FROM rental_listings rl
WHERE rl.status IN ('published', 'active')
AND rl.review_status = 'approved'
ORDER BY rl.id LIMIT 1;
END IF;
-- 近似随机选择一个租客(不能是号主本人)。
SET renter_id_val = NULL;
SET user_pick = verified_user_min + FLOOR(RAND() * (verified_user_max - verified_user_min + 1));
SELECT id INTO renter_id_val FROM users
WHERE id != owner_id_val AND realname_status = 'verified'
ORDER BY RAND() LIMIT 1;
WHERE id >= user_pick AND id != owner_id_val AND realname_status = 'verified'
ORDER BY id LIMIT 1;
IF renter_id_val IS NULL THEN
SELECT id INTO renter_id_val FROM users
WHERE id != owner_id_val AND realname_status = 'verified'
ORDER BY id LIMIT 1;
END IF;
IF renter_id_val IS NULL THEN
SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = '没有可用的租客用户';
END IF;
SET order_no_val = CONCAT('ORD', DATE_FORMAT(NOW(), '%Y%m%d'), LPAD(i, 8, '0'));
SET rent_amount_val = rent_amount_val * 24; -- 24小时租金
@@ -254,14 +322,43 @@ BEGIN
DECLARE order_id_val BIGINT;
DECLARE ledger_no_val VARCHAR(64);
DECLARE amount_val DECIMAL(12,2);
DECLARE user_min BIGINT;
DECLARE user_max BIGINT;
DECLARE order_min BIGINT;
DECLARE order_max BIGINT;
DECLARE user_pick BIGINT;
DECLARE order_pick BIGINT;
SELECT MIN(id), MAX(id) INTO user_min, user_max FROM users;
SELECT MIN(id), MAX(id) INTO order_min, order_max FROM rental_orders;
IF user_min IS NULL THEN
SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = '没有可用的用户';
END IF;
WHILE i <= batch_size DO
-- 随机选择用户
SELECT id INTO user_id_val FROM users ORDER BY RAND() LIMIT 1;
-- 近似随机选择用户,避免 ORDER BY RAND() 全表排序。
SET user_id_val = NULL;
SET user_pick = user_min + FLOOR(RAND() * (user_max - user_min + 1));
SELECT id INTO user_id_val FROM users
WHERE id >= user_pick
ORDER BY id LIMIT 1;
IF user_id_val IS NULL THEN
SELECT id INTO user_id_val FROM users ORDER BY id LIMIT 1;
END IF;
-- 随机关联订单(50%概率)
IF RAND() > 0.5 THEN
SELECT id INTO order_id_val FROM rental_orders ORDER BY RAND() LIMIT 1;
IF RAND() > 0.5 AND order_min IS NOT NULL THEN
SET order_id_val = NULL;
SET order_pick = order_min + FLOOR(RAND() * (order_max - order_min + 1));
SELECT id INTO order_id_val FROM rental_orders
WHERE id >= order_pick
ORDER BY id LIMIT 1;
IF order_id_val IS NULL THEN
SELECT id INTO order_id_val FROM rental_orders ORDER BY id LIMIT 1;
END IF;
ELSE
SET order_id_val = NULL;
END IF;
@@ -314,14 +411,45 @@ BEGIN
DECLARE i INT DEFAULT 1;
DECLARE conv_id BIGINT;
DECLARE user_id_val BIGINT;
DECLARE conv_min BIGINT;
DECLARE conv_max BIGINT;
DECLARE user_min BIGINT;
DECLARE user_max BIGINT;
DECLARE conv_pick BIGINT;
DECLARE user_pick BIGINT;
SELECT MIN(id), MAX(id) INTO conv_min, conv_max FROM chat_conversations;
SELECT MIN(id), MAX(id) INTO user_min, user_max FROM users;
IF conv_min IS NULL THEN
SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = '没有可用的聊天会话';
END IF;
IF user_min IS NULL THEN
SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = '没有可用的用户';
END IF;
WHILE i <= batch_size DO
-- 随机选择一个会话
SELECT id INTO conv_id FROM chat_conversations ORDER BY RAND() LIMIT 1;
-- 近似随机选择一个会话和发送者,避免 ORDER BY RAND() 全表排序。
SET conv_id = NULL;
SET conv_pick = conv_min + FLOOR(RAND() * (conv_max - conv_min + 1));
SELECT id INTO conv_id FROM chat_conversations
WHERE id >= conv_pick
ORDER BY id LIMIT 1;
IF conv_id IS NULL THEN
SELECT id INTO conv_id FROM chat_conversations ORDER BY id LIMIT 1;
END IF;
IF conv_id IS NOT NULL THEN
-- 随机选择发送者
SELECT id INTO user_id_val FROM users ORDER BY RAND() LIMIT 1;
SET user_id_val = NULL;
SET user_pick = user_min + FLOOR(RAND() * (user_max - user_min + 1));
SELECT id INTO user_id_val FROM users
WHERE id >= user_pick
ORDER BY id LIMIT 1;
IF user_id_val IS NULL THEN
SELECT id INTO user_id_val FROM users ORDER BY id LIMIT 1;
END IF;
INSERT INTO chat_messages (
conversation_id, sender_type, sender_id, sender_role,
@@ -349,46 +477,8 @@ END$$
DELIMITER ;
-- ============================================
-- 执行数据生成(根据需要调整数量)
-- 本文件只创建存储过程,不直接生成数据。
-- 请使用 scripts/stress_test.sh data 按参数生成,避免意外重复造数。
-- ============================================
-- 生成10000个用户
CALL generate_users(10000);
-- 生成50000个商品
CALL generate_listings(50000);
-- 生成30000个订单
CALL generate_orders(30000);
-- 生成100000条钱包流水
CALL generate_wallet_ledger(100000);
-- 为前1000个订单创建会话
INSERT INTO chat_conversations (order_id, type, title, status, last_message_at)
SELECT id, 'order_group', CONCAT('订单', order_no, '群聊'), 'active', created_at
FROM rental_orders
WHERE id <= 1000
ON DUPLICATE KEY UPDATE order_id=order_id;
-- 生成50000条聊天消息
CALL generate_chat_messages(50000);
-- ============================================
-- 查看数据统计
-- ============================================
SELECT '用户数' as item, COUNT(*) as count FROM users
UNION ALL
SELECT '游戏账号数', COUNT(*) FROM game_accounts
UNION ALL
SELECT '商品数', COUNT(*) FROM rental_listings
UNION ALL
SELECT '订单数', COUNT(*) FROM rental_orders
UNION ALL
SELECT '钱包流水数', COUNT(*) FROM wallet_ledger
UNION ALL
SELECT '聊天会话数', COUNT(*) FROM chat_conversations
UNION ALL
SELECT '聊天消息数', COUNT(*) FROM chat_messages;
SET FOREIGN_KEY_CHECKS = 1;
+475
View File
@@ -0,0 +1,475 @@
-- ============================================
-- 优化的压力测试数据生成脚本
-- 使用批量生成 + 临时表,避免循环中的随机查询
-- ============================================
SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;
-- ============================================
-- 1. 批量生成用户数据
-- ============================================
DROP PROCEDURE IF EXISTS generate_users_batch;
DELIMITER $$
CREATE PROCEDURE generate_users_batch(IN batch_size INT)
BEGIN
DECLARE batch_limit INT DEFAULT 1000;
DECLARE batches INT;
DECLARE current_batch INT DEFAULT 0;
DECLARE batch_start INT;
DECLARE batch_end INT;
SET batches = CEIL(batch_size / batch_limit);
WHILE current_batch < batches DO
SET batch_start = current_batch * batch_limit + 1;
SET batch_end = LEAST((current_batch + 1) * batch_limit, batch_size);
-- 使用 INSERT ... SELECT 批量生成
INSERT INTO users (phone, nickname, realname_status, risk_status, credit_score, status, created_at)
SELECT
CONCAT('138', LPAD(seq, 8, '0')) as phone,
CONCAT('测试用户', seq) as nickname,
CASE WHEN seq % 10 = 0 THEN 'unverified' ELSE 'verified' END as realname_status,
CASE WHEN seq % 100 = 0 THEN 'frozen' ELSE 'normal' END as risk_status,
80 + (seq % 20) as credit_score,
'active' as status,
DATE_SUB(NOW(), INTERVAL (seq % 365) DAY) as created_at
FROM (
SELECT @row := @row + 1 AS seq
FROM
(SELECT 0 UNION SELECT 1 UNION SELECT 2 UNION SELECT 3 UNION SELECT 4
UNION SELECT 5 UNION SELECT 6 UNION SELECT 7 UNION SELECT 8 UNION SELECT 9) t1,
(SELECT 0 UNION SELECT 1 UNION SELECT 2 UNION SELECT 3 UNION SELECT 4
UNION SELECT 5 UNION SELECT 6 UNION SELECT 7 UNION SELECT 8 UNION SELECT 9) t2,
(SELECT 0 UNION SELECT 1 UNION SELECT 2 UNION SELECT 3 UNION SELECT 4
UNION SELECT 5 UNION SELECT 6 UNION SELECT 7 UNION SELECT 8 UNION SELECT 9) t3,
(SELECT @row := batch_start - 1) r
LIMIT batch_end - batch_start + 1
) seqs
ON DUPLICATE KEY UPDATE id=id;
SET current_batch = current_batch + 1;
COMMIT;
END WHILE;
-- 批量生成实名记录(为已实名用户)
INSERT INTO user_realname (user_id, provider, status, masked_name, masked_id_no, verified_at)
SELECT
u.id,
'mock',
'success',
CONCAT('张*', CHAR(65 + (u.id % 26))),
CONCAT('3301**********', LPAD(u.id % 10000, 4, '0')),
DATE_SUB(NOW(), INTERVAL (u.id % 300) DAY)
FROM users u
WHERE u.realname_status = 'verified'
AND NOT EXISTS (SELECT 1 FROM user_realname WHERE user_id = u.id);
-- 批量生成钱包账户
INSERT INTO wallet_accounts (user_id, available_balance, frozen_balance, status)
SELECT
u.id,
(u.id % 10) * 100.00,
(u.id % 5) * 50.00,
'active'
FROM users u
WHERE NOT EXISTS (SELECT 1 FROM wallet_accounts WHERE user_id = u.id);
COMMIT;
END$$
DELIMITER ;
-- ============================================
-- 2. 批量生成游戏账号和商品
-- ============================================
DROP PROCEDURE IF EXISTS generate_listings_batch;
DELIMITER $$
CREATE PROCEDURE generate_listings_batch(IN batch_size INT)
BEGIN
DECLARE batch_limit INT DEFAULT 1000;
DECLARE batches INT;
DECLARE current_batch INT DEFAULT 0;
-- 创建临时表存储已实名用户ID
DROP TEMPORARY TABLE IF EXISTS tmp_verified_users;
CREATE TEMPORARY TABLE tmp_verified_users (
id BIGINT PRIMARY KEY,
row_num INT
);
INSERT INTO tmp_verified_users (id, row_num)
SELECT id, (@rn := @rn + 1) as row_num
FROM users, (SELECT @rn := 0) init
WHERE realname_status = 'verified'
ORDER BY id;
SET batches = CEIL(batch_size / batch_limit);
WHILE current_batch < batches DO
-- 批量生成游戏账号
INSERT INTO game_accounts (
owner_id, game_name, server_region, login_platform,
title, description, rank_level, haf_coin_amount, status
)
SELECT
u.id as owner_id,
'delta_force',
CASE (seq % 4)
WHEN 0 THEN '亚服'
WHEN 1 THEN '美服'
WHEN 2 THEN '欧服'
ELSE '国服'
END,
CASE (seq % 3)
WHEN 0 THEN 'Steam'
WHEN 1 THEN 'Epic'
ELSE 'WeGame'
END,
CONCAT('账号', seq, ' 高分段'),
CONCAT('这是一个测试账号,编号', seq),
CASE (seq % 5)
WHEN 0 THEN '青铜'
WHEN 1 THEN '白银'
WHEN 2 THEN '黄金'
WHEN 3 THEN '铂金'
ELSE '钻石'
END,
(seq % 100) * 10000,
'published'
FROM (
SELECT @row2 := @row2 + 1 AS seq
FROM
(SELECT 0 UNION SELECT 1 UNION SELECT 2 UNION SELECT 3 UNION SELECT 4
UNION SELECT 5 UNION SELECT 6 UNION SELECT 7 UNION SELECT 8 UNION SELECT 9) t1,
(SELECT 0 UNION SELECT 1 UNION SELECT 2 UNION SELECT 3 UNION SELECT 4
UNION SELECT 5 UNION SELECT 6 UNION SELECT 7 UNION SELECT 8 UNION SELECT 9) t2,
(SELECT 0 UNION SELECT 1 UNION SELECT 2 UNION SELECT 3 UNION SELECT 4
UNION SELECT 5 UNION SELECT 6 UNION SELECT 7 UNION SELECT 8 UNION SELECT 9) t3,
(SELECT @row2 := current_batch * batch_limit) r
LIMIT batch_limit
) seqs
INNER JOIN tmp_verified_users u ON u.row_num = (seq % (SELECT COUNT(*) FROM tmp_verified_users)) + 1
WHERE seq <= batch_size;
-- 批量生成租号商品
INSERT INTO rental_listings (
account_id, owner_id, price, deposit_amount,
in_transaction, status, review_status, published_at
)
SELECT
ga.id as account_id,
ga.owner_id,
5.00 + ((ga.id % 20) * 0.5) as price,
100.00 + ((ga.id % 10) * 50.00) as deposit_amount,
CASE WHEN ga.id % 10 = 0 THEN 1 ELSE 0 END as in_transaction,
CASE
WHEN ga.id % 20 = 0 THEN 'offline'
WHEN ga.id % 15 = 0 THEN 'draft'
ELSE 'published'
END as status,
CASE
WHEN ga.id % 15 = 0 THEN 'pending'
WHEN ga.id % 30 = 0 THEN 'rejected'
ELSE 'approved'
END as review_status,
DATE_SUB(NOW(), INTERVAL (ga.id % 90) DAY) as published_at
FROM game_accounts ga
WHERE ga.id > (SELECT COALESCE(MAX(account_id), 0) FROM rental_listings)
LIMIT batch_limit;
SET current_batch = current_batch + 1;
COMMIT;
END WHILE;
DROP TEMPORARY TABLE IF EXISTS tmp_verified_users;
END$$
DELIMITER ;
-- ============================================
-- 3. 批量生成订单数据(优化版)
-- ============================================
DROP PROCEDURE IF EXISTS generate_orders_batch;
DELIMITER $$
CREATE PROCEDURE generate_orders_batch(IN batch_size INT)
BEGIN
DECLARE batch_limit INT DEFAULT 1000;
DECLARE batches INT;
DECLARE current_batch INT DEFAULT 0;
-- 创建临时表:可用商品
DROP TEMPORARY TABLE IF EXISTS tmp_available_listings;
CREATE TEMPORARY TABLE tmp_available_listings (
id BIGINT PRIMARY KEY,
account_id BIGINT,
owner_id BIGINT,
price DECIMAL(12,2),
deposit_amount DECIMAL(12,2),
row_num INT
);
INSERT INTO tmp_available_listings (id, account_id, owner_id, price, deposit_amount, row_num)
SELECT id, account_id, owner_id, price, deposit_amount, (@rn := @rn + 1)
FROM rental_listings, (SELECT @rn := 0) init
WHERE status IN ('published', 'active') AND review_status = 'approved'
ORDER BY id;
-- 创建临时表:已实名用户
DROP TEMPORARY TABLE IF EXISTS tmp_verified_users;
CREATE TEMPORARY TABLE tmp_verified_users (
id BIGINT PRIMARY KEY,
row_num INT
);
INSERT INTO tmp_verified_users (id, row_num)
SELECT id, (@rn2 := @rn2 + 1)
FROM users, (SELECT @rn2 := 0) init
WHERE realname_status = 'verified'
ORDER BY id;
SET batches = CEIL(batch_size / batch_limit);
WHILE current_batch < batches DO
INSERT INTO rental_orders (
order_no, listing_id, account_id, owner_id, renter_id,
estimated_duration_hours, rent_amount, owner_rent_amount,
deposit_amount, platform_fee, status, handoff_status,
settlement_status, rented_at, created_at
)
SELECT
CONCAT('ORD', DATE_FORMAT(NOW(), '%Y%m%d'), LPAD(seq, 8, '0')) as order_no,
l.id as listing_id,
l.account_id,
l.owner_id,
r.id as renter_id,
24 as estimated_duration_hours,
l.price * 24 as rent_amount,
l.price * 24 * 0.95 as owner_rent_amount,
l.deposit_amount,
l.price * 24 * 0.05 as platform_fee,
CASE (seq % 10)
WHEN 0 THEN 'pending_payment'
WHEN 1 THEN 'cancelled'
WHEN 2 THEN 'closed'
ELSE 'completed'
END as status,
CASE (seq % 10)
WHEN 0 THEN 'none'
WHEN 1 THEN 'none'
WHEN 2 THEN 'owner_delivered'
ELSE 'owner_received'
END as handoff_status,
CASE (seq % 10)
WHEN 0 THEN 'unsettled'
WHEN 1 THEN 'unsettled'
ELSE 'settled'
END as settlement_status,
DATE_SUB(NOW(), INTERVAL (seq % 60) DAY) as rented_at,
DATE_SUB(NOW(), INTERVAL (seq % 60) DAY) as created_at
FROM (
SELECT @row3 := @row3 + 1 AS seq
FROM
(SELECT 0 UNION SELECT 1 UNION SELECT 2 UNION SELECT 3 UNION SELECT 4
UNION SELECT 5 UNION SELECT 6 UNION SELECT 7 UNION SELECT 8 UNION SELECT 9) t1,
(SELECT 0 UNION SELECT 1 UNION SELECT 2 UNION SELECT 3 UNION SELECT 4
UNION SELECT 5 UNION SELECT 6 UNION SELECT 7 UNION SELECT 8 UNION SELECT 9) t2,
(SELECT 0 UNION SELECT 1 UNION SELECT 2 UNION SELECT 3 UNION SELECT 4
UNION SELECT 5 UNION SELECT 6 UNION SELECT 7 UNION SELECT 8 UNION SELECT 9) t3,
(SELECT @row3 := current_batch * batch_limit) r
LIMIT batch_limit
) seqs
INNER JOIN tmp_available_listings l ON l.row_num = (seq % (SELECT COUNT(*) FROM tmp_available_listings)) + 1
INNER JOIN tmp_verified_users r ON r.row_num = (seq % (SELECT COUNT(*) FROM tmp_verified_users)) + 1
WHERE seq <= batch_size AND r.id != l.owner_id
LIMIT batch_limit;
SET current_batch = current_batch + 1;
COMMIT;
END WHILE;
DROP TEMPORARY TABLE IF EXISTS tmp_available_listings;
DROP TEMPORARY TABLE IF EXISTS tmp_verified_users;
END$$
DELIMITER ;
-- ============================================
-- 4. 批量生成钱包流水
-- ============================================
DROP PROCEDURE IF EXISTS generate_wallet_ledger_batch;
DELIMITER $$
CREATE PROCEDURE generate_wallet_ledger_batch(IN batch_size INT)
BEGIN
DECLARE batch_limit INT DEFAULT 1000;
DECLARE batches INT;
DECLARE current_batch INT DEFAULT 0;
-- 创建临时表:用户列表
DROP TEMPORARY TABLE IF EXISTS tmp_users;
CREATE TEMPORARY TABLE tmp_users (
id BIGINT PRIMARY KEY,
row_num INT
);
INSERT INTO tmp_users (id, row_num)
SELECT id, (@rn := @rn + 1)
FROM users, (SELECT @rn := 0) init
ORDER BY id;
-- 创建临时表:订单列表
DROP TEMPORARY TABLE IF EXISTS tmp_orders;
CREATE TEMPORARY TABLE tmp_orders (
id BIGINT PRIMARY KEY,
row_num INT
);
INSERT INTO tmp_orders (id, row_num)
SELECT id, (@rn2 := @rn2 + 1)
FROM rental_orders, (SELECT @rn2 := 0) init
ORDER BY id;
SET batches = CEIL(batch_size / batch_limit);
WHILE current_batch < batches DO
INSERT INTO wallet_ledger (
ledger_no, user_id, order_id, direction, amount,
balance_after, balance_type, biz_type, biz_no, remark, created_at
)
SELECT
CONCAT('LDG', DATE_FORMAT(NOW(), '%Y%m%d%H%i%s'), LPAD(seq, 6, '0')) as ledger_no,
u.id as user_id,
IF(seq % 2 = 0, o.id, NULL) as order_id,
CASE WHEN seq % 2 = 0 THEN 'in' ELSE 'out' END as direction,
(seq % 500) + (seq * 0.01) as amount,
1000.00 + (seq % 1000) as balance_after,
CASE WHEN seq % 5 = 0 THEN 'frozen' ELSE 'available' END as balance_type,
CASE (seq % 6)
WHEN 0 THEN 'rent_payment'
WHEN 1 THEN 'deposit_freeze'
WHEN 2 THEN 'settlement'
WHEN 3 THEN 'refund'
WHEN 4 THEN 'recharge'
ELSE 'withdraw'
END as biz_type,
CONCAT('BIZ', LPAD(seq, 10, '0')) as biz_no,
CONCAT('测试流水', seq) as remark,
DATE_SUB(NOW(), INTERVAL (seq % 180) DAY) as created_at
FROM (
SELECT @row4 := @row4 + 1 AS seq
FROM
(SELECT 0 UNION SELECT 1 UNION SELECT 2 UNION SELECT 3 UNION SELECT 4
UNION SELECT 5 UNION SELECT 6 UNION SELECT 7 UNION SELECT 8 UNION SELECT 9) t1,
(SELECT 0 UNION SELECT 1 UNION SELECT 2 UNION SELECT 3 UNION SELECT 4
UNION SELECT 5 UNION SELECT 6 UNION SELECT 7 UNION SELECT 8 UNION SELECT 9) t2,
(SELECT 0 UNION SELECT 1 UNION SELECT 2 UNION SELECT 3 UNION SELECT 4
UNION SELECT 5 UNION SELECT 6 UNION SELECT 7 UNION SELECT 8 UNION SELECT 9) t3,
(SELECT @row4 := current_batch * batch_limit) r
LIMIT batch_limit
) seqs
INNER JOIN tmp_users u ON u.row_num = (seq % (SELECT COUNT(*) FROM tmp_users)) + 1
LEFT JOIN tmp_orders o ON o.row_num = (seq % (SELECT COUNT(*) FROM tmp_orders)) + 1
WHERE seq <= batch_size
LIMIT batch_limit;
SET current_batch = current_batch + 1;
COMMIT;
END WHILE;
DROP TEMPORARY TABLE IF EXISTS tmp_users;
DROP TEMPORARY TABLE IF EXISTS tmp_orders;
END$$
DELIMITER ;
-- ============================================
-- 5. 批量生成聊天会话和消息
-- ============================================
DROP PROCEDURE IF EXISTS generate_chat_data_batch;
DELIMITER $$
CREATE PROCEDURE generate_chat_data_batch(IN message_count INT)
BEGIN
-- 先生成聊天会话(基于订单)
INSERT INTO chat_conversations (order_id, type, title, status, last_message_at)
SELECT
ro.id,
'order_group',
CONCAT('订单', ro.order_no, '群聊'),
'active',
ro.created_at
FROM rental_orders ro
WHERE NOT EXISTS (SELECT 1 FROM chat_conversations WHERE order_id = ro.id)
LIMIT 1000
ON DUPLICATE KEY UPDATE order_id=order_id;
-- 批量生成聊天消息
DECLARE batch_limit INT DEFAULT 1000;
DECLARE batches INT;
DECLARE current_batch INT DEFAULT 0;
DROP TEMPORARY TABLE IF EXISTS tmp_conversations;
CREATE TEMPORARY TABLE tmp_conversations (
id BIGINT PRIMARY KEY,
row_num INT
);
INSERT INTO tmp_conversations (id, row_num)
SELECT id, (@rn := @rn + 1)
FROM chat_conversations, (SELECT @rn := 0) init
ORDER BY id;
DROP TEMPORARY TABLE IF EXISTS tmp_users;
CREATE TEMPORARY TABLE tmp_users (
id BIGINT PRIMARY KEY,
row_num INT
);
INSERT INTO tmp_users (id, row_num)
SELECT id, (@rn2 := @rn2 + 1)
FROM users, (SELECT @rn2 := 0) init
ORDER BY id;
SET batches = CEIL(message_count / batch_limit);
WHILE current_batch < batches DO
INSERT INTO chat_messages (
conversation_id, sender_type, sender_id, sender_role,
content_type, content, created_at
)
SELECT
c.id as conversation_id,
'user' as sender_type,
u.id as sender_id,
CASE WHEN seq % 2 = 0 THEN 'owner' ELSE 'renter' END as sender_role,
'text' as content_type,
CONCAT('这是测试消息', seq, ',内容随机生成用于压力测试') as content,
DATE_SUB(NOW(), INTERVAL (seq % 30) DAY) as created_at
FROM (
SELECT @row5 := @row5 + 1 AS seq
FROM
(SELECT 0 UNION SELECT 1 UNION SELECT 2 UNION SELECT 3 UNION SELECT 4
UNION SELECT 5 UNION SELECT 6 UNION SELECT 7 UNION SELECT 8 UNION SELECT 9) t1,
(SELECT 0 UNION SELECT 1 UNION SELECT 2 UNION SELECT 3 UNION SELECT 4
UNION SELECT 5 UNION SELECT 6 UNION SELECT 7 UNION SELECT 8 UNION SELECT 9) t2,
(SELECT 0 UNION SELECT 1 UNION SELECT 2 UNION SELECT 3 UNION SELECT 4
UNION SELECT 5 UNION SELECT 6 UNION SELECT 7 UNION SELECT 8 UNION SELECT 9) t3,
(SELECT @row5 := current_batch * batch_limit) r
LIMIT batch_limit
) seqs
INNER JOIN tmp_conversations c ON c.row_num = (seq % (SELECT COUNT(*) FROM tmp_conversations)) + 1
INNER JOIN tmp_users u ON u.row_num = (seq % (SELECT COUNT(*) FROM tmp_users)) + 1
WHERE seq <= message_count
LIMIT batch_limit;
SET current_batch = current_batch + 1;
COMMIT;
END WHILE;
DROP TEMPORARY TABLE IF EXISTS tmp_conversations;
DROP TEMPORARY TABLE IF EXISTS tmp_users;
END$$
DELIMITER ;
-- ============================================
-- 本文件只创建存储过程,不直接生成数据。
-- 请使用 scripts/stress_test.sh data 按参数生成
-- ============================================
SET FOREIGN_KEY_CHECKS = 1;
-407
View File
@@ -1,407 +0,0 @@
package main
import (
"bytes"
"encoding/json"
"flag"
"fmt"
"math/rand"
"net/http"
"sync"
"sync/atomic"
"time"
)
// 压力测试工具 - 模拟实际业务场景
type TestConfig struct {
BaseURL string
Concurrency int
Duration time.Duration
Scenario string
}
type TestResult struct {
TotalRequests int64
SuccessRequests int64
FailedRequests int64
TotalLatency int64 // 毫秒
MinLatency int64
MaxLatency int64
Errors map[string]int64
}
var (
baseURL = flag.String("url", "http://localhost:8080", "API 基础地址")
concurrency = flag.Int("c", 10, "并发数")
duration = flag.Int("d", 60, "测试时长(秒)")
scenario = flag.String("s", "mixed", "测试场景: list_listings, create_order, chat, wallet, mixed")
)
func main() {
flag.Parse()
config := TestConfig{
BaseURL: *baseURL,
Concurrency: *concurrency,
Duration: time.Duration(*duration) * time.Second,
Scenario: *scenario,
}
fmt.Printf("=== 压力测试配置 ===\n")
fmt.Printf("目标地址: %s\n", config.BaseURL)
fmt.Printf("并发数: %d\n", config.Concurrency)
fmt.Printf("测试时长: %d 秒\n", *duration)
fmt.Printf("测试场景: %s\n", config.Scenario)
fmt.Printf("==================\n\n")
result := runTest(config)
printResult(result)
}
func runTest(config TestConfig) *TestResult {
result := &TestResult{
Errors: make(map[string]int64),
MinLatency: int64(^uint64(0) >> 1), // Max int64
}
var wg sync.WaitGroup
stopChan := make(chan struct{})
// 启动并发workers
for i := 0; i < config.Concurrency; i++ {
wg.Add(1)
go func(workerID int) {
defer wg.Done()
worker(workerID, config, result, stopChan)
}(i)
}
// 等待测试时长
time.Sleep(config.Duration)
close(stopChan)
wg.Wait()
return result
}
func worker(id int, config TestConfig, result *TestResult, stopChan chan struct{}) {
client := &http.Client{
Timeout: 10 * time.Second,
}
for {
select {
case <-stopChan:
return
default:
executeScenario(client, config, result)
}
}
}
func executeScenario(client *http.Client, config TestConfig, result *TestResult) {
switch config.Scenario {
case "list_listings":
testListListings(client, config.BaseURL, result)
case "create_order":
testCreateOrder(client, config.BaseURL, result)
case "chat":
testChatMessages(client, config.BaseURL, result)
case "wallet":
testWalletLedger(client, config.BaseURL, result)
case "mixed":
// 混合场景:按实际业务比例分配
r := rand.Intn(100)
switch {
case r < 40: // 40% 查询商品列表
testListListings(client, config.BaseURL, result)
case r < 60: // 20% 查询订单
testListOrders(client, config.BaseURL, result)
case r < 75: // 15% 查询钱包流水
testWalletLedger(client, config.BaseURL, result)
case r < 85: // 10% 聊天消息
testChatMessages(client, config.BaseURL, result)
case r < 95: // 10% 创建订单
testCreateOrder(client, config.BaseURL, result)
default: // 5% 支付
testPayOrder(client, config.BaseURL, result)
}
default:
testHealthCheck(client, config.BaseURL, result)
}
}
// ============================================
// 测试场景实现
// ============================================
func testHealthCheck(client *http.Client, baseURL string, result *TestResult) {
start := time.Now()
resp, err := client.Get(baseURL + "/health")
latency := time.Since(start).Milliseconds()
atomic.AddInt64(&result.TotalRequests, 1)
updateLatency(result, latency)
if err != nil {
atomic.AddInt64(&result.FailedRequests, 1)
recordError(result, "health_check_error: "+err.Error())
return
}
defer resp.Body.Close()
if resp.StatusCode == 200 {
atomic.AddInt64(&result.SuccessRequests, 1)
} else {
atomic.AddInt64(&result.FailedRequests, 1)
recordError(result, fmt.Sprintf("health_check_status_%d", resp.StatusCode))
}
}
func testListListings(client *http.Client, baseURL string, result *TestResult) {
// 模拟不同的查询条件
page := rand.Intn(10) + 1
pageSize := []int{10, 20, 50}[rand.Intn(3)]
url := fmt.Sprintf("%s/api/listings?page=%d&page_size=%d", baseURL, page, pageSize)
start := time.Now()
resp, err := client.Get(url)
latency := time.Since(start).Milliseconds()
atomic.AddInt64(&result.TotalRequests, 1)
updateLatency(result, latency)
if err != nil {
atomic.AddInt64(&result.FailedRequests, 1)
recordError(result, "list_listings_error: "+err.Error())
return
}
defer resp.Body.Close()
if resp.StatusCode == 200 {
atomic.AddInt64(&result.SuccessRequests, 1)
} else {
atomic.AddInt64(&result.FailedRequests, 1)
recordError(result, fmt.Sprintf("list_listings_status_%d", resp.StatusCode))
}
}
func testListOrders(client *http.Client, baseURL string, result *TestResult) {
// 需要登录token,这里模拟匿名访问(会返回401)
page := rand.Intn(5) + 1
url := fmt.Sprintf("%s/api/orders?page=%d&page_size=20", baseURL, page)
start := time.Now()
resp, err := client.Get(url)
latency := time.Since(start).Milliseconds()
atomic.AddInt64(&result.TotalRequests, 1)
updateLatency(result, latency)
if err != nil {
atomic.AddInt64(&result.FailedRequests, 1)
recordError(result, "list_orders_error: "+err.Error())
return
}
defer resp.Body.Close()
// 401是预期的(未登录)
if resp.StatusCode == 200 || resp.StatusCode == 401 {
atomic.AddInt64(&result.SuccessRequests, 1)
} else {
atomic.AddInt64(&result.FailedRequests, 1)
recordError(result, fmt.Sprintf("list_orders_status_%d", resp.StatusCode))
}
}
func testCreateOrder(client *http.Client, baseURL string, result *TestResult) {
// 模拟创建订单(需要登录,会返回401)
listingID := rand.Intn(1000) + 1
payload := map[string]interface{}{
"listing_id": listingID,
"estimated_duration_hours": 24,
}
body, _ := json.Marshal(payload)
start := time.Now()
resp, err := client.Post(
baseURL+"/api/orders",
"application/json",
bytes.NewBuffer(body),
)
latency := time.Since(start).Milliseconds()
atomic.AddInt64(&result.TotalRequests, 1)
updateLatency(result, latency)
if err != nil {
atomic.AddInt64(&result.FailedRequests, 1)
recordError(result, "create_order_error: "+err.Error())
return
}
defer resp.Body.Close()
// 401是预期的(未登录)
if resp.StatusCode == 200 || resp.StatusCode == 401 {
atomic.AddInt64(&result.SuccessRequests, 1)
} else {
atomic.AddInt64(&result.FailedRequests, 1)
recordError(result, fmt.Sprintf("create_order_status_%d", resp.StatusCode))
}
}
func testPayOrder(client *http.Client, baseURL string, result *TestResult) {
orderID := rand.Intn(1000) + 1
url := fmt.Sprintf("%s/api/orders/%d/pay", baseURL, orderID)
payload := map[string]interface{}{
"provider": "mock",
}
body, _ := json.Marshal(payload)
start := time.Now()
resp, err := client.Post(url, "application/json", bytes.NewBuffer(body))
latency := time.Since(start).Milliseconds()
atomic.AddInt64(&result.TotalRequests, 1)
updateLatency(result, latency)
if err != nil {
atomic.AddInt64(&result.FailedRequests, 1)
recordError(result, "pay_order_error: "+err.Error())
return
}
defer resp.Body.Close()
// 401是预期的(未登录)
if resp.StatusCode == 200 || resp.StatusCode == 401 {
atomic.AddInt64(&result.SuccessRequests, 1)
} else {
atomic.AddInt64(&result.FailedRequests, 1)
recordError(result, fmt.Sprintf("pay_order_status_%d", resp.StatusCode))
}
}
func testWalletLedger(client *http.Client, baseURL string, result *TestResult) {
page := rand.Intn(10) + 1
url := fmt.Sprintf("%s/api/wallet/ledger?page=%d&page_size=20", baseURL, page)
start := time.Now()
resp, err := client.Get(url)
latency := time.Since(start).Milliseconds()
atomic.AddInt64(&result.TotalRequests, 1)
updateLatency(result, latency)
if err != nil {
atomic.AddInt64(&result.FailedRequests, 1)
recordError(result, "wallet_ledger_error: "+err.Error())
return
}
defer resp.Body.Close()
// 401是预期的(未登录)
if resp.StatusCode == 200 || resp.StatusCode == 401 {
atomic.AddInt64(&result.SuccessRequests, 1)
} else {
atomic.AddInt64(&result.FailedRequests, 1)
recordError(result, fmt.Sprintf("wallet_ledger_status_%d", resp.StatusCode))
}
}
func testChatMessages(client *http.Client, baseURL string, result *TestResult) {
conversationID := rand.Intn(100) + 1
url := fmt.Sprintf("%s/api/chats/%d/messages?page=1&page_size=50", baseURL, conversationID)
start := time.Now()
resp, err := client.Get(url)
latency := time.Since(start).Milliseconds()
atomic.AddInt64(&result.TotalRequests, 1)
updateLatency(result, latency)
if err != nil {
atomic.AddInt64(&result.FailedRequests, 1)
recordError(result, "chat_messages_error: "+err.Error())
return
}
defer resp.Body.Close()
// 401是预期的(未登录)
if resp.StatusCode == 200 || resp.StatusCode == 401 {
atomic.AddInt64(&result.SuccessRequests, 1)
} else {
atomic.AddInt64(&result.FailedRequests, 1)
recordError(result, fmt.Sprintf("chat_messages_status_%d", resp.StatusCode))
}
}
// ============================================
// 辅助函数
// ============================================
func updateLatency(result *TestResult, latency int64) {
atomic.AddInt64(&result.TotalLatency, latency)
// 更新最小延迟
for {
current := atomic.LoadInt64(&result.MinLatency)
if latency >= current {
break
}
if atomic.CompareAndSwapInt64(&result.MinLatency, current, latency) {
break
}
}
// 更新最大延迟
for {
current := atomic.LoadInt64(&result.MaxLatency)
if latency <= current {
break
}
if atomic.CompareAndSwapInt64(&result.MaxLatency, current, latency) {
break
}
}
}
var errorMutex sync.Mutex
func recordError(result *TestResult, errMsg string) {
errorMutex.Lock()
defer errorMutex.Unlock()
result.Errors[errMsg]++
}
func printResult(result *TestResult) {
fmt.Printf("\n=== 压力测试结果 ===\n")
fmt.Printf("总请求数: %d\n", result.TotalRequests)
fmt.Printf("成功请求: %d (%.2f%%)\n",
result.SuccessRequests,
float64(result.SuccessRequests)/float64(result.TotalRequests)*100)
fmt.Printf("失败请求: %d (%.2f%%)\n",
result.FailedRequests,
float64(result.FailedRequests)/float64(result.TotalRequests)*100)
if result.TotalRequests > 0 {
avgLatency := result.TotalLatency / result.TotalRequests
fmt.Printf("\n延迟统计:\n")
fmt.Printf(" 最小延迟: %d ms\n", result.MinLatency)
fmt.Printf(" 平均延迟: %d ms\n", avgLatency)
fmt.Printf(" 最大延迟: %d ms\n", result.MaxLatency)
}
if len(result.Errors) > 0 {
fmt.Printf("\n错误统计:\n")
for err, count := range result.Errors {
fmt.Printf(" %s: %d 次\n", err, count)
}
}
qps := float64(result.TotalRequests) / float64(*duration)
fmt.Printf("\nQPS: %.2f\n", qps)
fmt.Printf("==================\n")
}
+120 -31
View File
@@ -27,41 +27,58 @@ function log_error() {
function show_usage() {
cat << EOF
压力测试脚本
压力测试脚本(优化版)
用法:
$0 [command] [options]
命令:
data 生成测试数据
test 执行压力测试
data 生成测试数据(使用优化的批量生成)
test 执行压力测试(支持真实认证)
monitor 监控系统性能
clean 清理测试数据
report 生成压测报告
all 执行完整流程(生成数据 + 压测 + 报告)
选项:
-u, --users NUM 生成用户数量(默认: 10000)
-l, --listings NUM 生成商品数量(默认: 50000
-o, --orders NUM 生成订单数量(默认: 30000
-c, --concurrency NUM 并发数(默认: 100
-d, --duration SEC 测试时长/秒(默认: 60
-s, --scenario NAME 测试场景: list_listings, create_order, wallet, chat, mixed(默认: mixed
--url URL 后端地址(默认: http://localhost:8080
-h, --help 显示帮助信息
数据生成:
-u, --users NUM 生成用户数量(默认: 1000
-l, --listings NUM 生成商品数量(默认: 5000
-o, --orders NUM 生成订单数量(默认: 3000
--ledger NUM 生成钱包流水数量(默认: 10000
--chat-messages NUM 生成聊天消息数量(默认: 5000
--use-optimized 使用优化的数据生成脚本(推荐
压力测试:
-c, --concurrency NUM 并发数(默认: 50
-d, --duration SEC 测试时长/秒(默认: 60)
-s, --scenario NAME 测试场景:
- realistic: 真实业务场景(默认)
- admin: 管理后台场景
- listing_only: 仅商品查询
--gradual 启用梯度压测
--warmup NUM 预热用户数(默认: 100)
--use-improved 使用改进的压测工具(支持真实认证)
--url URL 后端地址(默认: http://localhost:8080
通用:
-h, --help 显示帮助信息
示例:
# 生成测试数据
$0 data
# 小规模数据生成(使用优化脚本)
$0 data --use-optimized
# 执行混合场景压测(100并发,持续60秒)
$0 test -c 100 -d 60 -s mixed
# 真实场景压测(100个用户token50并发,持续60秒)
$0 test --use-improved -c 50 -d 60 -s realistic --warmup 100
# 执行商品列表查询压测
$0 test -s list_listings -c 200 -d 120
# 管理后台压测
$0 test --use-improved -c 30 -d 120 -s admin
# 执行完整流程
$0 all
# 梯度压测(逐步增加负载)
$0 test --use-improved --gradual -d 60
# 执行完整流程(优化版)
$0 all --use-optimized --use-improved
# 清理测试数据
$0 clean
@@ -70,14 +87,19 @@ EOF
}
# 默认参数
USERS=10000
LISTINGS=50000
ORDERS=30000
LEDGER=100000
CONCURRENCY=100
USERS=1000
LISTINGS=5000
ORDERS=3000
LEDGER=10000
CHAT_MESSAGES=5000
CONCURRENCY=50
DURATION=60
SCENARIO="mixed"
SCENARIO="realistic"
BASE_URL="http://localhost:8080"
USE_OPTIMIZED=false
USE_IMPROVED=true
GRADUAL=false
WARMUP=100
# 解析命令行参数
COMMAND=""
@@ -99,6 +121,14 @@ while [[ $# -gt 0 ]]; do
ORDERS="$2"
shift 2
;;
--ledger)
LEDGER="$2"
shift 2
;;
--chat-messages)
CHAT_MESSAGES="$2"
shift 2
;;
-c|--concurrency)
CONCURRENCY="$2"
shift 2
@@ -115,6 +145,22 @@ while [[ $# -gt 0 ]]; do
BASE_URL="$2"
shift 2
;;
--use-optimized)
USE_OPTIMIZED=true
shift
;;
--use-improved)
USE_IMPROVED=true
shift
;;
--gradual)
GRADUAL=true
shift
;;
--warmup)
WARMUP="$2"
shift 2
;;
-h|--help)
show_usage
exit 0
@@ -162,15 +208,45 @@ function check_backend() {
# 生成测试数据
function generate_data() {
log_info "开始生成测试数据..."
log_info "配置: 用户=$USERS, 商品=$LISTINGS, 订单=$ORDERS"
log_info "配置: 用户=$USERS, 商品=$LISTINGS, 订单=$ORDERS, 钱包流水=$LEDGER, 聊天消息=$CHAT_MESSAGES"
check_database || exit 1
# 选择使用的SQL脚本
SQL_FILE="$SCRIPT_DIR/load_test_data.sql"
if [ "$USE_OPTIMIZED" = true ]; then
SQL_FILE="$SCRIPT_DIR/load_test_data_optimized.sql"
log_info "使用优化的数据生成脚本"
fi
# 创建临时SQL文件
TMP_SQL="/tmp/load_test_data_$(date +%s).sql"
trap 'rm -f "$TMP_SQL"' RETURN
cat > "$TMP_SQL" << EOF
-- 临时生成的测试数据脚本
if [ "$USE_OPTIMIZED" = true ]; then
cat > "$TMP_SQL" << EOF
-- 优化版数据生成
USE hfb_sys;
-- 调用批量生成存储过程
CALL generate_users_batch($USERS);
CALL generate_listings_batch($LISTINGS);
CALL generate_orders_batch($ORDERS);
CALL generate_wallet_ledger_batch($LEDGER);
CALL generate_chat_data_batch($CHAT_MESSAGES);
-- 显示统计
SELECT '用户数' as item, COUNT(*) as count FROM users
UNION ALL SELECT '游戏账号数', COUNT(*) FROM game_accounts
UNION ALL SELECT '商品数', COUNT(*) FROM rental_listings
UNION ALL SELECT '订单数', COUNT(*) FROM rental_orders
UNION ALL SELECT '钱包流水数', COUNT(*) FROM wallet_ledger
UNION ALL SELECT '聊天会话数', COUNT(*) FROM chat_conversations
UNION ALL SELECT '聊天消息数', COUNT(*) FROM chat_messages;
EOF
else
cat > "$TMP_SQL" << EOF
-- 原始版数据生成
USE hfb_sys;
-- 调用存储过程生成数据
@@ -186,7 +262,7 @@ FROM rental_orders
WHERE id <= 1000
ON DUPLICATE KEY UPDATE order_id=order_id;
CALL generate_chat_messages(50000);
CALL generate_chat_messages($CHAT_MESSAGES);
-- 显示统计
SELECT '用户数' as item, COUNT(*) as count FROM users
@@ -197,16 +273,18 @@ UNION ALL SELECT '钱包流水数', COUNT(*) FROM wallet_ledger
UNION ALL SELECT '聊天会话数', COUNT(*) FROM chat_conversations
UNION ALL SELECT '聊天消息数', COUNT(*) FROM chat_messages;
EOF
fi
log_info "执行数据生成..."
# 先执行基础SQL创建存储过程
docker exec -i hfb-mysql mysql -uhfb -psecret hfb_sys < "$SCRIPT_DIR/load_test_data.sql"
docker exec -i hfb-mysql mysql -uhfb -psecret hfb_sys < "$SQL_FILE"
# 执行数据生成
docker exec -i hfb-mysql mysql -uhfb -psecret hfb_sys < "$TMP_SQL"
rm -f "$TMP_SQL"
trap - RETURN
log_info "测试数据生成完成!"
}
@@ -221,11 +299,22 @@ function run_stress_test() {
# 编译压测工具
log_info "编译压测工具..."
cd "$SCRIPT_DIR"
go build -o stress_test stress_test.go
if [ $? -ne 0 ]; then
log_error "压测工具编译失败"
return 1
fi
# 执行压测
log_info "开始执行压力测试..."
./stress_test -url "$BASE_URL" -c "$CONCURRENCY" -d "$DURATION" -s "$SCENARIO"
if [ "$GRADUAL" = true ]; then
./stress_test -url "$BASE_URL" -c "$CONCURRENCY" -d "$DURATION" -s "$SCENARIO" -warmup "$WARMUP" -gradual
else
./stress_test -url "$BASE_URL" -c "$CONCURRENCY" -d "$DURATION" -s "$SCENARIO" -warmup "$WARMUP"
fi
log_info "压力测试完成!"
}