Document hot-path data access rules
This commit is contained in:
@@ -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 |
|
||||||
|
| 任务完成 | 幂等写入队列或事务落库,成功后更新缓存 | 先查后写、多次发奖、不设幂等键 |
|
||||||
|
| 组队进副本 | 一次性读取 Party,reserve 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 慢查询为 0,Redis 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.
|
||||||
|
|||||||
Reference in New Issue
Block a user