diff --git a/server/docs/PERFORMANCE_AND_QPS_ANALYSIS.md b/server/docs/PERFORMANCE_AND_QPS_ANALYSIS.md index c92911c..bce6248 100644 --- a/server/docs/PERFORMANCE_AND_QPS_ANALYSIS.md +++ b/server/docs/PERFORMANCE_AND_QPS_ANALYSIS.md @@ -1,192 +1,286 @@ -# 游????????QPS ????? +# Game Performance & QPS Analysis (Dice Project) -## ????????????? -| 项? | 说? | -|------|------| -| ????? | Webman (?? Workerman)?驻??| -| HTTP ??? | `http://0.0.0.0:6688` | -| Worker ?? | `cpu_count() * 4`?? 8 ??= 32 ???? | -| ???| MySQL?hinkORM???? max=20/min=2 | -| ?? | Redis?hink-cache ?? `redis`?????max=20/min=2 | -| ??驱?注? | `config/cache.php` ?? `file`????使?该?????走?件?`config/think-cache.php` ?? `redis`?PI ??/???????Redis | +> This document describes the current dice game backend architecture, key endpoints, approximate QPS capacity, and concrete optimization suggestions. +> It is written in plain ASCII to avoid any encoding or garbled-text issues. --- -## ??????????????? -### 2.1 ???????? -| ?? | ???| 主???? | ???????????? | -|------|------|----------|------------------------| -| `POST /api/game/playStart` | ????游????? | ?? DB + Redis + ?? | ?????| -| `POST /api/game/buyLotteryTickets` | ?????| ?????表???+ Redis ?? | ?| -| `GET /api/game/config` | 游??? | ?表 DiceConfig ?????? | ??| -| `GET /api/game/lotteryPool` | ???? | DiceRewardConfig ?? | ?| -| `POST /api/v1/getGameUrl` | ??游?????? | ???? + JWT + Redis | ?| -| `POST /api/v1/getPlayerInfo` | ??信? | ??username ??DicePlayer | ??建? username ????索??| -| `POST /api/v1/getPlayerGameRecord` | 游?记??? | ?? + ??? N+1 ???| ?| -| `POST /api/v1/setPlayerWallet` | ?????? | ???????+ ????| ?| +## 1. Runtime Environment Overview -### 2.2 `playStart` ???请?????????????? -???游??次??????? -**????** +| Item | Description | +|-----------------|-----------------------------------------------------------------------------| +| Framework | Webman (on top of Workerman, long-running PHP workers) | +| Business module | `app/dice` (players, play records, rewards, lottery pool, dashboard, etc.) | +| HTTP endpoint | Typically `http://0.0.0.0:6688` | +| Worker count | Recommended: `cpu_count() * 4` (for example, 8 cores -> 32 workers) | +| Database | MySQL + ThinkORM, with connection pool (for example, max=20 / min=2) | +| Cache | Redis + think-cache, with connection pool (for example, max=20 / min=2) | -1. `DicePlayer::find($playerId)` ?????????2. `DiceRewardConfig::getCachedMinRealEv()` ?????????并??? -3. `LotteryService::getOrCreate()` ??????Redis ??`DicePlayer::find` + `DiceLotteryPoolConfig::where('type',0/1)->find()` 两? -4. `DiceLotteryPoolConfig::find($configId)` ??????ID ????5. **????*? - - `DicePlayRecord::create` - - `DicePlayer::find($playerId)`??次???????? - - `DicePlayer::save` - - ??? `DicePlayerTicketRecord::create`?5 ??? - - `DiceLotteryPoolConfig::where('id')->update(['ev' => Db::raw(...)])` ????? - - `DicePlayerWalletRecord::create` -6. ?????`DicePlayer::find($playerId)`??UserCache::setUser` +Recommended production configuration: -**Redis?* - -- `DiceRewardConfig::getCachedInstance()`?? getCachedByTier ?????????????- `LotteryService::getOrCreate()` ??????家????- `UserCache::setUser()` ??????信???? -??????好????????? `playStart` ???**6? ?DB 访?**??????2 ?find ???? ?update 彩????? ?create??? -?????中???????????????????家???次??? ---- - -## ??????????? - -### 3.1 ??? -1. **???? Worker ??* - - ??? DB ???max=20?orker ??= CPU?4??? 32 ?Worker ????????? 20 ????????????? - - 建???????? `DB_POOL_MAX`?? 32?4??并???MySQL `max_connections` ?????? -2. **`playStart` ???????** - - ??? `DicePlayer::find($playerId)`??????`DicePlayer::find($playerId)`?? - - ???为??????????已??`$player` ???????段??????SELECT?? -3. **`DiceLotteryPoolConfig::where('id', $configId)->update(['ev' => Db::raw(...)])`** - - ??????????? `ev`??并??????????????为???? - - ???????步累???????/次???????使? Redis 计??????步? DB?? -4. **索?** - - `dice_player.username`??dice_play_record.player_id`??dice_play_record.create_time`?? - `dice_player_wallet_record.player_id`??dice_player_ticket_record.player_id` ????索???表????????? - - 建????username` ???索??player_id` + `create_time` ??索????????件????? -### 3.2 ?? - -1. **??驱?????* - - `config/cache.php` ?? `file`???????该??? Cache??走?件?QPS ?? I/O ?????为???? - - 建???产??使? Redis??? `CACHE_MODE=redis` ?think-cache ??default ???? -2. **??????** - - `DiceRewardConfig::getCachedInstance()` 已???????????+ Redis???????好?? - - ?????????????`refreshCache()`???????????? -3. **UserCache ???* - - ?? `getUser`/`setUser` ??AES ?????QPS ?CPU ?????????????????????? CPU ??????????????value?? -### 3.3 ???? N+1 - -- `getPlayerGameRecord`??????`DicePlayRecord`?? `whereIn('id', $playerIds)` ??家并???? - ????已?????家????? N+1?????limit ??????????????????limit ?????100 ????? -- `getPlayerWalletRecord` / `getPlayerTicketRecord` 使? `with(['dicePlayer'])` ??载?????? -### 3.4 ?????? -- Worker ??= CPU?4 ??????????1 ?DB ?????????0 ??????满?? -- 建?????????`DB_POOL_WAIT_TIMEOUT` ?????????????????????????? ---- - -## ????PS ?????????????? -### 4.1 ????件 - -- 8 ??CPU?2 ?Worker??- MySQL/Redis ??????延????????????- ???中??????????otteryService??serCache ??中??? -### 4.2 ?????? -- **playStart**??次约 50?50 ms???????DB/Redis????Worker ?7?0 QPS?2 Worker ?**220?40 QPS**?? - ????DB ???? `dice_lottery_config.ev` ??????????影??*??估???? 200?00 QPS**??- **buyLotteryTickets**??次约 20?0 ms???约 **500?000+ QPS**????????满为??????- **getGameUrl / 平???**?????? DB ????次约 30?0 ms???约 **400?00 QPS**?????走????? -### 4.3 混??? - -??70% ?`playStart`??0% 为?????????? QPS ?? `playStart` ???*综?????250?00 QPS** ???为??????? -??????ab/wrk/k6 ?? `playStart` ?????????? MySQL ?????edis ?中?????????次???????? ---- - -## ?????????? -| 类? | 建? | -|------|------| -| ?? | ??设置 `CACHE_MODE=redis`???使? Redis ? Cache 驱? | -| ???| ??? `DB_POOL_MAX`?? 32?4???? ??Worker ??并???MySQL max_connections | -| 代? | `playStart` ??????已?? `$player`??????`DicePlayer::find($playerId)` | -| ???| ?`username`??player_id`??create_time` ????询?段?索???????| -| ????? | `dice_lottery_config.ev` ???????为 Redis ?? + ??/????? DB????????| -| ??? | ?`playStart`??buyLotteryTickets`??setPlayerWallet` ???????????????MySQL ????????????| -| ??? | 使? ab/wrk/k6 ?`/api/game/playStart` ?????????????P99 延????QPS | +- Use Redis as default cache: in `.env`, set `CACHE_MODE=redis`. +- Align `DB_POOL_MAX` / `REDIS_POOL_MAX` with worker count and MySQL `max_connections`. --- -## ??????????QPS ?P99 +## 2. Key Endpoints and Load Characteristics -1. **??? playStart** - - 使???? JWT????????? token???`POST /api/game/playStart` ??body `direction=0` ??`1`?? - - 示????? URL ??token?? - ```bash - # 使? ab - ab -n 1000 -c 32 -p post_body.json -T application/json -H "token: YOUR_JWT" http://127.0.0.1:6688/api/game/playStart +From the codebase, main game-related endpoints can be roughly grouped into: + +- High-load: multiple DB writes plus Redis and non-trivial business logic. +- Medium-load: some DB writes or heavier DB reads. +- Light: mostly cache reads or simple DB queries. + +### 2.1 Main endpoints (current project) + +Below is a simplified view; exact routes live in the dice module and API controllers: + +| Endpoint | Purpose | Load level | Notes / risks | +|-------------------------------------|--------------------------|------------|----------------------------------------------------------| +| `POST /api/game/playStart` | Start a play / roll | High | Multiple DB + Redis ops, main QPS bottleneck | +| `POST /api/game/buyLotteryTickets` | Buy tickets / chances | Med-High | Multiple table writes, wallet and ticket records | +| `GET /api/game/config` | Game config for frontend | Medium | Depends on `DiceConfig` and cache | +| `GET /api/game/lotteryPool` | Lottery / reward config | Medium | Depends on `DiceLotteryPoolConfig` / `DiceRewardConfig` | +| `POST /api/v1/getGameUrl` | Get game URL / entry | Medium | JWT + Redis auth; light DB usage | +| `POST /api/v1/getPlayerInfo` | Get player info | Medium | Needs index on `dice_player.username` | +| `POST /api/v1/getPlayerGameRecord` | Player history records | Med-High | Risk of N+1 if not using batch/with | +| `POST /api/v1/setPlayerWallet` | Adjust wallet balance | Medium | Wallet + records, must be concurrency-safe | + +For QPS capacity analysis, focus on: + +- `playStart` (core hot path). +- Ticket, wallet, record-related endpoints. - # 使? wrk?? lua ????token ??body? wrk -t4 -c32 -d30s -s play_start.lua http://127.0.0.1:6688/api/game/playStart - ``` - - ?????? Requests per second?PS?? Time per request???? P99 ?工??????? -2. **??????** - - ????????请??????? P99 ?????xx ??????? - - MySQL???????????nnoDB ?????? - - Redis????????????令????? -3. **????** - - ??QPS ????CPU ???????????代???????? - - ??DB ??????????? ???????????????????????? - - ??Redis 延??? ???大 key????令?????????? --- -## ????????????????? +## 3. `playStart` Call Flow and Resource Usage -??*???产??*???????????????项????????QPS ????? -### 7.1 ??????????????????? +The exact implementation is in the dice game logic layer and related services. A typical call flow looks like the following. -| ???| ??? | 说? | -|--------|------|------| -| ?| ?????设置 `CACHE_MODE=redis` | ??走?件?????? I/O ?????? think-cache ??????| -| ?| ??? DB ????`DB_POOL_MAX=32` ??`64` | ?? ??Worker ???32?????????????????| -| ?| ??? Redis ????`REDIS_POOL_MAX=32` | ?Worker ???????? Redis ????????| -| ?| ?? MySQL `max_connections` ????????? | ????署????? ? DB_POOL_MAX ???MySQL ????| +### 3.1 DB access (typical) -**示? .env ????* +1. Load player: + - `DicePlayer::find($playerId)` + - Should be done once per request and reused. -```env -CACHE_MODE=redis -DB_POOL_MAX=32 -DB_POOL_MIN=4 -REDIS_POOL_MAX=32 -``` +2. Load reward EV / minimum real EV: + - `DiceRewardConfig::getCachedMinRealEv()` + - First call hits DB and writes Redis; later calls hit Redis only. -### 7.2 ??????索??????? +3. Get or create lottery pool: + - `LotteryService::getOrCreate()` + - Reads from Redis; if missing, reads from DB (for example, `DiceLotteryPoolConfig::where('type', 0/1)->find()`). -| ???| ??? | 说? | -|--------|------|------| -| ?| ?`dice_player.username` 建??索? | ????etPlayerInfo??oken ??件???username ???????表?????| -| ?| ?`dice_play_record(player_id, create_time)` 建????| 游?记??????计???+?????????页?????| -| ?| 为?水表 `player_id`??create_time` 建索?| ?`dice_player_wallet_record`??dice_player_ticket_record` ???家????????????| +4. Load concrete pool config for this play: + - `DiceLotteryPoolConfig::find($configId)` -**示? SQL????表?????** +5. Persist this play and its side effects (within one or several transactions): + - `DicePlayRecord::create()` (play record) + - `DicePlayer::save()` (balance and state) + - `DicePlayerTicketRecord::create()` (ticket usage) + - `DiceLotteryPoolConfig::where('id')->update(['ev' => Db::raw(...)]` (EV statistics) + - `DicePlayerWalletRecord::create()` (wallet history) + +6. Refresh player cache: + - Reuse or reload player and call `UserCache::setUser($player)`. + +Rough DB count (assuming good caching and no redundant `find` calls): + +- About 3-6 DB queries/updates per `playStart`. +- About 3-6 Redis operations per `playStart`. + +If the code calls `DicePlayer::find($playerId)` multiple times in one request, DB usage and latency will increase accordingly. + +### 3.2 Redis access + +Typical Redis usages in the current design: + +- Reward / pool configs: `DiceRewardConfig::getCachedInstance()`, `getCachedByTier()`, `LotteryService::getOrCreate()`. +- Player cache: `UserCache::getUser()`, `UserCache::setUser()`. +- Optional counters: EV / statistics counters can be done via Redis `INCRBY`, etc. + +Goal: hot paths should hit Redis most of the time, and DB should primarily be for persistence and cold reads. + +--- + +## 4. Main Bottlenecks and Optimizations + +### 4.1 DB connections and slow SQL + +1. Connection pool sizing: + - With 32 workers and `DB_POOL_MAX=20`, connection exhaustion and waiting are likely at higher QPS. + - Recommendation: set `DB_POOL_MAX` to something like 32-64 (depending on CPU and MySQL `max_connections`), and check that MySQL allows enough connections. + +2. Avoid redundant queries: + - In `playStart`, ensure player is loaded once and reused (do not call `DicePlayer::find` multiple times per request). + +3. EV update strategy: + - Repeated `UPDATE dice_lottery_config SET ev = ev - ?` on a hot row causes lock contention. + - Better: + - Accumulate EV deltas in Redis (per pool or per shard). + - Periodic cron job to aggregate Redis deltas back into MySQL in batches. + +4. Index coverage: + - Ensure all high-frequency queries (`username`, `player_id`, `create_time`, status fields) use proper indexes (see section 6). + +### 4.2 Cache and serialization overhead + +1. Use Redis cache driver: + - In `.env`, set `CACHE_MODE=redis`. + - In `config/cache.php`, the default store should be Redis in production. + +2. Player cache encryption: + - If `UserCache` encrypts/decrypts data (for example, AES), CPU cost can be high at large QPS. + - Possible mitigations: + - Keep cached value small (only necessary fields). + - Limit how often `setUser` is called (only on actual state changes). + - Avoid double-encryption or repeated heavy serialization in hot paths. + +### 4.3 N+1 queries + +Endpoints like "get player game record" or wallet histories can easily trigger N+1 patterns: + +- Use `whereIn('player_id', $playerIds)` instead of one query per player. +- Use eager loading (`with(['dicePlayer'])`) for related player info. +- Keep page size (`limit`) moderate (for example, 100 rows or less per page). + +### 4.4 Redis vs DB coordination + +- Ensure worker count, DB pool and Redis pool are balanced: + - If CPU is not saturated but DB or Redis is fully blocked, it is a typical sign of misconfiguration. +- Monitor Redis: + - Use slowlog for heavy commands. + - Watch for large keys or hot keys; consider hash or sharding if needed. + +--- + +## 5. Approximate QPS Capacity (Single Node) + +These are ballpark figures for a single node with something like 8 cores and 32 workers, with MySQL and Redis in the same LAN and the optimizations above applied. Real numbers must come from your own load tests. + +### 5.1 Per-endpoint estimates + +- `playStart`: + - Typical latency: about 50-150 ms. + - Expected QPS: about 200-300 per node. + +- `buyLotteryTickets`: + - Typical latency: about 20-60 ms. + - Expected QPS: about 500-1000+ per node. + +- `getGameUrl` / login-style endpoints: + - Typical latency: about 30-80 ms. + - Expected QPS: about 400-800 per node. + +### 5.2 Mixed traffic scenario + +Example traffic mix: + +- 70% `playStart` +- 30% lighter endpoints (`getGameUrl`, `getPlayerInfo`, and similar) + +Under such a mix, with reasonable indexing and caching: + +- A single 8-core node can typically sustain about 250-400 QPS overall. +- To go significantly beyond that, you will normally use horizontal scaling (multiple app nodes) plus DB/Redis scaling (read replicas, sharding, and so on). + +--- + +## 6. Index and Table Design Recommendations + +### 6.1 Must-have indexes + +| Table | Index | Purpose | +|-----------------------------|-----------------------------------|------------------------------------------| +| `dice_player` | unique(`username`) | Fast lookup by username (login, API) | +| `dice_play_record` | index(`player_id`, `create_time`) | Player record listing plus time sorting | +| `dice_player_wallet_record` | index(`player_id`, `create_time`) | Wallet history listing | +| `dice_player_ticket_record` | index(`player_id`, `create_time`) | Ticket history listing | + +Example SQL: ```sql --- ???????添? ALTER TABLE dice_player ADD UNIQUE INDEX uk_username (username); ALTER TABLE dice_play_record ADD INDEX idx_player_create (player_id, create_time); ALTER TABLE dice_player_wallet_record ADD INDEX idx_player_create (player_id, create_time); ALTER TABLE dice_player_ticket_record ADD INDEX idx_player_create (player_id, create_time); ``` -### 7.3 代?????? DB ??????? -| ???| ??? | 说? | -|--------|------|------| -| ?| `playStart` ??????已?? `$player`????`DicePlayer::find($playerId)` | ?1 ?SELECT??????? `$player` ???????? 1 ?SELECT??| -| ?| `game/config` ???DiceConfig ????| ???????????Redis ?? 1? ???????表????| -| ?| `dice_lottery_config.ev` ?为 Redis ?? + ????? DB | ???? `UPDATE dice_lottery_config SET ev=ev-?`?????????????????N ?????次??| +### 6.2 Large-table strategy -### 7.4 ??????? -| ???| ??? | 说? | -|--------|------|------| -| ?| ?`playStart`??buyLotteryTickets` ???????????? | 便?????????????| -| ?| ???MySQL ??询????>200ms?| ????????????SQL??| -| ?| ???????? QPS ?P99 | ??ab/wrk/k6 ?`playStart` ?????????????????| +For very large history tables (play records, wallet logs, ticket logs), plan ahead: -### 7.5 ?????????? +- Partitioning or sharding (by time or by player ID). +- Archival strategy (move very old data to cold storage or delete). -- ???+ 索????????为?????????????????????? **20%?0%** ??? QPS??- ?? playStart ??2 次?????????? 2 ?round-trip?*playStart ????????? 5%?5%**??- ev ?为 Redis ????并??**playStart** ??????????playStart QPS ????????**10%?0%**??并???????? --- -**说?**???QPS ????????代?????估???????以???线????为???????/???????????`playStart` ?????????????????? +## 7. Load Testing and Metrics + +### 7.1 Load testing `playStart` + +1. Prepare: + - Seed enough test players and balances. + - Obtain valid JWT tokens. + +2. `ab` example: + +```bash +ab -n 2000 -c 32 \ + -p post_body.json \ + -T application/json \ + -H "token: YOUR_JWT" \ + http://127.0.0.1:6688/api/game/playStart +``` + +3. `wrk` example: + +```bash +wrk -t4 -c32 -d60s -s play_start.lua http://127.0.0.1:6688/api/game/playStart +``` + +4. Focus on: + - QPS (Requests per second) + - P95 / P99 latency + - Error rates (HTTP 5xx, timeouts, business failures) + - MySQL / Redis CPU and connection usage + +### 7.2 Typical troubleshooting + +- Low QPS but low CPU: + - Likely DB or Redis connection pool exhaustion or slow queries. +- High P99 spikes: + - Check for large transactions, schema changes, backup jobs, GC, or disk I/O hiccups. +- Redis issues: + - Look for big keys, blocking commands (for example, `KEYS`), or high single-key contention. + +--- + +## 8. Configuration and Code Checklist + +Use this list before load tests or production deployments. + +### 8.1 Configuration + +- `.env`: + - [ ] `CACHE_MODE=redis` + - [ ] `DB_POOL_MAX` / `DB_POOL_MIN` tuned for CPU and MySQL `max_connections` + - [ ] `REDIS_POOL_MAX` large enough for concurrent traffic +- MySQL: + - [ ] `max_connections` greater than or equal to the sum of all app DB pools + - [ ] Slow query log enabled with a reasonable threshold + +### 8.2 Code + +- [ ] `playStart` loads player once and reuses it everywhere. +- [ ] Reward and pool configuration is always accessed via cache; after changes, cache is refreshed. +- [ ] History and wallet endpoints avoid N+1; use batch queries and eager loading. +- [ ] EV and other hot counters use Redis aggregation instead of per-request `UPDATE` on hot rows. + +--- + +## 9. Summary + +- For the current dice project, `playStart` and related lottery, wallet, and ticket flows define the backend QPS ceiling. +- With proper connection pool tuning, Redis caching, indexing, and fewer redundant DB operations, a single 8-core node can typically handle a few hundred QPS of mixed traffic. +- Real capacity must be validated via `ab`, `wrk`, or `k6` load tests against your environment; this document serves as a practical guide for identifying and fixing performance bottlenecks. diff --git a/server/docs/PERFORMANCE_AND_QPS_ANALYSIS_CN.md b/server/docs/PERFORMANCE_AND_QPS_ANALYSIS_CN.md new file mode 100644 index 0000000..6fd8d7a --- /dev/null +++ b/server/docs/PERFORMANCE_AND_QPS_ANALYSIS_CN.md @@ -0,0 +1,293 @@ +# 游戏性能与 QPS 分析(Dice 项目) + +> 本文基于当前骰子游戏(dice)项目的实际代码结构,对后端接口的性能瓶颈、QPS 能力和优化方向做说明,便于后续压测、扩容和排查问题。 + +--- + +## 1. 运行环境与架构概览 + +| 项目 | 说明 | +|----------------|----------------------------------------------------------------------| +| 应用框架 | Webman(基于 Workerman 的常驻内存 PHP 框架) | +| 业务模块 | `app/dice`(玩家、抽奖记录、奖励配置、奖池配置、看板等) | +| HTTP 监听地址 | 通常为 `http://0.0.0.0:6688` | +| Worker 数量 | 推荐:`cpu_count() * 4`(例如 8 核 CPU → 32 个 Worker 进程) | +| 数据库 | MySQL + ThinkORM,使用连接池(例如 max=20 / min=2) | +| 缓存 | Redis + think-cache,使用连接池(例如 max=20 / min=2) | + +**生产环境配置建议:** + +- 在 `.env` 中统一使用 Redis 作为默认缓存:`CACHE_MODE=redis` +- 调整 `DB_POOL_MAX` / `REDIS_POOL_MAX`,使其与 Worker 数量和 MySQL `max_connections` 匹配 + +--- + +## 2. 核心接口与负载特征 + +从当前项目代码来看,游戏相关接口大致可以分为三类: + +- **高负载型**:强依赖 DB + Redis,多次写入或复杂逻辑(QPS 瓶颈主要在这里) +- **中负载型**:部分写 DB 或复杂读 DB +- **轻量型**:读缓存或简单 DB 查询 + +### 2.1 主要接口梳理 + +| 接口 | 用途 | 负载级别 | 说明 / 风险点 | +|-------------------------------------|--------------------|----------|--------------------------------------------------------| +| `POST /api/game/playStart` | 开始一局游戏 / 抽奖 | 高 | 多次 DB + Redis 操作,是整体 QPS 的核心瓶颈 | +| `POST /api/game/buyLotteryTickets` | 购买门票 / 次数 | 中-高 | 多表写入:门票记录、钱包记录等 | +| `GET /api/game/config` | 获取游戏配置 | 中 | 依赖 `DiceConfig`,若无缓存会有全表扫描风险 | +| `GET /api/game/lotteryPool` | 获取奖池 / 奖励配置 | 中 | 依赖 `DiceLotteryPoolConfig` / `DiceRewardConfig` | +| `POST /api/v1/getGameUrl` | 获取游戏 URL | 中 | JWT + Redis 鉴权,DB 较少,Redis 压力较大 | +| `POST /api/v1/getPlayerInfo` | 获取玩家信息 | 中 | 需要 `dice_player.username` 索引,否则易产生慢查询 | +| `POST /api/v1/getPlayerGameRecord` | 获取玩家历史记录 | 中-高 | 若未优化,容易出现 N+1 查询问题 | +| `POST /api/v1/setPlayerWallet` | 调整玩家钱包余额 | 中 | 涉及余额变更与流水写入,需处理好并发与幂等 | + +在进行 QPS 能力评估或性能优化时,优先关注: + +- `playStart` 接口; +- 门票 / 钱包 / 流水相关接口。 + +--- + +## 3. `playStart` 调用链与资源消耗 + +以下是典型的一次 `playStart` 请求在后端的大致流程(抽象描述): + +### 3.1 数据库访问路径(典型情况) + +1. **加载玩家信息** + - 调用 `DicePlayer::find($playerId)` + - 建议:每个请求只查询一次,并在后续逻辑中复用该对象。 + +2. **加载奖励 EV / 最小实际 EV** + - 调用 `DiceRewardConfig::getCachedMinRealEv()` + - 首次会访问 DB 并写入 Redis;后续命中 Redis 即可。 + +3. **获取或创建奖池(LotteryService)** + - 调用 `LotteryService::getOrCreate()`: + - 优先从 Redis 中读取奖池信息; + - 若缓存不存在,则从 DB 中加载:如 `DiceLotteryPoolConfig::where('type', 0/1)->find()`。 + +4. **根据本次玩法加载奖池配置** + - 调用 `DiceLotteryPoolConfig::find($configId)`。 + +5. **核心业务落库(可能在事务中执行)** + - `DicePlayRecord::create()`:写入本次抽奖记录; + - `DicePlayer::save()`:更新玩家余额、状态等; + - `DicePlayerTicketRecord::create()`:写入门票/次数流水; + - `DiceLotteryPoolConfig::where('id')->update(['ev' => Db::raw(...)]`:更新 EV 统计; + - `DicePlayerWalletRecord::create()`:写入钱包流水。 + +6. **刷新玩家缓存** + - 再次使用(或重新加载)玩家信息,调用 `UserCache::setUser($player)` 同步到 Redis。 + +**DB / Redis 调用次数粗略估算:** + +- 在写法合理、缓存命中良好的情况下,每次 `playStart` 大致会产生: + - 3 ~ 6 次 DB 访问(读 + 写); + - 3 ~ 6 次 Redis 操作。 +- 若在同一请求中多次重复调用 `DicePlayer::find($playerId)`,DB 调用次数和耗时都会显著上升。 + +### 3.2 Redis 使用路径 + +在当前设计中,Redis 主要用于: + +- 奖励 / 奖池配置缓存:`DiceRewardConfig::getCachedInstance()`、`getCachedByTier()`、`LotteryService::getOrCreate()` 等; +- 玩家缓存:`UserCache::getUser()`、`UserCache::setUser()`; +- 统计计数:可以使用 Redis `INCRBY` 等命令维护 EV 或其他统计数据。 + +**目标:** 热路径尽量命中 Redis,DB 主要负责落库和冷数据读取。 + +--- + +## 4. 性能瓶颈与优化方向 + +### 4.1 数据库连接与 SQL + +1. **连接池大小** + - 在 32 个 Worker 且 `DB_POOL_MAX=20` 的情况下,高并发时容易出现连接耗尽和等待。 + - 建议:根据 CPU 核心数和目标 QPS,将 `DB_POOL_MAX` 调整到 **32 ~ 64** 左右,并保证 MySQL `max_connections` 足够大。 + +2. **避免重复查询** + - 在 `playStart` 中,玩家信息应只查询一次:`$player = DicePlayer::find($playerId)`,后续逻辑统一使用 `$player`,避免重复 `find`。 + +3. **EV 更新策略** + - 频繁在在线请求中执行 `UPDATE dice_lottery_config SET ev = ev - ?`,会造成该行热点锁竞争; + - 建议: + - 在线请求仅将 EV 变动累加到 Redis 计数器; + - 通过定时任务批量同步 Redis 中的统计数据回 MySQL。 + +4. **索引覆盖** + - 高频查询条件(如 `username`、`player_id`、`create_time`、`status` 等)必须有合适索引(见第 6 节)。 + +### 4.2 缓存与序列化开销 + +1. **统一使用 Redis 缓存驱动** + - `.env` 中设置:`CACHE_MODE=redis`; + - `config/cache.php` 中,生产环境默认缓存驱动应为 Redis。 + +2. **UserCache 加解密成本** + - 若 `UserCache` 中对玩家信息做 AES 加解密,在高 QPS 场景下会占用较多 CPU; + - 可以: + - 精简缓存字段,只缓存必要信息; + - 控制 `setUser` 调用频率,避免无意义重复写入; + - 评估哪些字段确实需要加密,非敏感字段可不加密。 + +### 4.3 N+1 查询问题 + +历史记录、钱包流水等接口非常容易出现 N+1 查询,例如: + +- 循环按玩家逐条查询记录; +- 循环对每条记录再单独查询玩家信息。 + +建议: + +- 使用 `whereIn('player_id', $playerIds)` 做批量查询; +- 使用 `with(['dicePlayer'])` 或 join 预加载关联的玩家信息; +- 控制单页 `limit`,例如不超过 100 条。 + +### 4.4 Redis 与 DB 的整体协同 + +- Worker 数、DB 连接池、Redis 连接池三者要相互匹配: + - 出现 CPU 未打满但 DB/Redis 已经“顶满”的情况,多半是配置不平衡。 +- 对 Redis 的监控要关注: + - 慢日志(慢命令); + - 大 Key 或热点 Key(必要时做拆分或分桶)。 + +--- + +## 5. 单机 QPS 能力(估算) + +> 以下为在常见生产环境(如 8C32G 单机、MySQL/Redis 与应用在同一内网)且做好上述优化后的大致量级,仅用于评估和对比,实际仍需通过压测确认。 + +### 5.1 单接口预估 + +- `playStart` + - 单次耗时:约 50 ~ 150 ms(受 DB/Redis 延迟和业务分支影响); + - 单机 QPS:约 **200 ~ 300**。 + +- `buyLotteryTickets` + - 单次耗时:约 20 ~ 60 ms; + - 单机 QPS:约 **500 ~ 1000+**。 + +- `getGameUrl` / 登录类接口 + - 单次耗时:约 30 ~ 80 ms; + - 单机 QPS:约 **400 ~ 800**。 + +### 5.2 混合业务场景 + +假设: + +- 70% 请求是 `playStart`; +- 30% 请求为 `getGameUrl`、`getPlayerInfo` 等轻 / 中量接口; + +在 8 核 32 Worker、索引和缓存配置合理的前提下: + +- 单机综合 QPS 约可达到 **250 ~ 400 QPS**; +- 若需要显著更高的整体 QPS,应采用多节点水平扩展 + DB/Redis 扩容(读写分离、分库分表等)。 + +--- + +## 6. 索引与表结构建议 + +### 6.1 必备索引 + +| 表名 | 索引 | 用途说明 | +|------------------------------|-----------------------------------|--------------------------------| +| `dice_player` | 唯一索引 `unique(username)` | 通过用户名快速定位玩家 | +| `dice_play_record` | 普通索引 `(player_id, create_time)` | 玩家历史记录分页 + 时间排序 | +| `dice_player_wallet_record` | 普通索引 `(player_id, create_time)` | 钱包流水查询 | +| `dice_player_ticket_record` | 普通索引 `(player_id, create_time)` | 门票流水查询 | + +示例 SQL: + +```sql +ALTER TABLE dice_player ADD UNIQUE INDEX uk_username (username); +ALTER TABLE dice_play_record ADD INDEX idx_player_create (player_id, create_time); +ALTER TABLE dice_player_wallet_record ADD INDEX idx_player_create (player_id, create_time); +ALTER TABLE dice_player_ticket_record ADD INDEX idx_player_create (player_id, create_time); +``` + +### 6.2 大表策略 + +对于体量较大的历史表(抽奖记录、钱包流水、门票流水等),建议提前规划: + +- 分区或分表策略(按时间或按玩家 ID); +- 归档 / 删除过旧数据,例如只保留最近 N 个月的在线查询。 + +--- + +## 7. 压测步骤与指标观测 + +### 7.1 压测 `playStart` + +1. 准备数据: + - 预先创建足够多的测试玩家及初始余额; + - 获取一批可用的 JWT token。 + +2. 使用 `ab` 压测示例: + +```bash +ab -n 2000 -c 32 \ + -p post_body.json \ + -T application/json \ + -H "token: YOUR_JWT" \ + http://127.0.0.1:6688/api/game/playStart +``` + +3. 使用 `wrk` 压测示例: + +```bash +wrk -t4 -c32 -d60s -s play_start.lua http://127.0.0.1:6688/api/game/playStart +``` + +4. 重点关注指标: + - QPS(Requests per second); + - P95 / P99 延迟; + - 错误率(HTTP 5xx、超时、业务失败); + - MySQL / Redis 的 CPU 使用率与连接数。 + +### 7.2 常见问题排查思路 + +1. **QPS 上不去,CPU 却不高** + - 检查 DB / Redis 连接池是否经常“打满”; + - 是否存在慢 SQL(全表扫描、缺索引); + - 是否有外部依赖(如第三方接口)拖慢整体。 + +2. **P99 延迟偶尔飙高** + - 查看是否有大事务 / 大批量更新与压测同时进行; + - 观察磁盘 I/O 和 GC 情况; + - 检查 Redis 是否有大 Key 或阻塞命令(如 `KEYS`)。 + +--- + +## 8. 配置与代码自查清单 + +> 建议在压测前和上线前都按此清单进行一次自查。 + +### 8.1 配置层面 + +- `.env`: + - [ ] `CACHE_MODE=redis` 已开启; + - [ ] `DB_POOL_MAX` / `DB_POOL_MIN` 已根据 CPU 与 MySQL `max_connections` 调整; + - [ ] `REDIS_POOL_MAX` 能够支撑预期并发量。 +- MySQL: + - [ ] `max_connections` 不小于所有应用实例 DB 连接池上限之和; + - [ ] 已开启慢查询日志,并设置了合理阈值。 + +### 8.2 代码层面 + +- [ ] `playStart` 在一次请求中只加载一次玩家信息并全程复用; +- [ ] 奖励/奖池配置统一通过缓存访问,修改后有刷新缓存逻辑(如 `refreshCache()`); +- [ ] 历史记录、钱包流水接口避免 N+1 查询,使用批量查询或预加载; +- [ ] EV 等高频统计字段不直接在在线请求中频繁 `UPDATE`,而是通过 Redis 聚合后批量回写。 + +--- + +## 9. 总结 + +1. 对于当前 dice 项目,`playStart` 以及与之相关的奖池、钱包、门票逻辑,是后端 QPS 能力的关键瓶颈所在。 +2. 通过合理配置 DB/Redis 连接池、充分利用缓存、补齐必要索引、减少重复查询和热点 UPDATE,可以大幅提升单机可承载的 QPS,并降低 P99 延迟。 +3. 真正的容量边界必须通过 `ab` / `wrk` / `k6` 等工具在接近真实数据和流量模型的情况下进行压测验证,本文档主要为定位瓶颈和指导优化提供一个工程实践层面的参考。 + diff --git a/server/db/README_WEIGHT_TEST.md b/server/docs/README_WEIGHT_TEST.md similarity index 100% rename from server/db/README_WEIGHT_TEST.md rename to server/docs/README_WEIGHT_TEST.md