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

7.2 KiB
Raw Permalink Blame History

转账记录分页优化与验证

日期: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,不要将凭据写入文件。测试账号需具有创建/删除隔离库的权限。

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,共六个索引:转账表四个,流水表两个。脚本在隔离测试库执行成功;尚未应用到共享测试源表或生产。

钱包服务代码及索引需要配套发布。脚本已说明同名索引核对、逐条建索引及元数据锁注意事项。新代码没有索引时仍能执行,但性能目标依赖这些索引;单独增加索引也不能消除旧查询的隐式数字比较与 GROUP BY。上线后应核对完整列顺序及真实 SQL 执行计划。

本次代码范围为 WalletRuleTransactionServiceImpl、对应 Mapper/内部权限快照、SQL、测试及本记录。工作区同期的硬件订单、未激活钱包及前端修改不属于本次变更。