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. 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 ## Run and Channel Rules
The external run executor must not expose host paths, raw credentials, or direct sockets to plugins or platform_web. The external run executor must not expose host paths, raw credentials, or direct sockets to plugins or platform_web.