Files
pj_service_cloud/pjcloud-wallet/README_RULE_TRANSACTION_PERFORMANCE.md
T
2026-10-22 16:17:46 +08:00

79 lines
7.2 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 转账记录分页优化与验证
日期:2026-10-22。接口:`POST /wallet/wallet-rule-transaction/page-join`。
## 查询链路与瓶颈
管理端 `wallet-rule-transaction/index.vue` 通过 `useTable` 发出一次分页 POST,请求参数中的 `current/size` 绑定 Page,JSON 中的用户、部门、转账类型、退款状态及日期绑定 DTO。网关路由及 `wallet_walletRuleTransaction_view` 权限未变。
原 SQL 将转账表关联两条收支流水,再关联源/目标钱包及类型,最后 `GROUP BY a.id`、排序、分页。精确总数也需要对这组关联结果计数。测试库原始转账表仅有主键,流水没有按业务编号定位的索引。`t.biz_id` 为 VARCHAR,`a.id` 为 BIGINT,隐式比较既阻碍索引定位,也存在大整数浮点精度问题。
原始样本中 43 笔转账各关联两条转账流水;原执行计划出现全表扫描、临时表和 filesort。原查询还依赖非严格 GROUP BY 行为。
## 实现
1. 复用 `PjcloudDefaultDataScopeHandle` 解析权限,形成内部不可变快照。用户/部门筛选和数据权限在 COUNT、LIMIT 前执行;角色权限不会因拆分详情查询而重复获取。
2. 分页查询只取转账 ID,用 EXISTS 判断流水条件,每笔转账天然只有一行。COUNT 保持准确,默认统计不关联流水或钱包。
3. 只为当前页 ID 查询完整转账、钱包类型及一条符合条件的流水,再按分页 ID 顺序组装结果。元数据补齐不随 OFFSET 增长。
4. 同时保留 `t.biz_id = CAST(a.id AS CHAR)` 与 `a.id = CAST(t.biz_id AS SIGNED)`:前者保证精确匹配及流水索引定位,后者允许用户/部门筛出的少量流水反查转账主键。
5. 显式添加 `t.tenant_id = a.tenant_id`。真实租户插件未给 JOIN ON 内的相关子查询注入租户条件,不能只依赖自动改写。
6. 用户资料仍通过原 `getUserDeptByIds` 一次批量获取,ID 去重,空页不调用。筛选使用流水历史部门,展示仍沿用用户服务返回的当前部门。未引入逐行远程调用。
日期参数仍沿用原 LocalDate BETWEEN 边界;结束日零点的行为已回归。原有软删除流水的可见性、缺失流水的 LEFT JOIN 行为及金额字段保持一致。混合部门流水稳定取符合筛选/权限条件的最小流水 ID,不依赖 GROUP BY 任意挑选一行。
并发删除发生在 ID 分页与详情查询之间时,详情已不存在的记录不会以残缺对象返回。COUNT 与记录读取仍遵循原来多语句分页的实时并发语义,未引入跨 Feign 调用的长事务。
## 实测结果
MySQL 8.0.46。复制提供的测试库四张相关表到唯一命名的 `codex_wrt_*` 隔离库,再构造 200,000 笔转账和 1,000,000 条流水;包含真实样本及边界用例后共 **200,055 笔转账、1,001,150 条流水**。源库相关表未写入、未建索引。测试结束删除隔离库。
以下为预热后 3 次中位数,包含 MyBatis 分页 COUNT、ID 查询、详情查询及服务组装。Feign 使用桩,因此不包含真实远程调用、HTTP 网关和生产网络耗时。旧 SQL 使用源库原索引,新 SQL 使用配套新索引;通过隔离库索引可见性切换,避免新增索引干扰旧 SQL 的执行计划。旧 SQL 单条查询超过 5 秒则终止,标为超时,不将其伪装成实测中位数。
| 场景 | 旧查询 | 新查询 |
| --- | ---: | ---: |
| 默认首页,准确总数 189,810 | ≥5,000ms,超时 | 50.9ms |
| 第 5,000 页,每页 10 条 | ≥5,000ms,超时 | 260.3ms |
| 转账类型 | ≥5,000ms,超时 | 7.4ms |
| 退款状态 | ≥5,000ms,超时 | 7.2ms |
| 退回日期 | ≥5,000ms,超时 | 29.8ms |
| 单个用户 | 3.1ms | 3.6ms |
| 部门包含 | 2,285.3ms | 6.5ms |
| 部门排除,匹配 189,610 条 | 3,536.1ms | 737.8ms |
| 部门数据权限 | 未对旧外层权限包装计时 | 8.2ms |
默认详情的 EXPLAIN ANALYZE 显示主表按当前页 10 个主键读取,流水通过 `idx_wt_tenant_bizid_type` 每次定位约两行。用户筛选通过流水用户索引和转账主键查询。退款计数使用对应覆盖索引。深分页只扫描 ID,但测试优化器仍可能选择 skip scan + 排序;表中保留了该实际耗时,没有强制固定索引计划。
准确 COUNT 及大范围部门排除仍随匹配数据量增长;以上为测试环境结果,不代表生产 SLA。需要发布后使用真实租户、日期、筛选和并发验证生产执行计划及端到端耗时。
## 回归验证
- 真实 43 笔样本进行了 **405 组**全部字段、总数及分页的新旧结果对照。
- 20 组筛选/权限用例覆盖用户、部门包含/排除、空/空值参数、混合部门流水、本人权限、本人或部门、无权限、退款日期/状态、钱包 ID 和无结果。
- 验证 ONLY_FULL_GROUP_BY、相邻大整数 ID、带前导零/非法业务编号、重复/缺失/软删除流水、主表软删除、跨租户流水/钱包、跨租户更小流水 ID、日期零点边界、稳定排序、越界页、关闭计数及 1,000 条上限。
- 40 次并发请求验证租户隔离。控制器 MockMvc 验证 POST 参数、JSON 日期/Long ID、分页响应契约;该独立 MVC 测试未启动生产认证或真实 Feign。
- 百万流水压力回归:28 项通过,无失败、无跳过。大数据下还依据生成规则独立计算总数、排序和详情字段,旧查询超时也不会跳过新版正确性断言。
- 最终小数据及钱包导出回归:50 项,49 通过、1 项压力测试在该轮跳过;该压力测试已在上一轮通过。新增并发删除保护用例通过。
## 运行测试
通过调用环境提供 `MYSQL_USER`、`MYSQL_PWD`,不要将凭据写入文件。测试账号需具有创建/删除隔离库的权限。
```sh
export WALLET_RULE_TEST_JDBC_URL='jdbc:mysql://127.0.0.1:33905/mysql?useSSL=false&allowPublicKeyRetrieval=true&serverTimezone=Asia%2FShanghai'
MAVEN_OPTS='-Dfile.encoding=UTF-8' mvn -o -pl pjcloud-wallet/pjcloud-wallet-biz -am \
-Dtest=WalletRuleTransactionServiceTest,WalletRuleTransactionControllerTest,WalletRuleTransactionMySqlTest \
-Dwallet.rule.stress.rows=200000 \
-Dsurefire.failIfNoSpecifiedTests=false -DfailIfNoTests=false \
'-DargLine=-Xmx512m -Dfile.encoding=UTF-8' test
```
不传 `WALLET_RULE_TEST_JDBC_URL` 时自动跳过真实数据库测试;不传 `wallet.rule.stress.rows` 时跳过大数据生成及计时。
## 发布
配套脚本:[`20261022_wallet_rule_transaction_page_index.sql`](../db/20261022_wallet_rule_transaction_page_index.sql),共六个索引:转账表四个,流水表两个。脚本在隔离测试库执行成功;尚未应用到共享测试源表或生产。
钱包服务代码及索引需要配套发布。脚本已说明同名索引核对、逐条建索引及元数据锁注意事项。新代码没有索引时仍能执行,但性能目标依赖这些索引;单独增加索引也不能消除旧查询的隐式数字比较与 GROUP BY。上线后应核对完整列顺序及真实 SQL 执行计划。
本次代码范围为 `WalletRuleTransactionServiceImpl`、对应 Mapper/内部权限快照、SQL、测试及本记录。工作区同期的硬件订单、未激活钱包及前端修改不属于本次变更。