ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

零工日结提现:余额、提现单、事务、幂等,我是这么把这笔“钱“管住的

零工日结提现:余额、提现单、事务、幂等,我是这么把这笔“钱“管住的 零工日结提现余额、提现单、事务、幂等我是这么把这笔钱管住的导读xllg 是日结零工系统工人干完活当天就能提现。涉及钱的逻辑我最怕出 bug——多扣一笔、重复到账、提现单和余额对不上哪一样都够喝一壶的。这篇讲讲提现链路是怎么设计的余额账户 提现单 事务 幂等四层一起上。表结构余额和流水分开余额不能只存一个数字还得有流水可追溯。两张表user_wallet存账户余额wallet_flow存每一笔变动CREATETABLEuser_wallet(idBIGINTPRIMARYKEYAUTO_INCREMENT,user_idBIGINTNOTNULLCOMMENT用户ID,balanceDECIMAL(10,2)NOTNULLDEFAULT0.00COMMENT可用余额,frozenDECIMAL(10,2)NOTNULLDEFAULT0.00COMMENT冻结金额提现中的,versionINTNOTNULLDEFAULT0COMMENT乐观锁版本号,update_timeDATETIMEDEFAULTCURRENT_TIMESTAMPONUPDATECURRENT_TIMESTAMP,UNIQUEKEYuk_user(user_id))ENGINEInnoDBDEFAULTCHARSETutf8mb4COMMENT用户余额账户;CREATETABLEwallet_flow(idBIGINTPRIMARYKEYAUTO_INCREMENT,user_idBIGINTNOTNULL,typeTINYINTNOTNULLCOMMENT1收入 2支出 3冻结 4解冻,amountDECIMAL(10,2)NOTNULL,balance_afterDECIMAL(10,2)NOTNULLCOMMENT变动后余额,biz_noVARCHAR(64)NOTNULLCOMMENT业务单号幂等键,create_timeDATETIMEDEFAULTCURRENT_TIMESTAMP,UNIQUEKEYuk_biz(biz_no))ENGINEInnoDBDEFAULTCHARSETutf8mb4COMMENT钱包流水;核心设计流水表的 biz_no 唯一索引就是幂等键同一笔提现重复提交第二次插流水直接报唯一键冲突。提现事务先冻结再记账提现逻辑分两步把可用余额冻结生成提现单。两步必须在一个事务里否则出现钱冻了但单子没建就乱了ServicepublicclassWithdrawService{AutowiredprivateWalletMapperwalletMapper;AutowiredprivateWithdrawMapperwithdrawMapper;AutowiredprivateFlowMapperflowMapper;Transactional(rollbackForException.class)publicvoidapplyWithdraw(LonguserId,BigDecimalamount){// 1. 扣减可用余额、增加冻结带乐观锁防止并发intupdatedwalletMapper.freeze(userId,amount);if(updated!1){thrownewBizException(余额不足或账户被并发修改请重试);}// 2. 生成提现单WithdrawordernewWithdraw();order.setUserId(userId);order.setAmount(amount);order.setStatus(0);// 待打款withdrawMapper.insert(order);// 3. 记流水biz_no 唯一重复提交会失败flowMapper.insert(FlowBuilder.build(userId,3,amount,提现冻结));}}walletMapper.freeze的 SQL 是关键一条 UPDATE 同时完成判断和扣减UPDATEuser_walletSETfrozenfrozen#{amount},versionversion1WHEREuser_id#{userId}ANDbalance#{amount}balance #{amount}条件不满足时影响行数为 0Java 侧拿updated ! 1判断天然防住余额不够还提现和并发扣超。踩坑事务方法内部调自己Transactional 失效现象提现接口偶发出现余额扣了但提现单没生成的情况。查库发现 wallet_flow 有冻结流水、user_wallet 余额也减了但 withdraw 表里没有对应记录。排查先看日志报的是NullPointerException——插入提现单前取用户信息时空指针。关键问题是异常抛出来了但余额已经扣了说明事务没回滚。再看代码发现applyWithdraw是被同一个类里的另一个方法handleWithdrawRequest通过this.applyWithdraw()调用的。定位这就是经典的自调用问题——Transactional依赖 Spring 代理this.xxx()是直接调用原始对象的方法代理根本不参与事务注解形同虚设异常自然不会回滚。解决把提现事务方法拆到独立 Service或者通过注入自己的代理调用ServicepublicclassWithdrawService{// 通过 AopContext 拿代理调用自己保证事务生效Transactional(rollbackForException.class)publicvoidapplyWithdraw(LonguserId,BigDecimalamount){// ...冻结 建单 流水}publicvoidhandleWithdrawRequest(LonguserId,BigDecimalamount){// 需要事务的方法必须经过代理((WithdrawService)AopContext.currentProxy()).applyWithdraw(userId,amount);}}改造后测试验证人为制造异常余额、提现单、流水全部回滚三者保持一致。可直接复用的清单余额和流水分表余额是结果、流水是证据对账靠流水扣余额的 SQL 把判断 扣减合到一条 UPDATE影响行数做并发控制提现单用biz_no唯一索引做幂等重复提交直接冲突报错事务方法禁止自调用必须经过 Spring 代理才生效金额一律用DECIMAL禁止 float/double精度问题都是血泪这套提现链路在 xllg 里上线后对账单月月能对上没出过一次金额纠纷。涉及钱的功能宁可多写一层幂等也别赌不会重复提交。项目源码https://gitee.com/gzqkl/xllg
返回列表