#支付系统设计
#一笔 10 万元的重复支付
2023年,我们支付系统出了一个大事故:用户 A 充值 10 万元,扣了两次钱。
问题出在这段代码:
public void processPayment(PaymentRequest request) {
// 1. 查询订单
PaymentOrder order = orderDao.findById(request.getOrderId());
// 2. 检查状态
if (order.getStatus() == PaymentStatus.PAID) {
return; // 已支付
}
// 3. 发起支付
PaymentResult result = paymentGateway.pay(request);
// 4. 更新状态
order.setStatus(PaymentStatus.PAID);
orderDao.update(order);
}并发请求来了,两个线程同时检查到"未支付",都去调用了支付网关,账户被扣了两次钱。
支付系统的核心是:在任何情况下都不能多扣用户的钱。
#二、支付系统核心链路🔴
#2.1 整体架构
┌──────────────────────────────────────────────────────────┐
│ 用户端 │
│ APP / Web / H5 │
└──────────────────────────┬─────────────────────────────────┘
│
↓
┌──────────────────────────────────────────────────────────┐
│ 收银台(Payment UI) │
│ 展示支付方式、收银台统一收口 │
└──────────────────────────┬─────────────────────────────────┘
│
↓
┌──────────────────────────────────────────────────────────┐
│ 支付网关(Payment Gateway) │
│ 路由分发、参数校验、幂等控制 │
└──────────────────────────┬─────────────────────────────────┘
│
↓
┌──────────────────────────────────────────────────────────┐
│ 支付核心(Payment Core) │
│ 订单管理、渠道调用、回调处理 │
└──────────────────────────┬─────────────────────────────────┘
│
┌─────────────────┼─────────────────┐
↓ ↓ ↓
┌────────────────┐ ┌────────────────┐ ┌────────────────┐
│ 支付宝通道 │ │ 微信支付通道 │ │ 银联支付通道 │
└────────────────┘ └────────────────┘ └────────────────┘#2.2 核心流程
@Service
class PaymentService {
@Autowired
private PaymentOrderDao orderDao;
@Autowired
private PaymentGatewayRouter router;
/**
* 创建支付订单
*/
public PaymentOrder createOrder(CreatePaymentRequest request) {
// 1. 幂等检查
PaymentOrder existing = orderDao.findByMerchantOrderNo(
request.getMerchantOrderNo()
);
if (existing != null) {
return existing; // 返回已有订单
}
// 2. 创建订单
PaymentOrder order = new PaymentOrder();
order.setMerchantOrderNo(request.getMerchantOrderNo());
order.setAmount(request.getAmount());
order.setStatus(PaymentStatus.PENDING);
order.setCreateTime(Instant.now());
orderDao.save(order);
return order;
}
/**
* 发起支付
*/
public PaymentResponse initiatePayment(Long orderId, String payChannel) {
// 1. 获取订单
PaymentOrder order = orderDao.findById(orderId);
// 2. 幂等:已支付直接返回
if (order.getStatus() == PaymentStatus.PAID) {
return PaymentResponse.alreadyPaid();
}
// 3. 状态流转
order.setStatus(PaymentStatus.PROCESSING);
order.setPayChannel(payChannel);
order.setUpdateTime(Instant.now());
orderDao.update(order);
// 4. 调用支付渠道
PaymentGateway gateway = router.getGateway(payChannel);
PaymentResult result = gateway.pay(order);
// 5. 保存渠道流水号
order.setChannelOrderNo(result.getChannelOrderNo());
order.setUpdateTime(Instant.now());
orderDao.update(order);
return PaymentResponse.success(result);
}
}#三、幂等设计🔴
#3.1 幂等号机制
@Service
class IdempotentService {
@Autowired
private RedisTemplate<String, String> redisTemplate;
private static final String IDEMPOTENT_PREFIX = "payment:idempotent:";
/**
* 幂等检查
*/
public boolean isProcessed(String idempotentKey) {
String key = IDEMPOTENT_PREFIX + idempotentKey;
return Boolean.TRUE.equals(redisTemplate.hasKey(key));
}
/**
* 标记已处理
*/
public void markProcessed(String idempotentKey, String result) {
String key = IDEMPOTENT_PREFIX + idempotentKey;
redisTemplate.opsForValue().set(key, result, Duration.ofHours(24));
}
}#3.2 数据库乐观锁
@Dao
interface PaymentOrderDao {
@Query("UPDATE payment_order SET status = :newStatus, " +
"version = version + 1 " +
"WHERE id = :id AND status = :currentStatus AND version = :version")
int updateStatusWithOptimisticLock(
@Param("id") Long id,
@Param("currentStatus") String currentStatus,
@Param("newStatus") String newStatus,
@Param("version") int version
);
}
@Service
class PaymentService {
public void updateStatus(Long orderId, String newStatus) {
PaymentOrder order = orderDao.findById(orderId);
int affected = orderDao.updateStatusWithOptimisticLock(
orderId,
order.getStatus(),
newStatus,
order.getVersion()
);
if (affected == 0) {
throw new ConcurrentModificationException("并发更新失败");
}
}
}#四、异步回调处理🔴
#4.1 回调处理流程
@RestController
class PaymentCallbackController {
@Autowired
private PaymentCallbackService callbackService;
@PostMapping("/callback/{channel}")
public String handleCallback(
@PathVariable String channel,
@RequestBody String rawBody,
@RequestParam Map<String, String> params) {
// 1. 记录原始回调
callbackService.recordCallback(channel, rawBody, params);
// 2. 验签
if (!verifySignature(channel, params)) {
return "fail";
}
// 3. 幂等处理
String channelOrderNo = params.get("out_trade_no");
if (callbackService.isProcessed(channelOrderNo)) {
return "success";
}
// 4. 处理回调
try {
callbackService.processCallback(channel, params);
return "success";
} catch (Exception e) {
log.error("回调处理失败", e);
return "fail";
}
}
}#4.2 回调消息队列化
@Service
class PaymentCallbackService {
@Autowired
private RocketMQTemplate mqTemplate;
public void recordCallback(String channel, String rawBody, Map<String, String> params) {
// 记录原始回调
callbackDao.save(new PaymentCallback(channel, rawBody, params));
}
public void processCallback(String channel, Map<String, String> params) {
// 发送到 MQ,异步处理
mqTemplate.asyncSend("payment:callback:topic",
new CallbackMessage(channel, params),
new SendCallback() {
@Override
public void onSuccess(SendResult result) { }
@Override
public void onException(Throwable e) {
// 发送失败,标记重试
markForRetry(channel, params);
}
}
);
}
}
@RocketMQListener(topic = "payment:callback:topic")
class CallbackConsumer {
@Transactional
public void handle(CallbackMessage message) {
String channelOrderNo = message.getParams().get("out_trade_no");
String status = message.getParams().get("trade_status");
PaymentOrder order = orderDao.findByChannelOrderNo(channelOrderNo);
if ("TRADE_SUCCESS".equals(status)) {
order.setStatus(PaymentStatus.PAID);
order.setPaidTime(Instant.now());
} else if ("TRADE_CLOSED".equals(status)) {
order.setStatus(PaymentStatus.CLOSED);
}
orderDao.update(order);
}
}#五、差错处理与对账🟡
#5.1 异步对账
@Service
class ReconciliationService {
@Autowired
private PaymentOrderDao orderDao;
@Autowired
private ChannelApiService channelApi;
/**
* 日终对账:核对订单和渠道流水
*/
@Scheduled(cron = "0 30 4 * * ?") // 每天凌晨 4:30
public void dailyReconciliation(String date) {
// 1. 获取本地订单
List<PaymentOrder> localOrders = orderDao.findByDate(date);
// 2. 获取渠道流水
List<ChannelTransaction> channelTxs = channelApi.fetchDailyTransactions(date);
// 3. 构建对账文件
Map<String, PaymentOrder> orderMap = localOrders.stream()
.collect(Collectors.toMap(PaymentOrder::getChannelOrderNo, o -> o));
// 4. 比对差异
List<ReconcileDiff> diffs = new ArrayList<>();
for (ChannelTransaction tx : channelTxs) {
PaymentOrder order = orderMap.get(tx.getChannelOrderNo());
if (order == null) {
// 本地不存在,渠道存在:长款
diffs.add(new ReconcileDiff(tx, ReconcileType.LONG));
} else if (!order.getAmount().equals(tx.getAmount())) {
// 金额不一致
diffs.add(new ReconcileDiff(tx, order, ReconcileType.AMOUNT_MISMATCH));
} else {
// 匹配成功
order.setReconciled(true);
orderDao.update(order);
}
}
// 5. 处理差异
for (ReconcileDiff diff : diffs) {
handleDiff(diff);
}
}
}#5.2 长款短款处理
@Service
class DiffHandleService {
@Autowired
private PaymentOrderDao orderDao;
@Autowired
private RefundService refundService;
public void handleDiff(ReconcileDiff diff) {
switch (diff.getType()) {
case LONG:
// 长款:渠道有,本地没有
// 可能是本地创建订单失败,需要补充
PaymentOrder补充订单 = createMissingOrder(diff.getChannelTransaction());
refundService.autoRefund(补充订单.getAmount()); // 自动退款
break;
case SHORT:
// 短款:本地有,渠道没有
// 可能是渠道回调失败,需要主动查询
PaymentOrder order = orderDao.findById(diff.getOrderId());
queryChannelStatus(order);
break;
case AMOUNT_MISMATCH:
// 金额不一致
// 需要人工介入
sendAlert(diff);
break;
}
}
}【架构权衡】 支付系统的核心是"一致性"和"可靠性"。任何牺牲一致性的优化都是不可接受的。幂等设计、乐观锁、异步对账是支付系统的三大基石。
#六、面试总结
| 级别 | 期望回答 |
|---|---|
| P5 | 能说出支付基本流程和幂等设计 |
| P6 | 能说出回调处理、对账机制 |
| P7 | 能设计完整的支付系统架构 |