Document hot-path data access rules

This commit is contained in:
npc0-hue
2026-09-04 17:41:08 +08:00
parent 44f0b4a44b
commit 4923308fb4
+71
View File
@@ -77,6 +77,77 @@ Frontend and plugin page roots must keep these concerns in fixed directories:
Do not define business structs inside functions. Do not define request/response structs inside handlers. Do not define database models inside migrations. Do not hide shared frontend types inside page components.
## Database, Cache, and Hot-Path Rules
The following rules apply to every backend, plugin, and frontend data-access path. Before adding a database, Redis, or API call, identify the user operation that triggers it, estimate its call count, and keep the hot path bounded and cache-aware.
### SQL and Query Rules
- 禁止在热路径使用 `SELECT *``LIKE '%keyword%'`、无上限分页、深 `OFFSET`、无索引 `ORDER BY`、大范围 `OR` 扫描。
- 列表分页优先使用游标或上次 seen key,不用深页 `OFFSET` 扫全表。
- 写代码前先估算一次玩家操作会触发多少 SQL;一次 NPC 点击、一次菜单打开、一次任务刷新不应触发 N+1 查询。
- 新增或修改高频查询时,必须在注释、README 或 migration 说明里写出预期索引;复杂查询要能用 `EXPLAIN` 验证不会全表扫。
- 不为了“以后可能用”乱加索引;每个索引都会拖慢写入。只给真实查询路径建索引。
### Partitioning Rules
- 只有大体量、追加型、可按时间或赛季归档的数据才考虑分区:登录日志、活动贡献日志、奖励发放日志、审计事件、统计快照。
- 玩家当前进度、权限、队伍当前状态、小配置表通常不分区,优先靠主键和复合索引解决。
- 分区键必须出现在热查询条件里;如果查询不带 `season_id``event_id` 或月份,就不要指望分区救性能。
- 推荐按 `season_id``occurred_at` 月份分区;大型活动可按 `event_id` 做逻辑分片或独立表归档。
- 分区表必须有保留和归档策略,例如赛季结束 90 天后归档明细,只保留玩家摘要和领奖记录。
- 不允许为了临时活动创建永久无限增长表;活动结束必须有归档、压缩或清理计划。
- `audit events` 仅在现有 pre-1.0 规则允许、且有明确未来产品决策时才可引入;本节不授权提前添加审计系统。
### Redis Cache Rules
- Redis key 必须命名清楚:`npc0:<env>:<module>:<type>:<id>`,例如 `npc0:prod:rpg:party:<party_id>`
- 除明确说明的持久状态外,Redis key 必须设置 TTL;没有 TTL 的缓存 key 视为 bug。
- 推荐缓存:服务器在线人数、实例健康、Party 预约、队列、冷却、限流、玩家任务摘要、排行榜快照、NPC 菜单展示数据。
- 不推荐只存在 Redis:经济余额、永久称号、任务最终完成、奖励发放记录、封禁记录、玩家背包关键状态。
- NPC 点击、菜单打开、scoreboard 刷新优先读本地内存缓存或 Redis 快照;cache miss 才异步查数据库。
- 使用 Redis 锁必须带过期时间和唯一 token;释放锁时校验 token,避免误删别人的锁。
- 预约 slot、组队进 Pod、副本实例保留必须有 TTL 和幂等键,玩家掉线后可以过期释放。
- 禁止在生产热路径使用 `KEYS`;需要扫描只能用 `SCAN`,且放在后台任务或运维工具里。
- 多个 Redis 读写要 pipeline 或批量命令;不要对 100 个玩家循环发 100 次小请求。
- 防缓存击穿:热门排行榜、服务器列表、活动状态必须有短 TTL、本地缓存和刷新抖动,不让同一秒所有玩家一起打 DB。
### API and Query-Call Discipline
- 能一次批量 API 获取的数据,不允许拆成多个 API;能一次 SQL 查出来的数据,不允许在循环里逐条查。
- 禁止 N+1:不要先查玩家列表,再对每个玩家查进度、称号、队伍、余额。必须设计批量接口,例如 `loadProgressForPlayers(Collection<UUID>)`
- 同一个事件链里需要的数据必须在入口层集中加载,向下传递数据对象;不要让每个 helper 方法自己偷偷查库。
- 命令、NPC 点击、菜单打开必须加冷却或去抖;玩家连续点击不能造成连续 DB/API 请求。
- scoreboard、TAB、BossBar、Placeholder 刷新必须低频、缓存化、按变更刷新;不要每 tick 重算字符串和查状态。
- 跨服状态汇总要走缓存快照;不要每次 `/servers` 都同步探测所有后端或查数据库。
- 排行榜、全服贡献、统计面板按秒级或分钟级刷新,不做实时逐点击重算。
- 写入优先批量合并:任务进度、活动贡献、统计计数可以先入队,定时批量 flush;关键奖励必须幂等落库。
- 所有重试必须有限次数和退避;禁止无限重试打爆数据库或 Redis。
### Per-Operation Pressure Budget
| 操作 | 允许的热路径行为 | 禁止行为 |
| --- | --- | --- |
| 玩家进服 | 异步批量加载玩家摘要、权限外展示状态、冷却和队伍 | 主线程逐表查询 |
| NPC 点击 | 读本地缓存/Redis 快照,必要时异步查一次 | 同步查库、循环查多个系统 |
| 打开菜单 | 使用已缓存 DTO 一次性渲染 | 每个按钮单独查 API/SQL |
| 任务完成 | 幂等写入队列或事务落库,成功后更新缓存 | 先查后写、多次发奖、不设幂等键 |
| 组队进副本 | 一次性读取 Partyreserve slots,整队调度 | 每个队员单独调度导致打散 |
| 排行榜 | 读 Redis/内存快照,后台定时刷新 | 玩家每打开一次就聚合全表 |
| 每 tick 任务 | 0 DB、0 HTTP、尽量 0 Redis | 遍历全体玩家查远端状态 |
| 关服/重载 | 限时 flush 队列,失败写本地安全日志 | 无限等待数据库导致无法停服 |
### Code-Review Requirements
- 任何新增 DB/Redis/API 代码,都要在 PR/变更说明里写“调用次数预算”:一次命令、一次点击、一次进服、每分钟后台任务各会打多少次 DB/Redis/API。
- 如果一个方法名是 `getXxx``hasXxx``isXxx`,它不能暗中做远程调用;会远程调用的方法必须在名字或注释里明确,例如 `loadXxxAsync`
- 所有 DAO/Repository 方法默认异步或只允许在异步线程调用;主线程调用必须被明确禁止或断言。
- 所有 SQL 使用预编译参数,不拼接玩家输入。
- 连接池必须有最大连接数、连接超时、查询超时和慢查询日志;不能无限开连接。
- 触及经济、奖励、任务完成、活动贡献的代码必须可重试且幂等;Pod 重启、玩家重连、消息重复不能重复发奖。
- 压测前必须用 spark 或日志确认:主线程无阻塞 I/O,DB 慢查询为 0Redis hit rate 达标,单次玩家操作没有 N+1。
- 如果为了赶工无法满足本节要求,宁可不上这个功能,也不要把数据库和服务器一起拖炸。
## Run and Channel Rules
The external run executor must not expose host paths, raw credentials, or direct sockets to plugins or platform_web.