共计 2869 个字符,预计需要花费 8 分钟才能阅读完成。
背景痛点分析
在线充值业务看似简单,但在高并发场景下会暴露诸多问题。我们 Claude 平台在用户量突破百万时,充值系统开始出现明显瓶颈:

- 并发冲突:促销活动时出现超额充值(用户余额增加但订单未记录)
- 数据一致性:主从延迟导致查询结果不一致(显示充值未到账但实际已处理)
- 系统可用性:数据库 CPU 飙升至 90% 导致整个支付链路雪崩
最严重的一次事故发生在春节红包活动期间,每秒 3000+ 的充值请求直接击穿数据库,不得不临时关闭充值通道。这迫使我们重新设计整个架构。
技术选型对比
针对资金类操作,我们重点评估了三种方案:
- MySQL 分片方案
- 优点:成熟稳定、强一致性
-
缺点:扩容复杂,join 操作困难
-
Redis 事务方案
- 优点:性能极高(10w+ QPS)
-
缺点:持久化风险,不适合作为唯一数据源
-
分布式锁方案
- 优点:实现简单
- 缺点:锁竞争成为性能瓶颈
最终采取 组合方案:
– 用 MySQL 分片保证数据持久化
– Redis 做库存预扣减
– 分布式锁仅用于关键路径
核心架构实现
1. 分库分表设计
采用 ShardingSphere 5.x 实现水平分片:
# application-sharding.yml
spring:
shardingsphere:
datasource:
names: ds0,ds1
sharding:
tables:
t_order:
actual-data-nodes: ds$->{0..1}.t_order_$->{0..15}
table-strategy:
standard:
sharding-column: user_id
precise-algorithm-class-name: com.claude.config.ModuloShardingAlgorithm
按 user_id 哈希分片,避免热点问题。历史数据自动归档到冷库。
2. 消息最终一致性
充值核心流程:
- 用户发起请求 → 2. 生成预支付单 → 3. 调用支付网关 → 4. 异步通知结果
关键点在于第 4 步,我们采用 RocketMQ 的事务消息:
// 发送半消息
MessageBuilder builder = MessageBuilder.withPayload(order)
.setHeader(RocketMQHeaders.TRANSACTION_ID, txId);
rocketMQTemplate.sendMessageInTransaction("claude-tx-group",
"TOPIC_RECHARGE", builder.build(), order);
// 本地事务执行器
@RocketMQTransactionListener(txProducerGroup = "claude-tx-group")
public class RechargeListener implements RocketMQLocalTransactionListener {
@Override
public LocalTransactionState executeLocalTransaction(Message msg, Object arg) {
try {Order order = (Order)arg;
orderService.process(order); // 数据库操作
return LocalTransactionState.COMMIT_MESSAGE;
} catch (Exception e) {return LocalTransactionState.ROLLBACK_MESSAGE;}
}
// 省略检查逻辑...
}
3. 幂等性保障
采用「业务 ID+ 来源 + 类型」三元组做唯一约束:
CREATE TABLE `idempotent_record` (`biz_id` varchar(64) NOT NULL COMMENT '业务 ID',
`source` tinyint NOT NULL COMMENT '请求来源',
`type` varchar(32) NOT NULL COMMENT '业务类型',
`created_at` datetime DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`biz_id`,`source`,`type`)
) ENGINE=InnoDB;
处理前先插入记录,利用数据库唯一索引防重。
关键代码实现
分布式锁优化版
避免锁过期导致并发问题:
public boolean tryLock(String key, long expireSeconds) {String lockId = UUID.randomUUID().toString();
String script =
"if redis.call('setnx', KEYS[1], ARGV[1]) == 1 then" +
"return redis.call('pexpire', KEYS[1], ARGV[2]) else return 0 end";
Long result = redisTemplate.execute(new DefaultRedisScript<>(script, Long.class),
Collections.singletonList(key),
lockId,
String.valueOf(expireSeconds * 1000)
);
return result != null && result == 1;
}
补偿任务设计
每小时扫描状态为「处理中」超过 30 分钟的订单:
@Scheduled(cron = "0 0/5 * * * ?")
public void checkTimeoutOrders() {List<Order> timeoutOrders = orderMapper.selectTimeoutOrders(30);
timeoutOrders.forEach(order -> {
// 查询支付渠道确认状态
PaymentStatus status = paymentClient.query(order.getPaymentNo());
if (status == PaymentStatus.SUCCESS) {orderService.confirmOrder(order); // 补单
}
});
}
性能测试结果
使用 JMeter 模拟真实场景(持续 30 分钟):
| 场景 | 线程数 | QPS | 平均 RT | 错误率 |
|---|---|---|---|---|
| 原始架构 | 1000 | 428 | 2300ms | 12.3% |
| 优化后架构 | 1000 | 1856 | 520ms | 0.02% |
| 极限压测 | 3000 | 2634 | 1100ms | 0.8% |
关键提升点:
– 数据库负载从 90% 降至 35%
– 99 线延迟从 5s 优化到 800ms
– 不再出现资金不一致情况
避坑指南
- 分布式事务隔离级别
- 充值业务选择 Read Committed 足够
-
对账系统需要 Snapshot Isolation
-
资金审计要点
- 操作日志必须包含前 / 后余额快照
-
采用双向核对(用户余额变动总和 = 系统收入总和)
-
异常处理经验
- 网络超时默认当作失败处理
- 接入第三方支付时要做资金对账
- 任何资金操作都要有人工干预通道
开放性问题
现有架构在跨币种充值场景下会面临新的挑战:
– 如何保证汇率波动时的金额一致性?
– 分库分表后怎么做多币种余额汇总?
– 实时风控系统如何与高并发支付平衡?
欢迎在评论区分享你的解决方案。
