本文目录导读:

这是一个非常经典的支付系统设计问题。“去重表” 本身是防止重复支付的核心手段,但仅仅建一张表是不够的,需要在业务逻辑和数据库层面配合,才能真正防止重复支付。
核心原则是:利用唯一约束 + 事务 + 状态机。
以下是从前端到后端、从表设计到高并发的完整方案:
方案核心:业务唯一键 + 唯一索引
防止重复支付的关键,不是防止“请求重复发”,而是防止“一笔订单被成功扣款多次”。
去重表的核心逻辑:
- 定义业务唯一键: 订单不能直接用数据库自增ID作为去重条件,因为同一个支付请求可能会重试,需要用一个全局唯一的、业务上代表“一次支付尝试”的ID,
商户订单号 + 支付渠道 + 流水号。- 最常用方案: 使用支付渠道返回的流水号(如微信支付单号、支付宝交易号)与你的业务订单号组合作为唯一键。
- 更严谨方案: 自己生成一个全局唯一的
payment_request_id,每次点击“支付”按钮时生成。
- 建立唯一索引: 在去重表上,为这个业务唯一键建立 唯一索引(UNIQUE KEY)。
- 先插后更: 支付成功的核心逻辑变成:
- 第一步:尝试向去重表插入一条记录(包含:
order_id,channel_payment_no,status等)。 - 第二步:如果插入成功(说明这是第一次处理),则执行后续扣款、更新订单状态。
- 第三步:如果插入报错(违背唯一约束),说明已经处理过这笔支付,直接返回“支付成功”或忽略本次请求。
- 第一步:尝试向去重表插入一条记录(包含:
具体实现步骤(伪代码示例)
数据库去重表设计
CREATE TABLE `payment_deduplication` ( `id` bigint unsigned NOT NULL AUTO_INCREMENT, `order_id` varchar(64) NOT NULL COMMENT '你的订单号', `channel_payment_no` varchar(128) NOT NULL COMMENT '支付渠道流水号(如微信/支付宝单号)', `status` tinyint NOT NULL DEFAULT '0' COMMENT '0: 处理中, 1: 已成功', `created_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, `updated_at` datetime ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (`id`), -- 核心:唯一键,保证同一个支付流水号不会重复处理 UNIQUE KEY `uk_order_channel` (`order_id`, `channel_payment_no`), KEY `idx_order_id` (`order_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='支付去重表';
后端支付回调处理逻辑
// 伪代码:支付成功回调处理
public void handlePaymentCallback(PaymentCallback callback) {
String orderId = callback.getOrderId();
String channelPaymentNo = callback.getChannelPaymentNo();
// 1. 先查询去重表(第一层防护,快速判断)
PaymentDeduplication dedup = dedupMapper.selectByOrderAndChannel(orderId, channelPaymentNo);
if (dedup != null) {
// 已处理过,直接返回成功
log.info("重复回调,已处理: {} {}", orderId, channelPaymentNo);
return;
}
// 2. 尝试插入去重记录(核心防护,利用唯一索引)
PaymentDeduplication record = new PaymentDeduplication();
record.setOrderId(orderId);
record.setChannelPaymentNo(channelPaymentNo);
record.setStatus(0); // 处理中
try {
// 如果插入失败,会抛 DuplicateKeyException
dedupMapper.insert(record);
} catch (DuplicateKeyException e) {
// 3. 插入失败,说明另一个线程或请求已经处理了
log.info("唯一约束检测到重复: {} {}", orderId, channelPaymentNo);
return;
}
// 4. 插入成功,开始业务处理
// **注意:这里需要用事务保证原子性**
// 更新订单状态为已支付,增加用户余额等
try {
// 开启事务
transactionTemplate.execute(status -> {
// 4.1 检查订单当前状态是否为“未支付”(二次校验)
Order order = orderMapper.selectForUpdate(orderId); // 悲观锁
if (order.getStatus() != OrderStatus.UNPAID) {
throw new BusinessException("订单状态异常");
}
// 4.2 执行扣款/充值逻辑
accountService.addBalance(order.getUserId(), order.getAmount());
// 4.3 更新订单状态
order.setStatus(OrderStatus.PAID);
orderMapper.updateById(order);
// 4.4 更新去重记录状态为成功
record.setStatus(1);
dedupMapper.updateById(record);
return true;
});
} catch (Exception e) {
// 业务处理失败,需要记录失败原因,并考虑人工介入或补偿
log.error("支付处理失败: {} {}", orderId, channelPaymentNo, e);
// 这里可以记录到失败表,或更新去重表状态为失败
}
}
为什么不能只用“去重表”?(常见陷阱)
仅靠一张去重表和唯一约束,在高并发或业务处理中断时仍可能出现问题。
陷阱1:重复插入但事务未提交
- Thread A 插入去重表成功,事务未提交。
- Thread B 查询去重表,没查到(因为A未提交,默认读未提交会有问题,但如果是读已提交级别,B就看不到)。
- Thread B 也插入成功(唯一索引检查时,因为A未提交,锁可能没生效)。
- 结果:两个请求都通过了去重检查。
- 解决: 使用
SELECT ... FOR UPDATE(悲观锁)或INSERT ... ON DUPLICATE KEY UPDATE(MySQL特有)来保证原子性。推荐:INSERT ON DUPLICATE KEY UPDATE 是更常见的方案。
陷阱2:插入了去重表,但业务逻辑(扣钱、改状态)失败
- 插入去重表成功,但更新订单状态或扣钱时抛异常。
- 用户会得到“支付失败”的提示,但去重表里已经有记录。
- 当用户重试或渠道再次回调,去重表会阻止处理。
- 解决: 去重表的状态需要和业务状态保持最终一致性,通常做法是:去重表+失败重试表+人工对账。
陷阱3:幂等性不仅仅是去重,还包括重试
- 前端网络抖动,用户点击支付按钮多次,生成了多个不同的
channel_payment_no。 - 这些请求通过去重表后,在业务层仍然需要检查订单是否已经被其他请求支付成功。
- 解决: 在去重表之后、业务处理之前,加一层订单状态校验(如
select for update检查订单状态是否为“未支付”)。
更完善的方案:幂等性令牌(Idempotent Key)
在更通用的架构中,可以用一个全局唯一的 幂等键(Idempotent Key) 代替支付流水号。
流程:
- 前端请求支付时,先向后端获取一个幂等键(UUID)。
- 前端使用这个幂等键发起支付请求。
- 后端收到请求,先检查这个幂等键是否存在。
- 如果不存在,存入分布式缓存(Redis)或数据库(带唯一约束),设置过期时间。
- 执行业务逻辑。
- 返回结果。
优点: 即使网络重发多次,只要幂等键相同,请求只执行一次。
最佳实践清单
- 去重表(必选): 表设计包含
订单号 + 渠道流水号的唯一索引,用作最终防线。 - 事务 + 唯一性检查(必选): 使用
INSERT ... ON DUPLICATE KEY UPDATE或SELECT ... FOR UPDATE保证并发安全。 - 状态机校验(必选): 在事务内二次校验订单当前状态是否为“待支付”。
- 分布式锁(可选,用于极高并发): 在处理支付回调前,对订单ID加分布式锁(Redis/ Zookeeper),锁内执行事务。
- 异步对账(必须): 无论如何,系统需要运行一个定时任务,与支付渠道进行日终对账,核销异常的单边账(渠道显示已扣款,但你的系统显示未支付)。
- 前端防重(辅助): 前端按钮点击后置灰、禁用,减少无效请求。
一句话总结: 用 唯一索引 做硬性约束来防止数据重复,用 事务 + 状态机 防止并发写入导致的状态错误,用 异步对账 兜底修复不一致。
版权声明:NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
足球直播
足球直播
足球直播
篮球直播
篮球直播
篮球直播
NBA直播
NBA直播
NBA直播
CBA直播
CBA直播
CBA直播
亚冠直播
亚冠直播
亚冠直播
欧冠直播
欧冠直播
欧冠直播
英超直播
英超直播
英超直播
西甲直播
西甲直播
西甲直播
中超直播
中超直播
中超直播
足球直播
足球直播
足球直播
篮球直播
篮球直播
篮球直播
NBA直播
NBA直播
NBA直播
CBA直播
CBA直播
CBA直播
亚冠直播
亚冠直播
亚冠直播
欧冠直播
欧冠直播
欧冠直播
英超直播
英超直播
英超直播
西甲直播
西甲直播
西甲直播
中超直播
中超直播
中超直播
足球直播
足球直播
足球直播
篮球直播
篮球直播
篮球直播
NBA直播
NBA直播
NBA直播
CBA直播
CBA直播
CBA直播
亚冠直播
亚冠直播
亚冠直播
欧冠直播
欧冠直播
欧冠直播
英超直播
英超直播
英超直播
西甲直播
西甲直播
西甲直播
中超直播
中超直播
中超直播
足球直播
足球直播
足球直播
篮球直播
篮球直播
篮球直播
NBA直播
NBA直播
NBA直播
CBA直播
CBA直播
CBA直播
亚冠直播
亚冠直播
亚冠直播
欧冠直播
欧冠直播
欧冠直播
英超直播
英超直播
英超直播
西甲直播
西甲直播
西甲直播
中超直播
中超直播
中超直播
曼联直播
曼联直播
曼联直播
日职联直播
日职联直播
日职联直播
澳超直播
澳超直播
澳超直播
阿森纳直播
阿森纳直播
阿森纳直播
足球直播
足球直播
足球直播
NBA直播
NBA直播
NBA直播
曼联直播
曼联直播
曼联直播
日职联直播
日职联直播
日职联直播
澳超直播
澳超直播
澳超直播
阿森纳直播
阿森纳直播
阿森纳直播
足球直播
足球直播
足球直播
NBA直播
NBA直播
NBA直播
曼联直播
曼联直播
曼联直播
日职联直播
日职联直播
日职联直播
澳超直播
澳超直播
澳超直播
阿森纳直播
阿森纳直播
阿森纳直播
足球直播
足球直播
足球直播
NBA直播
NBA直播
NBA直播
曼联直播
曼联直播
曼联直播
日职联直播
日职联直播
日职联直播
澳超直播
澳超直播
澳超直播
阿森纳直播
阿森纳直播
阿森纳直播
足球直播
足球直播
足球直播
NBA直播
NBA直播
NBA直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
足球直播
足球直播
足球直播
足球直播
足球直播
英超直播
英超直播
英超直播
英超直播
英超直播
篮球直播
篮球直播
篮球直播
篮球直播
篮球直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
中超直播
中超直播
中超直播
中超直播
中超直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
足球直播
足球直播
足球直播
足球直播
足球直播
英超直播
英超直播
英超直播
英超直播
英超直播
篮球直播
篮球直播
篮球直播
篮球直播
篮球直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
中超直播
中超直播
中超直播
中超直播
中超直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播