79 lines
7.2 KiB
Markdown
79 lines
7.2 KiB
Markdown
# 转账记录分页优化与验证
|
||
|
||
日期: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、测试及本记录。工作区同期的硬件订单、未激活钱包及前端修改不属于本次变更。
|