支付系统设计

一笔 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能设计完整的支付系统架构