📖 开场:银行金库
想象你是银行金库的管理员 🏦:
不规范的金库(危险):
客户A:转账100元给客户B
↓
你:好的!
↓
A账户:-100元 ✅
↓
系统崩溃 💀
↓
B账户:没收到钱 ❌
结果:
- A的100元消失了 💀
- B也没收到钱 💀
- 银行亏了100元 ❌
规范的金库(安全):
客户A:转账100元给客户B
↓
你:好的!
↓
1. 记录账务流水(A→B,100元)📝
2. 冻结A账户的100元 🔒
3. A账户:-100元 ✅
4. B账户:+100元 ✅
5. 解冻 🔓
6. 更新流水状态:成功 ✅
中间任何步骤失败:
↓
回滚,钱不会丢 ✅
结果:
- 数据一致 ✅
- 资金安全 ✅
- 可追溯 ✅
这就是支付系统:资金的守护者!
🤔 核心挑战
挑战1:资金安全 🔐
问题:
- 转账失败,钱丢了 💀
- 重复扣款 💀
- 金额篡改 💀
必须:
- 数据一致性 ✅
- 幂等性 ✅
- 不能多扣也不能少扣 ✅
挑战2:高并发 🔥
双11:
- 1秒10万笔支付
- 每笔涉及:
- 余额扣减
- 订单更新
- 流水记录
- 通知发送
必须:
- 高性能 ✅
- 不能阻塞 ✅
挑战3:对账 📊
问题:
- 系统数据:今天收入100万
- 银行对账单:99万
- 差了1万!💀
原因:
- 系统BUG
- 网络故障
- 人为操作
必须:
- 每天对账 ✅
- 发现差异立即处理 ✅
🎯 核心设计
设计1:账户模型 💳
账户类型
账户类型:
1. 用户账户(User Account)
- 余额
- 冻结金额
2. 商家账户(Merchant Account)
- 收入
- 提现
3. 平台账户(Platform Account)
- 手续费收入
4. 担保账户(Escrow Account)
- 资金担保
数据库设计
-- ⭐ 账户表
CREATE TABLE t_account (
id BIGINT PRIMARY KEY COMMENT '账户ID',
user_id BIGINT NOT NULL COMMENT '用户ID',
account_type TINYINT NOT NULL COMMENT '账户类型:1-用户 2-商家 3-平台',
balance DECIMAL(15,2) NOT NULL DEFAULT 0 COMMENT '余额',
frozen_amount DECIMAL(15,2) NOT NULL DEFAULT 0 COMMENT '冻结金额',
total_income DECIMAL(15,2) NOT NULL DEFAULT 0 COMMENT '累计收入',
total_expense DECIMAL(15,2) NOT NULL DEFAULT 0 COMMENT '累计支出',
version INT NOT NULL DEFAULT 0 COMMENT '版本号(乐观锁)',
create_time DATETIME NOT NULL,
update_time DATETIME,
UNIQUE KEY uk_user_type (user_id, account_type),
INDEX idx_balance (balance)
) COMMENT '账户表';
-- ⭐ 账务流水表(核心)
CREATE TABLE t_account_log (
id BIGINT PRIMARY KEY COMMENT '流水ID',
trans_no VARCHAR(64) NOT NULL COMMENT '交易流水号(唯一)',
from_account_id BIGINT COMMENT '出账账户ID',
to_account_id BIGINT COMMENT '入账账户ID',
amount DECIMAL(15,2) NOT NULL COMMENT '金额',
trans_type TINYINT NOT NULL COMMENT '交易类型:1-充值 2-提现 3-转账 4-消费',
biz_type VARCHAR(50) COMMENT '业务类型:order/refund/withdraw',
biz_id VARCHAR(64) COMMENT '业务ID(订单号等)',
status TINYINT NOT NULL COMMENT '状态:1-处理中 2-成功 3-失败',
remark VARCHAR(500) COMMENT '备注',
create_time DATETIME NOT NULL,
update_time DATETIME,
UNIQUE KEY uk_trans_no (trans_no),
INDEX idx_from_account (from_account_id, create_time),
INDEX idx_to_account (to_account_id, create_time),
INDEX idx_biz (biz_type, biz_id)
) COMMENT '账务流水表';
设计2:支付流程 💸
订单支付流程
用户购买商品(100元):
1. 创建订单(待支付)
↓
2. 用户点击"支付"
↓
3. ⭐ 生成支付单(payment_no,幂等性保证)
↓
4. 检查余额(100元够不够)
↓
5. ⭐ 冻结金额(100元)
↓
6. 调用支付网关(微信/支付宝)
↓
7. 支付网关返回:支付成功
↓
8. ⭐ 扣减余额(100元)
↓
9. ⭐ 记录账务流水
↓
10. 更新订单状态:已支付
↓
11. ⭐ 异步通知商家
↓
12. 发货 ✅
任何步骤失败:
↓
回滚,释放冻结金额 ✅
代码实现
@Service
public class PaymentService {
@Autowired
private AccountService accountService;
@Autowired
private PaymentLogMapper paymentLogMapper;
@Autowired
private OrderService orderService;
@Autowired
private PaymentGateway paymentGateway;
/**
* ⭐ 支付订单
*/
@Transactional(rollbackFor = Exception.class)
public PaymentResult pay(Long userId, Long orderId, BigDecimal amount) {
// 1. 查询订单
Order order = orderService.getById(orderId);
if (order == null) {
throw new OrderNotFoundException("订单不存在");
}
if (order.getStatus() != OrderStatus.WAIT_PAY) {
throw new OrderStatusException("订单状态不是待支付");
}
// ⭐ 2. 生成支付单号(幂等性)
String paymentNo = generatePaymentNo(orderId);
// 检查支付单是否已存在(防止重复支付)
PaymentLog existingLog = paymentLogMapper.selectByPaymentNo(paymentNo);
if (existingLog != null) {
if (existingLog.getStatus() == PaymentStatus.SUCCESS) {
// 已支付成功,直接返回
return PaymentResult.success(existingLog);
} else if (existingLog.getStatus() == PaymentStatus.PROCESSING) {
// 正在处理中,拒绝重复请求
throw new PaymentProcessingException("支付正在处理中");
}
}
// 3. 创建支付流水(状态:处理中)
PaymentLog paymentLog = new PaymentLog();
paymentLog.setPaymentNo(paymentNo);
paymentLog.setUserId(userId);
paymentLog.setOrderId(orderId);
paymentLog.setAmount(amount);
paymentLog.setStatus(PaymentStatus.PROCESSING);
paymentLog.setCreateTime(new Date());
paymentLogMapper.insert(paymentLog);
try {
// ⭐ 4. 冻结金额
accountService.freeze(userId, amount, paymentNo);
// ⭐ 5. 调用支付网关
PaymentGatewayResponse response = paymentGateway.pay(paymentNo, amount);
if (response.isSuccess()) {
// ⭐ 6. 扣减余额(解冻并扣除)
accountService.deduct(userId, amount, paymentNo);
// ⭐ 7. 更新支付流水状态:成功
paymentLog.setStatus(PaymentStatus.SUCCESS);
paymentLog.setGatewayTradeNo(response.getTradeNo());
paymentLog.setUpdateTime(new Date());
paymentLogMapper.updateById(paymentLog);
// 8. 更新订单状态:已支付
orderService.updateStatus(orderId, OrderStatus.PAID);
// ⭐ 9. 异步通知商家
notifyMerchant(orderId);
return PaymentResult.success(paymentLog);
} else {
// 支付失败
throw new PaymentFailedException(response.getErrorMsg());
}
} catch (Exception e) {
// ⭐ 失败,释放冻结金额
accountService.unfreeze(userId, amount, paymentNo);
// 更新支付流水状态:失败
paymentLog.setStatus(PaymentStatus.FAILED);
paymentLog.setErrorMsg(e.getMessage());
paymentLog.setUpdateTime(new Date());
paymentLogMapper.updateById(paymentLog);
throw e;
}
}
/**
* 生成支付单号(幂等性保证)
*/
private String generatePaymentNo(Long orderId) {
// 格式:PAY + 订单ID + 时间戳
return "PAY" + orderId + System.currentTimeMillis();
}
/**
* 异步通知商家
*/
private void notifyMerchant(Long orderId) {
// 发送MQ消息
// ...
}
}
设计3:账户服务 💳
@Service
public class AccountService {
@Autowired
private AccountMapper accountMapper;
@Autowired
private AccountLogMapper accountLogMapper;
/**
* ⭐ 冻结金额
*/
@Transactional(rollbackFor = Exception.class)
public void freeze(Long userId, BigDecimal amount, String transNo) {
// 1. 查询账户
Account account = accountMapper.selectByUserId(userId);
if (account == null) {
throw new AccountNotFoundException("账户不存在");
}
if (account.getBalance().compareTo(amount) < 0) {
throw new InsufficientBalanceException("余额不足");
}
// ⭐ 2. 冻结金额(乐观锁)
int rows = accountMapper.freeze(account.getId(), amount, account.getVersion());
if (rows == 0) {
throw new ConcurrentUpdateException("账户更新失败,请重试");
}
// 3. 记录账务流水
AccountLog log = new AccountLog();
log.setTransNo(transNo);
log.setAccountId(account.getId());
log.setAmount(amount);
log.setTransType(TransType.FREEZE);
log.setStatus(TransStatus.SUCCESS);
log.setCreateTime(new Date());
accountLogMapper.insert(log);
}
/**
* ⭐ 解冻并扣除金额
*/
@Transactional(rollbackFor = Exception.class)
public void deduct(Long userId, BigDecimal amount, String transNo) {
Account account = accountMapper.selectByUserId(userId);
// ⭐ 解冻并扣除(乐观锁)
int rows = accountMapper.deduct(account.getId(), amount, account.getVersion());
if (rows == 0) {
throw new ConcurrentUpdateException("账户更新失败,请重试");
}
// 记录账务流水
AccountLog log = new AccountLog();
log.setTransNo(transNo);
log.setAccountId(account.getId());
log.setAmount(amount);
log.setTransType(TransType.DEDUCT);
log.setStatus(TransStatus.SUCCESS);
log.setCreateTime(new Date());
accountLogMapper.insert(log);
}
/**
* ⭐ 释放冻结金额
*/
@Transactional(rollbackFor = Exception.class)
public void unfreeze(Long userId, BigDecimal amount, String transNo) {
Account account = accountMapper.selectByUserId(userId);
// ⭐ 释放冻结金额(乐观锁)
int rows = accountMapper.unfreeze(account.getId(), amount, account.getVersion());
if (rows == 0) {
throw new ConcurrentUpdateException("账户更新失败,请重试");
}
// 记录账务流水
AccountLog log = new AccountLog();
log.setTransNo(transNo);
log.setAccountId(account.getId());
log.setAmount(amount);
log.setTransType(TransType.UNFREEZE);
log.setStatus(TransStatus.SUCCESS);
log.setCreateTime(new Date());
accountLogMapper.insert(log);
}
}
Mapper实现:
@Mapper
public interface AccountMapper {
/**
* ⭐ 冻结金额(乐观锁)
*/
@Update("UPDATE t_account " +
"SET balance = balance - #{amount}, " +
" frozen_amount = frozen_amount + #{amount}, " +
" version = version + 1 " +
"WHERE id = #{accountId} " +
" AND version = #{version} " +
" AND balance >= #{amount}")
int freeze(@Param("accountId") Long accountId,
@Param("amount") BigDecimal amount,
@Param("version") Integer version);
/**
* ⭐ 解冻并扣除金额(乐观锁)
*/
@Update("UPDATE t_account " +
"SET frozen_amount = frozen_amount - #{amount}, " +
" total_expense = total_expense + #{amount}, " +
" version = version + 1 " +
"WHERE id = #{accountId} " +
" AND version = #{version} " +
" AND frozen_amount >= #{amount}")
int deduct(@Param("accountId") Long accountId,
@Param("amount") BigDecimal amount,
@Param("version") Integer version);
/**
* ⭐ 释放冻结金额(乐观锁)
*/
@Update("UPDATE t_account " +
"SET balance = balance + #{amount}, " +
" frozen_amount = frozen_amount - #{amount}, " +
" version = version + 1 " +
"WHERE id = #{accountId} " +
" AND version = #{version} " +
" AND frozen_amount >= #{amount}")
int unfreeze(@Param("accountId") Long accountId,
@Param("amount") BigDecimal amount,
@Param("version") Integer version);
}
设计4:异步通知 🔔
问题
支付成功后:
↓
通知商家发货
↓
商家服务器宕机 💀
↓
通知失败 ❌
必须:
- 重试机制 ✅
- 确保商家收到通知 ✅
解决方案:MQ + 重试
支付成功:
↓
发送MQ消息
↓
消费者:调用商家回调接口
↓
成功 → ACK ✅
失败 → 重试(最多3次)
↓
3次都失败 → 人工介入
代码实现:
@Service
public class PaymentNotifyService {
@Autowired
private RocketMQTemplate rocketMQTemplate;
/**
* ⭐ 发送支付成功通知
*/
public void sendPaymentNotify(Long orderId, String paymentNo) {
PaymentNotifyMessage message = new PaymentNotifyMessage();
message.setOrderId(orderId);
message.setPaymentNo(paymentNo);
message.setRetryCount(0);
// 发送到MQ
rocketMQTemplate.syncSend("payment-notify-topic", message);
}
}
/**
* ⭐ 支付通知消费者
*/
@Component
@RocketMQMessageListener(
topic = "payment-notify-topic",
consumerGroup = "payment-notify-consumer"
)
public class PaymentNotifyConsumer implements RocketMQListener<PaymentNotifyMessage> {
@Autowired
private MerchantService merchantService;
@Autowired
private RocketMQTemplate rocketMQTemplate;
@Override
public void onMessage(PaymentNotifyMessage message) {
try {
// ⭐ 调用商家回调接口
boolean success = merchantService.notifyPaymentSuccess(
message.getOrderId(),
message.getPaymentNo()
);
if (success) {
System.out.println("⭐ 商家通知成功");
} else {
// 通知失败,重试
retry(message);
}
} catch (Exception e) {
e.printStackTrace();
// 异常,重试
retry(message);
}
}
/**
* 重试
*/
private void retry(PaymentNotifyMessage message) {
int retryCount = message.getRetryCount();
if (retryCount < 3) {
// 重试次数未达到上限,继续重试
message.setRetryCount(retryCount + 1);
// 延迟发送(指数退避:1分钟、2分钟、4分钟)
int delayLevel = getDelayLevel(retryCount + 1);
rocketMQTemplate.syncSend(
"payment-notify-topic",
MessageBuilder.withPayload(message).build(),
3000,
delayLevel
);
System.out.println("⭐ 重试第" + (retryCount + 1) + "次");
} else {
// 重试次数达到上限,记录到数据库,人工处理
System.out.println("⭐ 重试失败,需要人工处理");
// saveToFailedLog(message);
}
}
/**
* 获取延迟级别
*/
private int getDelayLevel(int retryCount) {
// RocketMQ延迟级别:1=1分钟, 2=5分钟, 3=10分钟
switch (retryCount) {
case 1: return 1; // 1分钟
case 2: return 2; // 5分钟
case 3: return 3; // 10分钟
default: return 3;
}
}
}
设计5:对账系统 📊
为什么要对账?
问题:
- 系统记录:今天收入100万
- 支付宝对账单:99.5万
- 差了5000元!💀
原因可能:
1. 系统BUG(少记录了)
2. 网络故障(支付成功但通知丢失)
3. 退款(系统未更新)
4. 人工操作(手动调整)
必须:
每天对账,发现差异 ✅
对账流程
对账流程(每天凌晨1点执行):
1. 下载支付宝对账单(昨天的)
↓
2. 查询系统账务流水(昨天的)
↓
3. 逐条比对:
- 金额是否一致
- 状态是否一致
↓
4. 生成差异报表
↓
5. 发送给财务人员 ✅
代码实现:
@Service
public class ReconciliationService {
@Autowired
private AlipayClient alipayClient;
@Autowired
private AccountLogMapper accountLogMapper;
/**
* ⭐ 对账(定时任务,每天凌晨1点)
*/
@Scheduled(cron = "0 0 1 * * ?")
public void reconcile() {
// 昨天的日期
LocalDate yesterday = LocalDate.now().minusDays(1);
System.out.println("⭐ 开始对账:" + yesterday);
// 1. 下载支付宝对账单
List<AlipayBill> alipayBills = alipayClient.downloadBill(yesterday);
// 2. 查询系统账务流水
List<AccountLog> accountLogs = accountLogMapper.selectByDate(yesterday);
// 3. 比对
List<ReconciliationDiff> diffs = compare(alipayBills, accountLogs);
if (diffs.isEmpty()) {
System.out.println("⭐ 对账成功,无差异");
} else {
System.out.println("⭐ 对账发现差异:" + diffs.size() + "条");
// 4. 保存差异记录
saveDiffs(diffs);
// 5. 发送报警
sendAlert(diffs);
}
}
/**
* 比对账单
*/
private List<ReconciliationDiff> compare(List<AlipayBill> alipayBills,
List<AccountLog> accountLogs) {
List<ReconciliationDiff> diffs = new ArrayList<>();
// 将系统流水转换为Map(key=交易流水号)
Map<String, AccountLog> logMap = accountLogs.stream()
.collect(Collectors.toMap(AccountLog::getTransNo, log -> log));
// 逐条比对
for (AlipayBill bill : alipayBills) {
String transNo = bill.getTransNo();
AccountLog log = logMap.get(transNo);
if (log == null) {
// 系统中没有这条记录
ReconciliationDiff diff = new ReconciliationDiff();
diff.setTransNo(transNo);
diff.setType(DiffType.MISSING_IN_SYSTEM);
diff.setAlipayAmount(bill.getAmount());
diffs.add(diff);
} else {
// 比对金额
if (!bill.getAmount().equals(log.getAmount())) {
ReconciliationDiff diff = new ReconciliationDiff();
diff.setTransNo(transNo);
diff.setType(DiffType.AMOUNT_DIFF);
diff.setAlipayAmount(bill.getAmount());
diff.setSystemAmount(log.getAmount());
diffs.add(diff);
}
// 标记已比对
logMap.remove(transNo);
}
}
// 系统中多余的记录
for (AccountLog log : logMap.values()) {
ReconciliationDiff diff = new ReconciliationDiff();
diff.setTransNo(log.getTransNo());
diff.setType(DiffType.MISSING_IN_ALIPAY);
diff.setSystemAmount(log.getAmount());
diffs.add(diff);
}
return diffs;
}
/**
* 发送报警
*/
private void sendAlert(List<ReconciliationDiff> diffs) {
// 发送钉钉通知/邮件
// ...
}
}
🎓 面试题速答
Q1: 支付系统如何保证幂等性?
A: 支付单号 + 状态校验:
// 生成唯一支付单号
String paymentNo = "PAY" + orderId + timestamp;
// 检查支付单是否已存在
PaymentLog log = paymentLogMapper.selectByPaymentNo(paymentNo);
if (log != null && log.getStatus() == PaymentStatus.SUCCESS) {
return PaymentResult.success(log); // 已支付,直接返回
}
关键:支付单号唯一 + 数据库唯一索引
Q2: 如何防止余额超扣?
A: 冻结金额 + 乐观锁:
-- 冻结金额
UPDATE t_account
SET balance = balance - #{amount},
frozen_amount = frozen_amount + #{amount},
version = version + 1
WHERE id = #{accountId}
AND version = #{version}
AND balance >= #{amount} -- ⭐ 余额充足检查
流程:
- 冻结金额
- 调用支付网关
- 成功:扣除冻结金额
- 失败:释放冻结金额
Q3: 账务流水有什么作用?
A: 三大作用:
-
资金追溯:
- 每笔交易都有记录
- 可追溯资金流向
-
对账:
- 与支付宝对账单比对
- 发现差异
-
审计:
- 监管要求
- 防止资金风险
必须:每笔交易都记录流水
Q4: 异步通知如何保证可靠?
A: MQ + 重试机制:
支付成功 → 发送MQ消息
↓
消费者:调用商家回调
↓
失败 → 重试(1分钟、5分钟、10分钟)
↓
3次都失败 → 人工处理
关键:
- 消息持久化(MQ不丢消息)
- 重试机制(指数退避)
- 最终人工兜底
Q5: 如何对账?
A: 下载对账单 + 逐条比对:
每天凌晨1点:
1. 下载支付宝对账单(昨天)
2. 查询系统账务流水(昨天)
3. 逐条比对:
- 金额是否一致
- 状态是否一致
4. 发现差异 → 报警 → 人工处理
Q6: 支付系统如何设计才安全?
A: 五大保障:
- 账务流水:每笔交易都记录
- 幂等性:防止重复扣款
- 乐观锁:防止并发问题
- 冻结金额:防止超扣
- 对账:每天对账,发现差异
🎬 总结
支付系统核心设计
┌────────────────────────────────────┐
│ 1. 账户模型 │
│ - 余额 + 冻结金额 │
│ - 乐观锁 │
└────────────────────────────────────┘
┌────────────────────────────────────┐
│ 2. 账务流水(核心)⭐ │
│ - 每笔交易都记录 │
│ - 可追溯 │
└────────────────────────────────────┘
┌────────────────────────────────────┐
│ 3. 幂等性 │
│ - 支付单号唯一 │
│ - 状态校验 │
└────────────────────────────────────┘
┌────────────────────────────────────┐
│ 4. 异步通知 │
│ - MQ + 重试 │
│ - 最终一致性 │
└────────────────────────────────────┘
┌────────────────────────────────────┐
│ 5. 对账系统 ⭐ │
│ - 每天对账 │
│ - 发现差异 │
└────────────────────────────────────┘
🎉 恭喜你!
你已经完全掌握了支付系统的设计!🎊
核心要点:
- 账务流水:每笔交易都记录,可追溯
- 幂等性:支付单号唯一,防止重复扣款
- 冻结金额:先冻结后扣除,防止超扣
- 异步通知:MQ + 重试,确保可靠
- 对账系统:每天对账,发现差异
下次面试,这样回答:
"支付系统的核心是账务流水表。每笔交易都记录流水,包括交易流水号、出账账户、入账账户、金额、交易类型、业务ID、状态等。流水号全局唯一,作为幂等性保证。
支付流程分为三步:冻结、扣除、记录。首先冻结用户账户的金额,调用支付网关,成功后扣除冻结金额并记录流水,失败则释放冻结金额。账户表使用乐观锁,更新时检查version字段和余额是否充足,防止并发超扣。
幂等性通过支付单号保证。支付单号格式为'PAY+订单ID+时间戳',数据库设置唯一索引。支付前先查询支付单是否存在,如果已存在且状态为成功,直接返回不再重复扣款。
异步通知使用RocketMQ实现。支付成功后发送MQ消息,消费者调用商家回调接口。如果调用失败,采用指数退避重试,分别在1分钟、5分钟、10分钟后重试。3次都失败则记录到数据库,人工介入处理。
对账系统每天凌晨1点执行。下载支付宝的对账单,查询系统的账务流水,逐条比对交易流水号、金额、状态是否一致。发现差异后生成报表,发送钉钉通知给财务人员处理。对账是支付系统的最后一道防线,确保资金安全。"
面试官:👍 "很好!你对支付系统的设计理解很深刻!"
本文完 🎬
上一篇: 212-设计一个分布式缓存系统.md
下一篇: 214-设计一个优惠券系统.md
作者注:写完这篇,我觉得钱包更安全了!💰
如果这篇文章对你有帮助,请给我一个Star⭐!