嘿,既然你点开了这个话题,我想我们要么是正在被线上的“订单丢单”问题折磨得睡不着觉,要么就是准备着手构建一套能扛住双11流量洪峰的支付中台。不管是哪种情况,我都得先给你泼点冷水:支付系统没有银弹,只有取舍。
今天我不讲那些写在教科书里的“高内聚低耦合”空话,我想带你钻进代码堆里,看看一个成熟的支付库到底是怎么从一张白纸变成坚如磐石的工业级组件的。我会把架构设计、核心痛点、以及那些在压测时才能看到的“坑”,一层层剥开给你看。
一、 为什么我们要有自己的支付库?别急着否定
很多人第一反应是:“直接用支付宝/微信的SDK不就行了吗?为什么要自己搞一套?”
这就好比问:“我有钱,为什么还要办银行卡?”
接入方SDK(Software Development Kit)解决的是“能不能收钱”的问题,而支付库(Payment Library/SDK)解决的是“怎么稳、怎么对、怎么快”的问题。
想象一下这个场景:双11零点,你的订单量瞬间飙升到10万QPS。
- 直接调SDK的问题:每次发起支付,网络往返延迟(RTT)可能在100ms以上。如果用户网络抖动,重试机制没做好,可能导致用户重复付款,或者明明付了钱但订单状态没更新。
- 支付库的价值:它在你的业务层和支付渠道层之间,充当了一个“缓冲带”和“翻译官”。它统一了接口、处理了异步回调、保证了幂等性,并且最关键的是——它能把外部支付动作和本地业务状态解耦。
所以,开发支付库的目标很明确:屏蔽差异、保证一致、提升性能、便于扩展。
二、 架构设计:分层不是摆设,是救命稻草
一个健壮的支付库,架构必须清晰。我见过太多团队把支付逻辑散落在各个业务模块里,最后改需求改到怀疑人生。我们采用经典的四层架构,每一层都有明确的职责边界。
1. 接口层(API Layer):对外友好的门面
这是业务方(订单系统、营销系统等)唯一能接触到的地方。
/**
* 支付服务核心接口
* 注意:所有返回结果必须包含唯一的业务追踪号
*/
public interface PaymentService {
/**
* 发起支付请求
* @param orderRequest 包含金额、订单号、支付方式等
* @return PaymentResult 包含支付通道返回的支付链接或二维码信息
*/
PaymentResult initiatePayment(PaymentRequest orderRequest);
/**
* 查询订单支付状态
* @param merchantOrderId 商户订单号
* @return 支付状态及详情
*/
PaymentQueryResult queryPayment(String merchantOrderId);
/**
* 撤销/关闭订单(针对未支付的订单)
*/
void closeOrder(String merchantOrderId);
/**
* 退款请求
*/
RefundResult refund(RefundRequest refundRequest);
}
关键点:接口要简单。不要暴露内部复杂的渠道映射逻辑。业务方只需要知道“我下单了,结果是什么”。
2. 领域层(Domain Layer):核心规则与状态机
这是支付库的“大脑”。这里不依赖任何外部SDK,只包含纯Java/Kotlin逻辑。
2.1 支付状态机(State Machine)
支付状态是支付库最复杂的部分。一个状态错了,钱就可能对不上。
public enum PaymentStatus {
INIT, // 初始化,等待支付
PENDING, // 已提交给渠道,等待回调
SUCCESS, // 支付成功
CLOSED, // 订单关闭/超时
FAILED, // 支付失败
REFUNDING, // 退款中
REFUNDED, // 退款完成
UNKNOWN // 状态未知,需要人工介入
}
/**
* 状态转换规则定义
* 这是一个简化的状态机模型,实际生产环境建议使用专门的BPMN或JBPM引擎
*/
public class PaymentStateMachine {
private static final Map<PaymentStatus, Set<PaymentStatus>> ALLOWED_TRANSITIONS = new HashMap<>();
static {
// INIT -> PENDING
ALLOWED_TRANSITIONS.computeIfAbsent(PaymentStatus.INIT, k -> new HashSet<>()).add(PaymentStatus.PENDING);
// INIT -> CLOSED (用户主动取消)
ALLOWED_TRANSITIONS.computeIfAbsent(PaymentStatus.INIT, k -> new HashSet<>()).add(PaymentStatus.CLOSED);
// PENDING -> SUCCESS (渠道回调成功)
ALLOWED_TRANSITIONS.computeIfAbsent(PaymentStatus.PENDING, k -> new HashSet<>()).add(PaymentStatus.SUCCESS);
// PENDING -> FAILED (渠道明确拒绝)
ALLOWED_TRANSITIONS.computeIfAbsent(PaymentStatus.PENDING, k -> new HashSet<>()).add(PaymentStatus.FAILED);
// PENDING -> CLOSED (超时关闭)
ALLOWED_TRANSITIONS.computeIfAbsent(PaymentStatus.PENDING, k -> new HashSet<>()).add(PaymentStatus.CLOSED);
// SUCCESS -> REFUNDING (发起退款)
ALLOWED_TRANSITIONS.computeIfAbsent(PaymentStatus.SUCCESS, k -> new HashSet<>()).add(PaymentStatus.REFUNDING);
// REFUNDING -> REFUNDED
ALLOWED_TRANSITIONS.computeIfAbsent(PaymentStatus.REFUNDING, k -> new HashSet<>()).add(PaymentStatus.REFUNDED);
// REFUNDING -> FAILED (退款失败)
ALLOWED_TRANSITIONS.computeIfAbsent(PaymentStatus.REFUNDING, k -> new HashSet<>()).add(PaymentStatus.FAILED);
}
public boolean canTransition(PaymentStatus from, PaymentStatus to) {
Set<PaymentStatus> targets = ALLOWED_TRANSITIONS.get(from);
return targets != null && targets.contains(to);
}
}
为什么要这么折腾?
因为支付系统中,“状态错误”是致命的。如果允许从 INIT 直接跳到 SUCCESS(绕过渠道回调),你的账务系统会乱套。状态机强制你通过合法的步骤流转,哪怕看起来麻烦,但在出问题时,它是你唯一的追溯依据。
2.2 策略模式:多渠道统一接入
支持微信、支付宝、银联、Apple Pay… 每个渠道的API格式、签名算法、回调结构都不一样。
/**
* 支付渠道策略接口
*/
public interface PaymentChannelStrategy {
/**
* 获取支持的支付方式
*/
List<PaymentMethod> supportedMethods();
/**
* 发起预支付
* @param context 支付上下文
* @return 渠道特定的预支付参数(如微信的prepay_id,支付宝的form表单)
*/
PrePayResult prePay(PaymentContext context);
/**
* 处理渠道回调
* @param rawNotify 渠道原始回调数据
* @return 处理结果(成功/失败/需要重试)
*/
NotifyResult handleNotify(Map<String, String> rawNotify);
/**
* 查询订单
*/
QueryResult queryOrder(String merchantOrderId);
/**
* 退款
*/
RefundResult refund(RefundContext context);
/**
* 获取渠道标识,用于路由
*/
String getChannelCode();
}
/**
* 微信支付策略实现(伪代码,展示核心逻辑)
*/
@Component
public class WeChatPayStrategy implements PaymentChannelStrategy {
@Override
public PrePayResult prePay(PaymentContext context) {
// 1. 构建微信统一下单请求参数
WeChatUnifiedOrderRequest request = buildRequest(context);
// 2. 生成签名(注意:微信支付V3版本使用HMAC-SHA256)
String signature = sign(request);
// 3. 调用微信API
String xmlResponse = weChatApiClient.unifiedOrder(request);
// 4. 解析响应,提取 prepay_id
PrePayResult result = new PrePayResult();
result.setChannelPrepayId(parsePrepayId(xmlResponse));
result.setBizParams(buildBizParams(context, result.getChannelPrepayId()));
return result;
}
@Override
public NotifyResult handleNotify(Map<String, String> rawNotify) {
// 1. 验签(至关重要,防止伪造回调)
if (!verifySignature(rawNotify)) {
return NotifyResult.fail("Signature verification failed");
}
// 2. 解密数据(V3版本需要AES-256-GCM解密)
String decryptedData = decryptData(rawNotify.get("resource"));
// 3. 业务处理:更新本地订单状态
// 注意:这里要再次检查幂等性
return paymentService.processSuccessNotify(decryptedData);
}
}
核心思想:业务层调用 PaymentService,PaymentService 根据用户选择的支付方式,通过策略工厂找到对应的 PaymentChannelStrategy 实现。新接入一个渠道,只需要新增一个实现类,无需修改任何现有代码。这就是开闭原则(OCP)在支付库中的完美体现。
3. 数据层(Data Layer):持久化与一致性
支付数据必须可靠。这里主要涉及两个表:
- 支付订单表(PaymentOrder):记录商户订单与渠道订单的映射关系。
- 支付流水表(PaymentTransaction):记录每一次与渠道的交互(请求、回调、查询、退款)。这张表是审计的依据。
数据库设计要点
CREATE TABLE payment_order (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
merchant_order_id VARCHAR(64) NOT NULL COMMENT '商户订单号',
channel_order_id VARCHAR(128) COMMENT '渠道订单号',
amount DECIMAL(12, 2) NOT NULL COMMENT '金额(分)',
currency VARCHAR(3) DEFAULT 'CNY',
status TINYINT NOT NULL COMMENT '支付状态',
channel_code VARCHAR(32) NOT NULL COMMENT '支付渠道',
pay_method VARCHAR(32) COMMENT '支付方式',
notify_count INT DEFAULT 0 COMMENT '回调处理次数',
create_time DATETIME DEFAULT CURRENT_TIMESTAMP,
update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
UNIQUE KEY uk_merchant_order (merchant_order_id),
INDEX idx_status_channel (status, channel_code)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='支付主订单表';
CREATE TABLE payment_transaction (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
payment_order_id BIGINT NOT NULL,
transaction_type TINYINT NOT NULL COMMENT '1:发起支付 2:回调 3:查询 4:退款 5:关闭',
channel_request_data TEXT COMMENT '渠道请求报文',
channel_response_data TEXT COMMENT '渠道响应报文',
status TINYINT COMMENT '交易状态',
error_code VARCHAR(32) COMMENT '错误码',
error_msg VARCHAR(255) COMMENT '错误信息',
create_time DATETIME DEFAULT CURRENT_TIMESTAMP,
INDEX idx_order_id (payment_order_id),
INDEX idx_create_time (create_time)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='支付流水表';
注意:payment_transaction 表是追加写入的,不要修改。这样你可以回溯整个支付过程,排查问题时就像看监控录像一样清晰。
三、 高并发优化:从“能用”到“好用”
架构设计好了,只是具备了“正确性”的基础。但要应对高并发,我们需要在性能上下狠功夫。以下是我在实战中总结的几个关键优化点。
1. 异步化:解耦等待与处理
支付的核心痛点是异步回调。商户系统不能傻等微信回调,也不能一直轮询渠道。
方案:本地消息表 + 消息队列
商户下单 -> 支付库创建订单(INIT) -> 调用渠道预支付 -> 返回支付参数给商户
|
v
支付库发布“支付发起事件”到MQ
|
v
消费者监听MQ,执行后续逻辑(如发送支付成功通知、更新业务状态)
为什么这样做?
- 削峰填谷:双11时,MQ可以缓冲海量的回调请求,避免数据库被打爆。
- 最终一致性:即使瞬间失败,MQ的重试机制也能保证最终处理成功。
代码示例:基于RocketMQ的异步处理
@Service
public class PaymentNotifyConsumer implements RocketMQListener<PaymentNotifyEvent> {
@Autowired
private PaymentService paymentService;
@Autowired
private OrderService orderService;
@Override
public void onMessage(PaymentNotifyEvent event) {
// 1. 业务幂等检查(双重保险)
if (paymentService.isAlreadyProcessed(event.getMerchantOrderId())) {
log.info("Order {} already processed, skip.", event.getMerchantOrderId());
return;
}
try {
// 2. 更新支付订单状态
PaymentResult result = paymentService.handleSuccessNotify(event);
// 3. 更新商户订单状态(例如:将订单状态改为“待发货”)
orderService.updateOrderStatus(event.getMerchantOrderId(), OrderStatus.PAID);
// 4. 发送支付成功通知(短信、App Push等)
notificationService.sendPaymentSuccess(event.getUserId(), event.getAmount());
// 5. 记录成功日志
log.info("Payment success for order: {}", event.getMerchantOrderId());
} catch (Exception e) {
log.error("Failed to process payment notify for order: {}", event.getMerchantOrderId(), e);
// 6. 异常时返回失败,MQ会重试(建议设置最大重试次数,避免无限循环)
throw new RuntimeException("Payment notify processing failed", e);
}
}
}
关键点:MQ的重试间隔要合理(如1s, 5s, 10s, 1min…),并且要有死信队列,避免消息一直失败堆积。
2. 缓存策略:减少数据库压力
支付查询操作非常频繁。用户下单后,可能会多次点击“查询支付状态”。如果每次查都走数据库,数据库很快会被打垮。
方案:Redis缓存 + 本地缓存(Caffeine)
@Service
public class PaymentQueryService {
@Autowired
private RedisTemplate<String, PaymentOrder> redisTemplate;
@Autowired
private PaymentOrderRepository repository;
// 本地缓存,TTL 5秒,用于极高并发下的热点订单
private final LoadingCache<String, PaymentOrder> localCache = Caffeine.newBuilder()
.maximumSize(1000)
.expireAfterWrite(5, TimeUnit.SECONDS)
.build(key -> {
PaymentOrder order = repository.findByMerchantOrderId(key);
return order;
});
public PaymentQueryResult query(String merchantOrderId) {
// 1. 先查本地缓存
PaymentOrder order = localCache.getIfPresent(merchantOrderId);
// 2. 未命中,查Redis
if (order == null) {
order = redisTemplate.opsForValue().get("payment:order:" + merchantOrderId);
}
// 3. 再未命中,查数据库
if (order == null) {
order = repository.findByMerchantOrderId(merchantOrderId);
// 写入Redis,设置过期时间(防止缓存穿透)
if (order != null) {
redisTemplate.opsForValue().set("payment:order:" + merchantOrderId, order, 30, TimeUnit.MINUTES);
localCache.put(merchantOrderId, order);
}
}
// 4. 返回结果
return convertToQueryResult(order);
}
}
为什么两级缓存?
- Redis:分布式缓存,所有节点共享,保证数据一致性(相对)。
- Caffeine:本地缓存,无网络开销,极快。适合热点数据(如某位用户的支付订单)。
注意:缓存更新时要小心缓存穿透和缓存雪崩。对于不存在的订单号,可以缓存一个空对象,或者使用布隆过滤器。
3. 幂等性设计:防止重复支付
这是支付系统中最容易出问题的地方。网络抖动、用户手动重试、MQ重复消费,都可能导致同一笔订单被支付两次。
幂等性实现方案
方案一:数据库唯一索引
在 payment_order 表中,merchant_order_id 是唯一的。如果重复插入,会抛异常。但这只能防止“创建重复订单”,不能防止“重复回调更新状态”。
方案二:状态机检查(推荐)
在处理回调时,先检查当前状态,再决定是否可以转换。
”`java public PaymentResult handleSuccessNotify(String merchantOrderId, String channelOrderNo) {
// 1. 获取订单
PaymentOrder order = getOrderByOrderId(merchantOrderId);
if (order == null) {
throw new BusinessException("Order not found");
}
// 2. 幂等检查:如果已经是SUCCESS,直接返回成功
if (order.getStatus() == PaymentStatus.SUCCESS) {
log.info("Order {} is already paid, ignore duplicate notify.", merchantOrderId);
return PaymentResult.success();
}
// 3. 状态机检查:只有PENDING状态才能转为SUCCESS
if (!stateMachine.canTransition(order.getStatus(), PaymentStatus.SUCCESS)) {
log.warn("Invalid transition from {} to SUCCESS for order {}", order.getStatus(), merchantOrderId);
return PaymentResult.fail("Invalid state transition");
}
// 4. 原子更新:使用乐观锁或CAS
int rows = repository.updateStatus(merchantOrderId, PaymentStatus.SUCCESS, order.getStatus());
