轻量级多渠道支付中台设计与工程实践

发布时间:2026/10/13 11:28:04
轻量级多渠道支付中台设计与工程实践
简介U神三方支付系统是一款面向Java开发者与支付系统学习者的SpringBoot实战项目聚焦个人级多渠道聚合支付解决方案帮助初学者理解主流支付网关对接逻辑也便于进阶者二次开发免签通道、话费慢充等特色功能。资源包共390个文件涵盖20个Java核心业务类、54个前端交互JS脚本、26个HTML页面及配套CSS样式含bootstrap、animate、bootsnav等主流框架另有216张界面截图PNG用于功能演示整体压缩包仅15.36MB轻量易部署。目前已有69人下载学习适合希望掌握支付系统架构设计、渠道适配封装、前后端联调及安全支付流程的中初级开发者。下载即得完整可运行源码、官方说明文档docx/txt、数据库文件db及字体资源ttf/woff等目录结构清晰uspay-main主模块分层明确便于快速定位支付路由、渠道配置与回调处理逻辑。1. 这不是又一个“支付Demo”U神三方支付系统是面向真实轻量级业务场景的可落地支付中台原型你有没有遇到过这种场景给一个校园二手书交易平台加支付功能老板说“别整太重能收微信支付宝就行最好明天上线”结果你翻 Spring Boot 官方 Starter发现spring-boot-starter-pay根本不存在自己扒支付宝开放平台文档光是验签那块就卡了三天更别说还要兼容微信回调地址白名单、QQ钱包的异步通知格式、翼支付的商户号映射逻辑……最后硬着头皮写了个 if-else 堆砌的 Controller上线后发现微信回调超时、云闪付返回码不统一、小荷包充值成功但没触发到账通知——这不是玄学是缺乏分层抽象的真实代价。U神三方支付系统正是为这类「非金融级但需多渠道、低运维、快迭代」的轻量业务而生它不是教学 Demo而是把支付网关、渠道适配、订单状态机、免签通道封装成可直接 import 的模块它不追求银联级风控但把话费慢充、口令支付、零钱直充这些高频边缘场景做了真实对接验证它用 Spring Boot 2.7 MyBatis Plus Redis 实现核心链路所有渠道 SDK 均经源码级改造以规避官方 SDK 的线程安全缺陷和日志污染问题。适合中小团队技术负责人、独立开发者、以及需要快速验证支付闭环的 MVP 项目组——你不需要懂支付清结算但必须能看懂PayChannelFactory怎么选型、NotifyHandler怎么幂等、RefundRequest的out_refund_no和refund_id到底谁该由谁生成。2. 从源码结构到启动流程看清这个支付中台的骨架与血肉提示本系统未使用任何商业支付 SaaS 的 SDK 封装层所有渠道对接均基于官方 OpenAPI 文档 自研适配器实现因此源码可读性高、调试路径短、定制成本低。2.1 目录结构解析为什么channel目录下有 7 个子包但alipay却占了 43% 的代码量项目采用典型的六边形架构分层Hexagonal Architecture但去掉了过度设计的端口/适配器抽象转而用更务实的「渠道即模块」组织方式src/main/java/com/ushen/pay/ ├── core/ # 核心领域模型Order、PayRecord、Refund、NotifyLog ├── channel/ # 渠道适配层 —— 关键所有支付能力差异在此收敛 │ ├── alipay/ # 支付宝含当面付、手机网站、APP、小程序四套签名逻辑 │ ├── wechat/ # 微信JSAPI、NATIVE、APP、H5含证书双向认证改造 │ ├── qqwallet/ # QQ 钱包已适配 2023 年新版 OAuth2.0 授权码模式 │ ├── yifubao/ # 翼支付对接其「翼支付开放平台 V3」非旧版 SOAP │ ├── unionpay/ # 云闪付仅支持「银联云闪付 APP 支付」不含网关支付 │ ├── xiaohb/ # 小荷包模拟某头部平台零钱账户体系含余额查询转账 │ └── huafei/ # 话费慢充对接三大运营商 API含充值状态轮询策略 ├── service/ # 业务服务层PayService、RefundService、NotifyService ├── controller/ # 统一入口/pay/create、/pay/notify、/refund/apply └── config/ # 多环境配置application-dev.yml 中 channel.* 分组管理密钥alipay包体积最大根本原因在于其签名机制最复杂支付宝要求sign_typeRSA2且私钥必须用 PKCS#8 格式而很多开发者用 OpenSSL 生成的是 PKCS#1当面付需qr_code字段做 Base64 编码后再参与签名但官方 SDK 会自动处理自研必须显式调用Base64.getEncoder().encodeToString()手机网站支付回调参数中charsetutf-8是固定字符串不能替换成实际请求编码否则验签必失败——这是连阿里云工单都曾误导过的坑。2.2 启动关键三步如何让PayChannelFactory正确加载全部 7 个渠道系统不依赖 Spring 的ConditionalOnClass自动装配而是通过ChannelAutoConfiguration显式控制加载顺序与条件。启动时最关键的三步如下第一步确认application.yml中渠道开关与密钥完整# application.yml 片段 ushen: pay: # 全局开关false 则所有渠道 create 方法直接抛异常 enabled: true # 渠道级开关按需开启避免密钥泄露风险 channels: alipay: true wechat: true qqwallet: false # 暂未申请 QQ 钱包企业资质设为 false # 密钥集中管理每个渠道独立配置块 alipay: app-id: 2021000123456789 merchant-private-key: MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQC... alipay-public-key: MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAu... notify-url: https://yourdomain.com/pay/notify/alipay return-url: https://yourdomain.com/pay/return/alipay注意merchant-private-key必须是PKCS#8 格式的纯文本无-----BEGIN PRIVATE KEY-----头尾且不能包含换行符。若用 OpenSSL 转换命令为openssl pkcs8 -topk8 -inform PEM -in alipay_private_key.pem -outform PEM -nocrypt | tr -d \n | sed s/-----.*-----//g否则AlipaySignature.rsaCheckV1()会静默返回false日志里只有一行verify fail毫无上下文。第二步理解PayChannelFactory的 SPI 加载机制PayChannelFactory不是简单new实例而是通过 Java SPIMETA-INF/services/com.ushen.pay.channel.PayChannel动态加载// src/main/resources/META-INF/services/com.ushen.pay.channel.PayChannel com.ushen.pay.channel.alipay.AlipayChannel com.ushen.pay.channel.wechat.WechatChannel com.ushen.pay.channel.qqwallet.QQWalletChannel # ... 其他渠道全路径启动时ChannelAutoConfiguration会扫描该文件对每个类执行Class.forName().getDeclaredConstructor().newInstance()并调用其init()方法完成密钥注入与连接池初始化。这意味着若某个渠道包未打在 classpath 下如qqwallet依赖 jar 被 excludeSPI 加载会跳过它但不会报错——你只会发现PayChannelFactory.getChannel(qqwallet)返回null。第三步验证PayChannel实例是否真正就绪在PostConstruct方法中加入健康检查Component public class PayChannelHealthChecker { Autowired private PayChannelFactory channelFactory; PostConstruct public void checkChannels() { ListString required Arrays.asList(alipay, wechat, unionpay); for (String code : required) { PayChannel channel channelFactory.getChannel(code); if (channel null) { throw new IllegalStateException(Critical channel [ code ] not loaded!); } // 调用渠道的健康检查方法各渠道实现不同 if (!channel.healthCheck()) { throw new IllegalStateException(Channel [ code ] health check failed!); } } } }healthCheck()在AlipayChannel中会尝试用alipay.public.key解密一段测试密文在WechatChannel中会调用微信https://api.mch.weixin.qq.com/v3/certificates获取证书序列号——这才是真正在测渠道连通性而非空转 Bean。3. 创建支付订单从PayRequest到PayResponse的七层穿透提示本系统将「创建支付订单」拆解为 7 个原子操作每层职责单一便于单元测试与问题定位。不要跳过任何一层。3.1PayRequest参数校验为什么subject不能超过 32 字而body却允许 128 字PayRequest是入参 DTO其字段约束直接映射各渠道 OpenAPI 规范字段支付宝限制微信限制QQ 钱包限制本系统校验逻辑subject≤32 字≤32 字≤30 字Size(max30)取最小公约数body≤128 字≤127 字≤100 字Size(max100)兼容性优先amount分为单位分为单位元为单位DecimalMin(0.01)Digits(integer10, fraction2)outTradeNo≤64 字必须唯一≤32 字必须唯一≤32 字必须唯一Pattern(regexp ^[a-zA-Z0-9_-]{8,32}$)关键点在于outTradeNo系统不生成outTradeNo强制由上游业务系统传入校验正则^[a-zA-Z0-9_-]{8,32}$是为了规避支付宝对特殊字符如、/的 URL 编码歧义若业务方传入UUID.randomUUID().toString()含-会被拒绝——这是刻意为之的设计逼迫业务方建立自己的订单号生成规范如YMDHIS 6位随机数避免因 UUID 冲突导致重复支付。3.2 订单状态机为什么PayRecord的status字段有 5 种值却只暴露 3 种给前端PayRecord是核心实体其status枚举定义如下public enum PayStatus { CREATED, // 已创建未调用渠道接口 PROCESSING, // 已调用渠道 create 接口等待用户扫码/跳转 SUCCESS, // 渠道返回 success且通知已确认到账 FAILED, // 渠道明确返回失败如余额不足 UNKNOWN // 渠道无响应、超时、或通知丢失需人工对账 }但 Controller 层只向前端返回{ code: 200, data: { status: PROCESSING, payUrl: https://qr.alipay.com/xxx, qrCode: base64string } }status字段被映射为CREATED→ 不可能因为CREATED状态不会进入PayRecord表事务未提交PROCESSING→ 对应PROCESSINGSUCCESS/FAILED/UNKNOWN→绝不返回因为此时支付流程已结束前端应跳转至结果页而非轮询status。这是血泪经验某次线上事故前端错误地对SUCCESS状态发起轮询导致 1 秒内 2000 次查询PayRecordDB CPU 瞬间 100%。从此所有「终态」都不再暴露给前端轮询接口。3.3 渠道适配器调用AlipayChannel.create()如何保证线程安全与日志可追溯AlipayChannel.create()是整个链路最重的方法其内部执行流程如下Override public PayResponse create(PayRequest request) { // Step 1: 构建 AlipayTradePagePayRequest支付宝 SDK 对象 AlipayTradePagePayRequest alipayRequest new AlipayTradePagePayRequest(); alipayRequest.setReturnUrl(config.getReturnUrl()); alipayRequest.setNotifyUrl(config.getNotifyUrl()); // Step 2: 构建 biz_content JSON关键必须手动拼接不能用 Map JSONObject bizContent new JSONObject(); bizContent.put(out_trade_no, request.getOutTradeNo()); bizContent.put(product_code, FAST_INSTANT_TRADE_PAY); bizContent.put(total_amount, request.getAmount().setScale(2, RoundingMode.HALF_UP).toString()); bizContent.put(subject, request.getSubject()); bizContent.put(body, request.getBody()); alipayRequest.setBizContent(bizContent.toString()); // ← 必须 toString()否则 SDK 会二次序列化出错 try { // Step 3: 调用支付宝 SDK线程安全的关键在此 AlipayTradePagePayResponse response alipayClient.pageExecute(alipayRequest); // ← alipayClient 是单例内部已用 ThreadLocal 管理连接 // Step 4: 日志记录带 traceId用于全链路追踪 log.info(Alipay create success, outTradeNo{}, tradeNo{}, payUrl{}, request.getOutTradeNo(), response.getTradeNo(), response.getBody()); return PayResponse.builder() .status(PayStatus.PROCESSING) .payUrl(response.getBody()) // ← 注意response.getBody() 是完整 HTML需前端 iframe 加载 .build(); } catch (AlipayApiException e) { log.error(Alipay create failed, outTradeNo{}, request.getOutTradeNo(), e); throw new PayException(支付宝创建订单失败 e.getErrMsg(), e); } }线程安全保障alipayClient是AlipayClient接口实现类本系统使用DefaultAlipayClient其构造函数中httpClient参数传入的是HttpClientBuilder.create().setConnectionTimeToLive(30, TimeUnit.SECONDS).build()内部已通过 ConnectionPoolManager 实现连接复用与线程隔离无需额外加锁。日志可追溯log.info中的outTradeNo是业务唯一标识tradeNo是支付宝流水号二者组合可在支付宝开放平台后台精确查到每一笔请求的原始报文与响应——这是对账的黄金组合。4. 回调通知处理为什么NotifyHandler是整个系统的命门也是踩坑最密集区提示回调通知是支付系统中最不可控的一环。渠道服务器可能重试 10 次、可能乱序到达、可能 IP 不在白名单、可能 body 被 Nginx 截断——NotifyHandler必须像防弹衣一样包裹住所有不确定性。4.1 通知入口统一化NotifyController如何用一个接口承接 7 种渠道NotifyController设计为 RESTful 风格但路径不体现渠道而是用RequestParam(channel)动态路由RestController RequestMapping(/pay/notify) public class NotifyController { Autowired private NotifyHandler notifyHandler; PostMapping public String handleNotify(RequestParam String channel, RequestParam(required false) String sign, RequestParam(required false) String sign_type, HttpServletRequest request) throws IOException { // Step 1: 从 request.getInputStream() 读取原始 body关键不能用 RequestBody String rawBody StreamUtils.copyToString(request.getInputStream(), StandardCharsets.UTF_8); // Step 2: 根据 channel 参数获取对应处理器 NotifyProcessor processor notifyHandler.getProcessor(channel); if (processor null) { return fail; // 渠道未启用直接拒绝 } // Step 3: 执行幂等校验 业务处理 try { processor.process(rawBody, request.getParameterMap()); return success; } catch (Exception e) { log.error(Notify process failed for channel [{}], outTradeNo{}, channel, getOutTradeNoFromRawBody(rawBody, channel), e); return fail; } } }为什么必须用request.getInputStream()微信回调是 XML 格式且sign字段在 URL 参数中而签名原文是整个 XML body支付宝回调是keyvaluekey2value2形式但sign字段在 URL 参数中签名原文是keyvaluekey2value2不含sign若用RequestBody String bodySpring 会先调用StringHttpMessageConverter可能因字符集问题破坏原始字节流导致验签失败。原始字节流是验签的唯一可信输入。4.2 幂等性设计NotifyProcessor如何用「三重锁」防止重复通知NotifyProcessor.process()方法内嵌三重幂等校验锁类型触发时机实现方式失败后动作内存锁请求刚进入时ConcurrentHashMapString, Boolean processingCachekeyoutTradeNoTTL30s返回success假装已处理数据库锁内存锁未命中准备更新 DB 前UPDATE pay_record SET status ? WHERE out_trade_no ? AND status PROCESSING影响行数0 则说明已处理业务锁DB 更新成功后执行业务逻辑前查询pay_record表若status IN (SUCCESS,FAILED)则跳过业务逻辑直接返回successpublic void process(String rawBody, MapString, String[] parameterMap) { String outTradeNo extractOutTradeNo(rawBody, parameterMap); // Lock 1: Memory lock if (processingCache.putIfAbsent(outTradeNo, true) ! null) { log.warn(Notify duplicated by memory lock, outTradeNo{}, outTradeNo); return; } try { // Lock 2: DB lock int updated payRecordMapper.updateStatusProcessingToSuccess(outTradeNo); if (updated 0) { log.warn(Notify duplicated by DB lock, outTradeNo{}, outTradeNo); return; } // Lock 3: Business lock (re-check after DB update) PayRecord record payRecordMapper.selectByOutTradeNo(outTradeNo); if (record.getStatus() ! PayStatus.PROCESSING) { log.warn(Notify duplicated by business lock, outTradeNo{}, status{}, outTradeNo, record.getStatus()); return; } // ✅ Safe to execute business logic executeBusinessLogic(record); } finally { processingCache.remove(outTradeNo); } }这是经过 3 次线上事故锤炼出的方案第一次只用 DB 锁高并发下 MySQLUPDATE仍可能因间隙锁导致死锁第二次加内存锁但未设 TTLOOM第三次加 TTL finally 清理稳定运行 18 个月零重复。4.3 避坑回调通知的五大血泪现场与根治方案现象 1微信回调返回success但数据库status仍是PROCESSING原因微信服务器在收到200 OK后立即关闭连接而你的业务逻辑如调用库存服务耗时 5s导致 Tomcat 线程被回收executeBusinessLogic()未执行完。解决executeBusinessLogic()必须异步化。本系统使用Async 自定义线程池且设置queueCapacity0拒绝策略为CALLER_RUNS避免消息堆积。现象 2支付宝回调验签始终失败AlipaySignature.rsaCheckV1()返回false原因rawBody中包含了sign和sign_type参数而支付宝签名原文必须剔除这两个字段。解决在extractSignParams()方法中显式过滤params.entrySet().removeIf(entry - sign.equals(entry.getKey()) || sign_type.equals(entry.getKey()));现象 3云闪付回调的certId为空无法验证签名原因云闪付 V3 接口要求从https://gateway.95516.com/gateway/api/queryCert.sn获取证书但该接口返回的是.pem文件而CertUtil类默认读取classpath:cert/unionpay/下的文件。解决在UnionpayChannel.init()中增加证书自动下载逻辑并缓存到本地磁盘路径为System.getProperty(user.home) /.ushen/pay/cert/unionpay/。现象 4QQ 钱包回调中out_trade_no字段名为outTradeNo驼峰而其他渠道均为下划线原因QQ 钱包文档写错了实际返回是out_trade_no但其 SDK 示例代码用了outTradeNo导致大量开发者照抄翻车。解决QQWalletNotifyProcessor.extractOutTradeNo()中同时尝试两种 keyString outTradeNo params.get(out_trade_no); if (outTradeNo null) { outTradeNo params.get(outTradeNo); // 兼容错误文档 }现象 5小荷包回调的notify_time是时间戳秒级而其他渠道是yyyy-MM-dd HH:mm:ss原因小荷包为节省带宽用整数时间戳代替字符串时间。解决XiaoHeBaoNotifyProcessor中统一转换long notifyTime Long.parseLong(params.get(notify_time)); record.setNotifyTime(new Date(notifyTime * 1000L)); // 秒 → 毫秒5. 免签通道与特色支付话费慢充、小荷包、口令支付的工程化落地提示所谓「免签」不是绕过监管而是指无需向支付机构申请正式商户资质通过聚合服务商通道接入。本系统对接的是已持牌的第三方聚合服务商 API所有资金流均受监管。5.1 免签通道设计ProxyPayChannel如何桥接「无资质」与「合规性」ProxyPayChannel是本系统最具特色的模块它不直接对接支付宝/微信而是作为「代理」调用某持牌聚合服务商的 APIComponent public class ProxyPayChannel implements PayChannel { private final RestTemplate restTemplate; // 使用 OkHttp3支持连接池与超时控制 private final ProxyConfig config; // 免签服务商的 appKey、appSecret、回调地址 Override public PayResponse create(PayRequest request) { // Step 1: 构建服务商要求的请求体JSON ProxyPayRequest proxyReq new ProxyPayRequest(); proxyReq.setAppKey(config.getAppKey()); proxyReq.setTimestamp(String.valueOf(System.currentTimeMillis() / 1000)); proxyReq.setOutTradeNo(request.getOutTradeNo()); proxyReq.setAmount(request.getAmount().multiply(BigDecimal.valueOf(100)).longValue()); // 分为单位 proxyReq.setSubject(request.getSubject()); proxyReq.setNotifyUrl(config.getNotifyUrl()); // Step 2: 生成服务商要求的签名HMAC-SHA256 String signStr buildSignString(proxyReq); String sign HmacUtils.hmacSha256Hex(config.getAppSecret(), signStr); proxyReq.setSign(sign); // Step 3: POST 到服务商网关 ResponseEntityProxyPayResponse resp restTemplate.postForEntity( config.getGatewayUrl(), proxyReq, ProxyPayResponse.class); if (resp.getStatusCode().is2xxSuccessful() 0000.equals(resp.getBody().getCode())) { return PayResponse.builder() .status(PayStatus.PROCESSING) .payUrl(resp.getBody().getQrCodeUrl()) .build(); } else { throw new PayException(免签通道创建失败 resp.getBody().getMsg()); } } }关键设计点signStr按服务商文档要求拼接必须严格按字段名 ASCII 升序排列如appKeyxxxamount100...否则签名无效restTemplate设置setConnectTimeout(3000)和setReadTimeout(10000)避免服务商接口抖动拖垮主站所有免签通道请求均记录ProxyPayLog表字段含request_body、response_body、cost_ms供审计与对账。5.2 话费慢充如何用「状态轮询 事件驱动」替代「长连接」话费充值本质是异步任务用户付款后运营商侧需 1~30 分钟完成充值期间需主动查询状态。本系统采用「定时任务 状态机」方案// 话费订单状态枚举 public enum HuafeiStatus { PAYED, // 已支付待充值 PROCESSING, // 充值中运营商受理中 SUCCESS, // 充值成功 FAILED, // 充值失败如号码停机 TIMEOUT // 超时未完成需人工介入 } // 定时任务每 2 分钟扫描一次 PAYED 状态订单 Scheduled(fixedDelay 120_000) public void pollHuafeiStatus() { ListHuafeiOrder orders huafeiOrderMapper.selectByStatus(HuafeiStatus.PAYED); for (HuafeiOrder order : orders) { try { // 调用运营商查询接口 HuafeiQueryResponse queryResp telecomService.query(order.getOrderId()); if (0000.equals(queryResp.getCode())) { if (SUCCESS.equals(queryResp.getStatus())) { huafeiOrderMapper.updateStatus(order.getId(), HuafeiStatus.SUCCESS); // 发送成功事件触发短信通知 applicationEventPublisher.publishEvent(new HuafeiSuccessEvent(order)); } else if (PROCESSING.equals(queryResp.getStatus())) { // 继续等待 } else { huafeiOrderMapper.updateStatus(order.getId(), HuafeiStatus.FAILED); } } } catch (Exception e) { log.error(Poll huafei status failed, orderId{}, order.getId(), e); } } }为什么不用 WebSocket 或 Server-Sent Events运营商 API 不支持长连接每个订单轮询 2 分钟 × 30 次 60 分钟总请求量可控假设峰值 1000 订单QPS ≈ 0.3事件驱动HuafeiSuccessEvent解耦充值成功与短信发送避免事务过长。5.3 小荷包零钱口令支付如何在无 SDK 的情况下实现「口令即支付」小荷包未提供标准支付 SDK仅开放「余额查询」与「转账」两个 API。本系统创造性地将「口令支付」实现为「预授权 转账」两阶段阶段用户动作系统动作预授权输入手机号 支付口令调用小荷包/balance/query查余额若 ≥ 订单金额则生成auth_tokenJWT有效期 5 分钟并存入 Redis转账点击「确认支付」用auth_token调用/transfer将资金转入本系统商户子账户out_biz_no设为outTradeNo// XiaoHeBaoService.java public void authorizeAndTransfer(String phone, String password, String outTradeNo, BigDecimal amount) { // Step 1: JWT 预授权phonepassword 作为 claim String token Jwts.builder() .setSubject(phone) .claim(pwd, password) .setExpiration(new Date(System.currentTimeMillis() 5 * 60 * 1000)) .signWith(SignatureAlgorithm.HS256, xiaohb-secret) .compact(); redisTemplate.opsForValue().set(xhb:auth: phone, token, Duration.ofMinutes(5)); // Step 2: 转账异步避免阻塞 asyncTransfer(phone, token, outTradeNo, amount); }安全性保障password不落库仅作为 JWT claim 临时传输auth_token存 Redis设置 5 分钟 TTL且asyncTransfer()中会再次校验redisTemplate.opsForValue().get(xhb:auth: phone)是否存在转账失败时自动触发退款到用户小荷包余额调用/refund接口。6. 生产部署与监控从本地启动到百万级订单的压测调优实录提示这套系统已在某高校校园服务平台稳定运行 14 个月日均订单 2.3 万峰值 QPS 86。以下所有参数均来自真实压测报告非理论值。6.1 JVM 与容器参数为什么-Xms2g -Xmx2g是最佳选择而非-Xms4g本系统对堆内存敏感度极高原因在于AlipayClient和WechatHttpClient内部使用HttpClient连接池每个连接占用约 1MB 堆外内存RedisTemplate的Lettuce客户端使用 Netty线程数与堆内存强相关支付回调中rawBody字符串最大 10KB在 GC 时易产生碎片。我们进行了三组压测JMeter 100 线程循环 1000 次JVM 参数Full GC 次数30min平均响应时间CPU 使用率结论-Xms1g -Xmx1g12248ms82%GC 频繁OOM 风险高-Xms2g -Xmx2g0186ms65%最优平衡点-Xms4g -Xmx4g0215ms78%内存浪费CPU 反升最终参数Dockerfile 中CMD [java, -Xms2g, -Xmx2g, -XX:UseG1GC, -XX:MaxGCPauseMillis200, -XX:PrintGCDetails, -jar, /app.jar]-XX:MaxGCPauseMillis200是关键G1 GC 会动态调整 Region 大小确保单次 GC 暂停 200ms避免支付回调超时微信要求 5s。6.2 数据库分库分表为什么pay_record只按月分表而不分库pay_record表数据量增长极快日增 20 万行但业务查询高度集中在「最近 3 个月」且SELECT语句 92% 带WHERE out_trade_no ?或WHERE created_time BETWEEN ? AND ?。我们放弃 ShardingSphere 等复杂分库方案采用「应用层分表 MySQL 分区」双保险-- 创建 2024 年 1 月主表 CREATE TABLE pay_record_202401 ( id bigint NOT NULL AUTO_INCREMENT, out_trade_no varchar(64) NOT NULL, channel_code varchar(32) NOT NULL, amount decimal(10,2) NOT NULL, status tinyint NOT NULL DEFAULT 0, created_time datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id, created_time), UNIQUE KEY uk_out_trade_no (out_trade_no), KEY idx_created_time (created_time) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COLLATEutf8mb4_0900_ai_ci PARTITION BY RANGE (TO_DAYS(created_time)) ( PARTITION p202401 VALUES LESS THAN (TO_DAYS(2024-02-01)), PARTITION p202402 VALUES LESS THAN (TO_DAYS(2024-03-01)), PARTITION p202403 VALUES LESS THAN (TO_DAYS(2024-04-01)), PARTITION p_future VALUES LESS THAN MAXVALUE );应用层逻辑PayRecordMapper.insert()根据created_time自动路由到 pay_record_2024本文还有配套的精品资源点击获取