一、业务场景:电商商户余额提现需求
在电商平台中,商户通过销售商品产生的收入会暂存于「平台账户余额」中,核心需求是:支持商户将账户余额提现至微信钱包。
这一场景的核心痛点是「分布式事务一致性」—— 既要确保商户账户余额准确扣减,又要保证微信钱包能成功到账,避免 “扣了钱没到账” 或 “没扣钱却到账” 的资金异常问题,是分布式系统中极具代表性的实战案例。
二、技术方案:MQ 最终一致性方案
采用「MQ 事务消息 + 最终一致性」方案,通过消息队列的可靠性投递与消费,确保跨系统(电商账户系统、微信支付系统)的数据一致性,整体流程如下:
关键设计:事务消息确保 “业务操作(扣余额、创记录)成功” 与 “消息投递成功” 强绑定,避免消息丢失;消费者失败重试机制确保微信打款最终能执行成功。
三、实战落地:代码与效果演示
1. 前置准备:数据库表设计
需 2 张核心表存储账户与提现数据,SQL 如下:
-- 1. 账户表:存储商户账户信息与余额
drop table if exists t_account;
create table if not exists t_account
(
id varchar(32) not null primary key comment '用户ID',
name varchar(50) not null comment '商户名称',
balance decimal(12, 2) not null comment '账户余额(单位:元)'
) comment '电商平台账户表';
-- 初始化测试数据:商户ID=1,余额1000元
insert ignore into t_account_lesson037 value ('1','路人1','1000.00');
-- 2. 提现记录表:跟踪每笔提现的状态(核心:分布式事务对账依据)
drop table if exists t_cash_out;
create table if not exists t_cash_out
(
id varchar(32) not null primary key comment '提现记录ID',
account_id varchar(32) not null comment '关联账户ID',
price decimal(12, 2) not null comment '提现金额',
status smallint not null comment '状态(0:待处理,100:提现成功)',
create_time datetime not null comment '创建时间',
update_time datetime comment '最后更新时间'
) comment '账户提现记录表';
2. 核心代码实现
(1)提现接口:处理业务逻辑 + 发送事务消息
接口路径:com.itsoku.lesson037.controller.AccountController#cashOut
核心职责(3 件事):
- 校验账户余额是否充足;
- 扣减账户余额 + 创建「待处理」状态的提现记录(同一数据库事务,确保原子性);
- 发送 MQ 事务消息(确保业务操作成功后,消息必投递)。
@PostMapping("/account/cashOut")
@Transactional(rollbackFor = Exception.class)
public ResultDTO cashOut(@RequestBody CashOutDTO cashOutDTO) {
// 1. 获取当前商户账户(实际场景需从登录态获取accountId)
String accountId = "1";
AccountDO account = accountMapper.selectById(accountId);
if (account == null) {
return ResultDTO.fail("账户不存在");
}
// 2. 校验余额是否充足
BigDecimal cashOutAmount = new BigDecimal(cashOutDTO.getPrice());
if (account.getBalance().compareTo(cashOutAmount) < 0) {
return ResultDTO.fail("余额不足");
}
// 3. 扣减账户余额
account.setBalance(account.getBalance().subtract(cashOutAmount));
accountMapper.updateById(account);
// 4. 创建提现记录(状态:待处理)
CashOutDO cashOutDO = new CashOutDO();
cashOutDO.setId(UUID.randomUUID().toString());
cashOutDO.setAccountId(accountId);
cashOutDO.setPrice(cashOutAmount);
cashOutDO.setStatus(0); // 0=待处理
cashOutDO.setCreateTime(new Date());
cashOutMapper.insert(cashOutDO);
// 5. 发送MQ事务消息(确保消息投递成功)
mqProducer.sendCashOutTransactionMsg(cashOutDO.getId(), accountId, cashOutAmount);
return ResultDTO.success("提现请求已受理");
}
(2)提现消息消费者:调用微信接口 + 更新状态
核心职责(2 件事):
- 调用微信支付接口完成打款(依赖微信接口的幂等性,避免重复打款);
- 更新提现记录状态为「成功」。
特性:继承 AbstractRetryConsumer 抽象类,自动拥有「失败衰减式重试」能力(如:失败后隔 10s、30s、1min 重试,避免瞬时故障导致的失败)。
@Component
public class CashOutMsgConsumer extends AbstractRetryConsumer<CashOutMsg> {
@Autowired
private WeChatPayService weChatPayService;
@Autowired
private CashOutMapper cashOutMapper;
@Override
public boolean consume(CashOutMsg msg) {
try {
// 1. 调用微信支付接口:给商户微信钱包打款
// 微信接口天然支持幂等(通过提现记录ID作为唯一标识)
boolean paySuccess = weChatPayService.transferToWeChat(
msg.getAccountId(), // 商户微信关联ID(实际需存储)
msg.getCashOutId(), // 唯一标识(幂等键)
msg.getPrice() // 打款金额
);
if (!paySuccess) {
log.error("微信打款失败,cashOutId:{}", msg.getCashOutId());
return false; // 返回false触发重试
}
// 2. 更新提现记录状态为「成功」
CashOutDO updateDO = new CashOutDO();
updateDO.setId(msg.getCashOutId());
updateDO.setStatus(100); // 100=提现成功
updateDO.setUpdateTime(new Date());
cashOutMapper.updateById(updateDO);
return true; // 消费成功
} catch (Exception e) {
log.error("提现消费异常,cashOutId:{}", msg.getCashOutId(), e);
return false; // 异常时触发重试
}
}
}
3. 效果验证步骤
步骤 1:启动应用
步骤 2:查看初始账户余额
执行 SQL 查询账户初始状态:
select * from t_account;
初始结果:id=1,name = chen 1,balance=1000.00(元)。
步骤 3:调用提现接口
使用 HTTP 工具(如 Postman、IDEA HTTP Client)发起请求:
### 商户提现请求
POST http://localhost:8080/account/cashOut
Accept: application/json
Content-Type: application/json
{
"price": "10.00" // 提现10元
}
步骤 4:验证最终结果
账户余额验证:
select * from t_account;
结果:balance=990.00(元),余额成功扣减 10 元。
提现记录验证:
select * from t_cash_out;
结果:status=100(提现成功),update_time 有值,说明微信打款与状态更新均完成。