增加测试 修复错误

This commit is contained in:
yml
2026-06-05 22:01:14 +08:00
parent 402c19320d
commit b04fb629cb
6 changed files with 2042 additions and 1 deletions
+394
View File
@@ -0,0 +1,394 @@
-- ============================================
-- 压力测试数据生成脚本
-- 用于生成大量测试数据以进行性能测试
-- ============================================
SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;
-- 清理测试数据(可选,谨慎使用)
-- DELETE FROM wallet_ledger WHERE user_id > 1000;
-- DELETE FROM rental_orders WHERE id > 100;
-- DELETE FROM rental_listings WHERE id > 100;
-- DELETE FROM game_accounts WHERE id > 100;
-- DELETE FROM users WHERE id > 1000;
-- ============================================
-- 1. 批量生成用户数据(10000个用户)
-- ============================================
DROP PROCEDURE IF EXISTS generate_users;
DELIMITER $$
CREATE PROCEDURE generate_users(IN batch_size INT)
BEGIN
DECLARE i INT DEFAULT 1;
DECLARE phone_num VARCHAR(32);
DECLARE nick VARCHAR(64);
WHILE i <= batch_size DO
SET phone_num = CONCAT('138', LPAD(i, 8, '0'));
SET nick = CONCAT('测试用户', i);
INSERT INTO users (
phone, nickname, realname_status, risk_status,
credit_score, status, created_at
) VALUES (
phone_num, nick,
CASE WHEN i % 10 = 0 THEN 'unverified' ELSE 'verified' END,
CASE WHEN i % 100 = 0 THEN 'frozen' ELSE 'normal' END,
80 + (i % 20),
'active',
DATE_SUB(NOW(), INTERVAL (i % 365) DAY)
) ON DUPLICATE KEY UPDATE id=id;
-- 为已实名用户创建实名记录
IF i % 10 != 0 THEN
INSERT INTO user_realname (
user_id, provider, status, masked_name, masked_id_no, verified_at
) VALUES (
LAST_INSERT_ID(), 'mock', 'success',
CONCAT('张*', CHAR(65 + (i % 26))),
CONCAT('3301**********', LPAD(i % 10000, 4, '0')),
DATE_SUB(NOW(), INTERVAL (i % 300) DAY)
) ON DUPLICATE KEY UPDATE user_id=user_id;
END IF;
-- 为用户创建钱包
INSERT INTO wallet_accounts (user_id, available_balance, frozen_balance, status)
VALUES (
LAST_INSERT_ID(),
(i % 10) * 100.00,
(i % 5) * 50.00,
'active'
) ON DUPLICATE KEY UPDATE user_id=user_id;
SET i = i + 1;
-- 每1000条提交一次
IF i % 1000 = 0 THEN
COMMIT;
END IF;
END WHILE;
COMMIT;
END$$
DELIMITER ;
-- ============================================
-- 2. 批量生成游戏账号和商品(50000个商品)
-- ============================================
DROP PROCEDURE IF EXISTS generate_listings;
DELIMITER $$
CREATE PROCEDURE generate_listings(IN batch_size INT)
BEGIN
DECLARE i INT DEFAULT 1;
DECLARE user_id_val BIGINT;
DECLARE account_id_val BIGINT;
DECLARE price_val DECIMAL(12,2);
DECLARE deposit_val DECIMAL(12,2);
WHILE i <= batch_size DO
-- 随机选择一个用户作为号主(已实名用户)
SELECT id INTO user_id_val FROM users
WHERE realname_status = 'verified'
ORDER BY RAND() LIMIT 1;
-- 创建游戏账号
INSERT INTO game_accounts (
owner_id, game_name, server_region, login_platform,
title, description, rank_level, haf_coin_amount, status
) VALUES (
user_id_val,
'delta_force',
CASE (i % 4)
WHEN 0 THEN '亚服'
WHEN 1 THEN '美服'
WHEN 2 THEN '欧服'
ELSE '国服'
END,
CASE (i % 3)
WHEN 0 THEN 'Steam'
WHEN 1 THEN 'Epic'
ELSE 'WeGame'
END,
CONCAT('账号', i, ' 高分段'),
CONCAT('这是一个测试账号,编号', i),
CASE (i % 5)
WHEN 0 THEN '青铜'
WHEN 1 THEN '白银'
WHEN 2 THEN '黄金'
WHEN 3 THEN '铂金'
ELSE '钻石'
END,
(i % 100) * 10000,
'active'
);
SET account_id_val = LAST_INSERT_ID();
-- 创建租号商品
SET price_val = 5.00 + (i % 20) * 0.5;
SET deposit_val = 100.00 + (i % 10) * 50.00;
INSERT INTO rental_listings (
account_id, owner_id, price, deposit_amount,
in_transaction, status, review_status, published_at
) VALUES (
account_id_val,
user_id_val,
price_val,
deposit_val,
CASE WHEN i % 10 = 0 THEN 1 ELSE 0 END,
CASE
WHEN i % 20 = 0 THEN 'offline'
WHEN i % 15 = 0 THEN 'draft'
ELSE 'active'
END,
CASE
WHEN i % 15 = 0 THEN 'pending'
WHEN i % 30 = 0 THEN 'rejected'
ELSE 'approved'
END,
DATE_SUB(NOW(), INTERVAL (i % 90) DAY)
);
SET i = i + 1;
IF i % 1000 = 0 THEN
COMMIT;
END IF;
END WHILE;
COMMIT;
END$$
DELIMITER ;
-- ============================================
-- 3. 批量生成订单数据(30000个订单)
-- ============================================
DROP PROCEDURE IF EXISTS generate_orders;
DELIMITER $$
CREATE PROCEDURE generate_orders(IN batch_size INT)
BEGIN
DECLARE i INT DEFAULT 1;
DECLARE listing_id_val BIGINT;
DECLARE account_id_val BIGINT;
DECLARE owner_id_val BIGINT;
DECLARE renter_id_val BIGINT;
DECLARE order_no_val VARCHAR(64);
DECLARE rent_amount_val DECIMAL(12,2);
DECLARE deposit_val DECIMAL(12,2);
WHILE i <= batch_size DO
-- 随机选择一个上架的商品
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;
-- 随机选择一个租客(不能是号主本人)
SELECT id INTO renter_id_val FROM users
WHERE id != owner_id_val AND realname_status = 'verified'
ORDER BY RAND() LIMIT 1;
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小时租金
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
) VALUES (
order_no_val,
listing_id_val,
account_id_val,
owner_id_val,
renter_id_val,
24,
rent_amount_val,
rent_amount_val * 0.95, -- 号主实得95%
deposit_val,
rent_amount_val * 0.05, -- 平台5%手续费
CASE (i % 10)
WHEN 0 THEN 'pending_payment'
WHEN 1 THEN 'cancelled'
WHEN 2 THEN 'closed'
ELSE 'completed'
END,
CASE (i % 10)
WHEN 0 THEN 'none'
WHEN 1 THEN 'none'
WHEN 2 THEN 'owner_delivered'
ELSE 'owner_received'
END,
CASE (i % 10)
WHEN 0 THEN 'unsettled'
WHEN 1 THEN 'unsettled'
ELSE 'settled'
END,
DATE_SUB(NOW(), INTERVAL (i % 60) DAY),
DATE_SUB(NOW(), INTERVAL (i % 60) DAY)
);
SET i = i + 1;
IF i % 1000 = 0 THEN
COMMIT;
END IF;
END WHILE;
COMMIT;
END$$
DELIMITER ;
-- ============================================
-- 4. 批量生成钱包流水(100000条流水)
-- ============================================
DROP PROCEDURE IF EXISTS generate_wallet_ledger;
DELIMITER $$
CREATE PROCEDURE generate_wallet_ledger(IN batch_size INT)
BEGIN
DECLARE i INT DEFAULT 1;
DECLARE user_id_val BIGINT;
DECLARE order_id_val BIGINT;
DECLARE ledger_no_val VARCHAR(64);
DECLARE amount_val DECIMAL(12,2);
WHILE i <= batch_size DO
-- 随机选择用户
SELECT id INTO user_id_val FROM users ORDER BY RAND() LIMIT 1;
-- 随机关联订单(50%概率)
IF RAND() > 0.5 THEN
SELECT id INTO order_id_val FROM rental_orders ORDER BY RAND() LIMIT 1;
ELSE
SET order_id_val = NULL;
END IF;
SET ledger_no_val = CONCAT('LDG', DATE_FORMAT(NOW(), '%Y%m%d%H%i%s'), LPAD(i, 6, '0'));
SET amount_val = (i % 500) + RAND() * 100;
INSERT INTO wallet_ledger (
ledger_no, user_id, order_id, direction, amount,
balance_after, balance_type, biz_type, biz_no, remark, created_at
) VALUES (
ledger_no_val,
user_id_val,
order_id_val,
CASE WHEN i % 2 = 0 THEN 'in' ELSE 'out' END,
amount_val,
1000.00 + (i % 1000),
CASE WHEN i % 5 = 0 THEN 'frozen' ELSE 'available' END,
CASE (i % 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,
CONCAT('BIZ', LPAD(i, 10, '0')),
CONCAT('测试流水', i),
DATE_SUB(NOW(), INTERVAL (i % 180) DAY)
);
SET i = i + 1;
IF i % 1000 = 0 THEN
COMMIT;
END IF;
END WHILE;
COMMIT;
END$$
DELIMITER ;
-- ============================================
-- 5. 批量生成聊天消息(50000条消息)
-- ============================================
DROP PROCEDURE IF EXISTS generate_chat_messages;
DELIMITER $$
CREATE PROCEDURE generate_chat_messages(IN batch_size INT)
BEGIN
DECLARE i INT DEFAULT 1;
DECLARE conv_id BIGINT;
DECLARE user_id_val BIGINT;
WHILE i <= batch_size DO
-- 随机选择一个会话
SELECT id INTO conv_id FROM chat_conversations ORDER BY RAND() LIMIT 1;
IF conv_id IS NOT NULL THEN
-- 随机选择发送者
SELECT id INTO user_id_val FROM users ORDER BY RAND() LIMIT 1;
INSERT INTO chat_messages (
conversation_id, sender_type, sender_id, sender_role,
content_type, content, created_at
) VALUES (
conv_id,
'user',
user_id_val,
CASE WHEN i % 2 = 0 THEN 'owner' ELSE 'renter' END,
'text',
CONCAT('这是测试消息', i, ',内容随机生成用于压力测试'),
DATE_SUB(NOW(), INTERVAL (i % 30) DAY)
);
END IF;
SET i = i + 1;
IF i % 1000 = 0 THEN
COMMIT;
END IF;
END WHILE;
COMMIT;
END$$
DELIMITER ;
-- ============================================
-- 执行数据生成(根据需要调整数量)
-- ============================================
-- 生成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;
+407
View File
@@ -0,0 +1,407 @@
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")
}
+393
View File
@@ -0,0 +1,393 @@
#!/bin/bash
# 压力测试快速启动脚本
set -e
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
PROJECT_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
# 颜色输出
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m' # No Color
function log_info() {
echo -e "${GREEN}[INFO]${NC} $1"
}
function log_warn() {
echo -e "${YELLOW}[WARN]${NC} $1"
}
function log_error() {
echo -e "${RED}[ERROR]${NC} $1"
}
function show_usage() {
cat << EOF
压力测试脚本
用法:
$0 [command] [options]
命令:
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 显示帮助信息
示例:
# 生成测试数据
$0 data
# 执行混合场景压测(100并发,持续60秒)
$0 test -c 100 -d 60 -s mixed
# 执行商品列表查询压测
$0 test -s list_listings -c 200 -d 120
# 执行完整流程
$0 all
# 清理测试数据
$0 clean
EOF
}
# 默认参数
USERS=10000
LISTINGS=50000
ORDERS=30000
LEDGER=100000
CONCURRENCY=100
DURATION=60
SCENARIO="mixed"
BASE_URL="http://localhost:8080"
# 解析命令行参数
COMMAND=""
while [[ $# -gt 0 ]]; do
case $1 in
data|test|monitor|clean|report|all)
COMMAND="$1"
shift
;;
-u|--users)
USERS="$2"
shift 2
;;
-l|--listings)
LISTINGS="$2"
shift 2
;;
-o|--orders)
ORDERS="$2"
shift 2
;;
-c|--concurrency)
CONCURRENCY="$2"
shift 2
;;
-d|--duration)
DURATION="$2"
shift 2
;;
-s|--scenario)
SCENARIO="$2"
shift 2
;;
--url)
BASE_URL="$2"
shift 2
;;
-h|--help)
show_usage
exit 0
;;
*)
log_error "未知参数: $1"
show_usage
exit 1
;;
esac
done
if [ -z "$COMMAND" ]; then
log_error "请指定命令"
show_usage
exit 1
fi
# 检查数据库连接
function check_database() {
log_info "检查数据库连接..."
if docker exec hfb-mysql mysql -uhfb -psecret -e "SELECT 1" &>/dev/null; then
log_info "数据库连接正常"
return 0
else
log_error "数据库连接失败,请确保 Docker 容器正在运行"
log_info "提示: 执行 'docker-compose -f deploy/docker-compose.dev.yml up -d' 启动服务"
return 1
fi
}
# 检查后端服务
function check_backend() {
log_info "检查后端服务..."
if curl -s "$BASE_URL/health" &>/dev/null; then
log_info "后端服务正常"
return 0
else
log_warn "后端服务未响应: $BASE_URL"
log_info "提示: 确保后端服务已启动"
return 1
fi
}
# 生成测试数据
function generate_data() {
log_info "开始生成测试数据..."
log_info "配置: 用户=$USERS, 商品=$LISTINGS, 订单=$ORDERS"
check_database || exit 1
# 创建临时SQL文件
TMP_SQL="/tmp/load_test_data_$(date +%s).sql"
cat > "$TMP_SQL" << EOF
-- 临时生成的测试数据脚本
USE hfb_sys;
-- 调用存储过程生成数据
CALL generate_users($USERS);
CALL generate_listings($LISTINGS);
CALL generate_orders($ORDERS);
CALL generate_wallet_ledger($LEDGER);
-- 生成聊天会话
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;
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;
EOF
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 < "$TMP_SQL"
rm -f "$TMP_SQL"
log_info "测试数据生成完成!"
}
# 执行压力测试
function run_stress_test() {
log_info "开始压力测试..."
log_info "配置: 场景=$SCENARIO, 并发=$CONCURRENCY, 时长=${DURATION}秒, 目标=$BASE_URL"
check_backend || log_warn "后端服务未响应,测试可能失败"
# 编译压测工具
log_info "编译压测工具..."
cd "$SCRIPT_DIR"
go build -o stress_test stress_test.go
# 执行压测
log_info "开始执行压力测试..."
./stress_test -url "$BASE_URL" -c "$CONCURRENCY" -d "$DURATION" -s "$SCENARIO"
log_info "压力测试完成!"
}
# 监控系统性能
function monitor_system() {
log_info "开始监控系统性能(按 Ctrl+C 停止)..."
echo ""
echo "=== Docker 容器资源使用 ==="
docker stats --no-stream hfb-backend hfb-mysql hfb-redis 2>/dev/null || log_warn "部分容器未运行"
echo ""
echo "=== MySQL 连接数 ==="
docker exec hfb-mysql mysql -uhfb -psecret -e "SHOW STATUS LIKE 'Threads_connected';" 2>/dev/null
echo ""
echo "=== MySQL 慢查询统计 ==="
docker exec hfb-mysql mysql -uhfb -psecret -e "SHOW STATUS LIKE 'Slow_queries';" 2>/dev/null
echo ""
echo "=== Redis 信息 ==="
docker exec hfb-redis redis-cli INFO stats | grep -E "total_connections_received|total_commands_processed|instantaneous_ops_per_sec" 2>/dev/null
echo ""
log_info "持续监控请使用: docker stats"
}
# 清理测试数据
function clean_data() {
log_warn "即将清理所有测试数据,此操作不可恢复!"
read -p "确认继续?(yes/no): " confirm
if [ "$confirm" != "yes" ]; then
log_info "已取消清理操作"
exit 0
fi
check_database || exit 1
log_info "开始清理测试数据..."
docker exec -i hfb-mysql mysql -uhfb -psecret hfb_sys << 'EOF'
SET FOREIGN_KEY_CHECKS = 0;
-- 清理测试数据(保留ID < 1000的数据)
DELETE FROM chat_messages WHERE id > 100;
DELETE FROM chat_conversations WHERE id > 100;
DELETE FROM wallet_ledger WHERE id > 100;
DELETE FROM rental_orders WHERE id > 100;
DELETE FROM rental_listings WHERE id > 100;
DELETE FROM game_accounts WHERE id > 100;
DELETE FROM users WHERE id > 1000;
-- 重置自增ID
ALTER TABLE users AUTO_INCREMENT = 1001;
ALTER TABLE game_accounts AUTO_INCREMENT = 101;
ALTER TABLE rental_listings AUTO_INCREMENT = 101;
ALTER TABLE rental_orders AUTO_INCREMENT = 101;
ALTER TABLE wallet_ledger AUTO_INCREMENT = 101;
ALTER TABLE chat_conversations AUTO_INCREMENT = 101;
ALTER TABLE chat_messages AUTO_INCREMENT = 101;
SET FOREIGN_KEY_CHECKS = 1;
-- 显示清理后的统计
SELECT '用户数' as item, COUNT(*) as count FROM users
UNION ALL SELECT '商品数', COUNT(*) FROM rental_listings
UNION ALL SELECT '订单数', COUNT(*) FROM rental_orders
UNION ALL SELECT '钱包流水数', COUNT(*) FROM wallet_ledger;
EOF
log_info "测试数据清理完成!"
}
# 生成压测报告
function generate_report() {
log_info "生成压测报告..."
REPORT_FILE="$PROJECT_ROOT/stress_test_report_$(date +%Y%m%d_%H%M%S).txt"
{
echo "==================================="
echo "压力测试报告"
echo "生成时间: $(date '+%Y-%m-%d %H:%M:%S')"
echo "==================================="
echo ""
echo "--- 数据库统计 ---"
docker exec hfb-mysql mysql -uhfb -psecret hfb_sys -e "
SELECT '用户数' as item, COUNT(*) as count FROM users
UNION ALL SELECT '商品数', COUNT(*) FROM rental_listings
UNION ALL SELECT '订单数', COUNT(*) FROM rental_orders
UNION ALL SELECT '钱包流水', COUNT(*) FROM wallet_ledger;
" 2>/dev/null
echo ""
echo "--- MySQL 性能指标 ---"
docker exec hfb-mysql mysql -uhfb -psecret -e "
SHOW STATUS LIKE 'Threads_connected';
SHOW STATUS LIKE 'Slow_queries';
SHOW STATUS LIKE 'Questions';
" 2>/dev/null
echo ""
echo "--- 表大小统计 ---"
docker exec hfb-mysql mysql -uhfb -psecret hfb_sys -e "
SELECT
table_name AS '表名',
ROUND(((data_length + index_length) / 1024 / 1024), 2) AS '大小(MB)',
table_rows AS '行数'
FROM information_schema.TABLES
WHERE table_schema = 'hfb_sys'
ORDER BY (data_length + index_length) DESC
LIMIT 10;
" 2>/dev/null
} > "$REPORT_FILE"
cat "$REPORT_FILE"
log_info "报告已保存到: $REPORT_FILE"
}
# 执行完整流程
function run_all() {
log_info "执行完整压测流程..."
generate_data
sleep 3
run_stress_test
sleep 2
generate_report
log_info "完整流程执行完成!"
}
# 执行命令
case $COMMAND in
data)
generate_data
;;
test)
run_stress_test
;;
monitor)
monitor_system
;;
clean)
clean_data
;;
report)
generate_report
;;
all)
run_all
;;
*)
log_error "未知命令: $COMMAND"
show_usage
exit 1
;;
esac