恒美微站
首页
关于我们
建站服务
主题模板
案例展示
资讯中心
联系我们
30 人岗位 3 秒抢完:xllg 零工报名名额并发扣减与用工单状态流转
首页
资讯中心
/
30 人岗位 3 秒抢完:xllg 零工报名名额并发扣减与用工单状态流转
30 人岗位 3 秒抢完:xllg 零工报名名额并发扣减与用工单状态流转
发布时间:2026/10/14 1:52:49
30 人岗位 3 秒抢完xllg 零工报名名额并发扣减与用工单状态流转导读零工岗位一发出去报名是秒级的——工人盯着手机抢名额。并发上来以后超卖、重复报名、名额扣成负数全是问题。xllg日结零工招工系统在报名环节用原子扣减 唯一约束 状态机三件套把抢名额这件事做到了既不超卖也不漏报。零工报名的并发和电商秒杀本质一样有限名额多人同时争抢。但比秒杀更麻烦的是报名不是一次点击就结束——报上名之后还有待确认 → 已录取 → 打卡 → 结算一整条链路每一步状态都影响名额释放逻辑。设计核心就两件事名额怎么扣才不超卖订单状态怎么流转才不乱。第一步名额扣减必须原子最容易翻车的写法是先查剩余名额再判断够不够够就扣。三个步骤之间隔着网络和线程切换两个请求同时查到时剩 1 个都能通过判断然后一起扣——超卖。正确做法是让扣减本身带条件一次 UPDATE 完成判断 扣减TransactionalpublicApplyResultapply(ApplyCommandcmd){// 1. 原子扣减只有剩余名额 0 才扣affected rows 0 说明没抢到introwsjobMapper.deductQuota(cmd.getJobId());if(rows0){returnApplyResult.fail(手慢了名额已被抢完);}// 2. 生成报名记录唯一约束兜底防同一人重复报名try{applyMapper.insert(ApplyRecord.builder().jobId(cmd.getJobId()).workerId(cmd.getWorkerId()).status(ApplyStatus.PENDING.code()).build());}catch(DuplicateKeyExceptione){// 名额扣了但报名重复——必须回滚名额否则名额悄悄少了jobMapper.rollbackQuota(cmd.getJobId());thrownewBizException(你已报名过该岗位);}returnApplyResult.success();}对应 Mapper 里的原子 SQLUPDATE jx_job SET quota_remaining quota_remaining - 1, version version 1 WHERE id #{jobId} AND quota_remaining 0 UPDATE jx_job SET quota_remaining quota_remaining 1 WHERE id #{jobId}WHERE quota_remaining 0是关键扣减自带条件数据库层面保证不会扣成负数。这也是为什么零工平台这类高并发扣减别用先查后改一条条件 UPDATE 就能扛住。第二步唯一约束防重复报名原子扣减解决了超卖但同一工人疯狂点报名按钮可能扣两次名额、插两条记录。靠应用层判断不可靠数据库唯一约束才是最后防线CREATETABLEjx_apply_record(idBIGINTUNSIGNEDAUTO_INCREMENTPRIMARYKEY,job_idBIGINTUNSIGNEDNOTNULL,worker_idBIGINTUNSIGNEDNOTNULL,statusTINYINTNOTNULLDEFAULT1COMMENT1待确认 2已录取 3已打卡 4已完成 5已取消,create_timeDATETIMENOTNULLDEFAULTCURRENT_TIMESTAMP,update_timeDATETIMENOTNULLDEFAULTCURRENT_TIMESTAMPONUPDATECURRENT_TIMESTAMP,-- 核心同一岗位同一工人只能有一条报名UNIQUEKEYuk_job_worker(job_id,worker_id),KEYidx_worker_status(worker_id,status))ENGINEInnoDBDEFAULTCHARSETutf8mb4COMMENT零工报名记录;代码里插记录要 catchDuplicateKeyException并回滚已扣的名额——上面 apply 方法里已经处理了。这里有个容易忽略的细节报错回滚名额和正常流程释放名额是两码事取消报名走状态机5已取消回滚走异常路径两条路都要保证名额不丢失。状态流转名额释放要有唯一出口报名状态不能随便改xllg 用状态机统一管理。重点是取消报名要释放名额而且必须和状态变更在同一个事务里否则状态改了名额没回来publicenumApplyStatus{PENDING(1),ADMITTED(2),CHECKED_IN(3),FINISHED(4),CANCELLED(5);publicbooleancanTransitTo(ApplyStatustarget){switch(this){casePENDING:returntargetADMITTED||targetCANCELLED;caseADMITTED:returntargetCHECKED_IN||targetCANCELLED;caseCHECKED_IN:returntargetFINISHED;// 打卡后不能取消caseFINISHED:returnfalse;caseCANCELLED:returnfalse;default:returnfalse;}}}Transactionalpublicvoidcancel(LongapplyId,LongworkerId){ApplyRecordrecordapplyMapper.selectById(applyId);// 校验归属只能取消自己的报名if(!record.getWorkerId().equals(workerId)){thrownewBizException(无权操作该报名);}ApplyStatuscurApplyStatus.of(record.getStatus());if(!cur.canTransitTo(ApplyStatus.CANCELLED)){thrownewBizException(当前状态不可取消: cur);}// 状态变更 名额释放同一事务applyMapper.updateStatus(applyId,ApplyStatus.CANCELLED.code(),record.getVersion());jobMapper.rollbackQuota(record.getJobId());}状态机枚举里的canTransitTo就是规则表业务代码里想跳状态必须过这一关非法流转直接被拒。打卡后禁止取消这条规则很重要——工人已经到场干活了名额不能再放出去否则雇主那边人来了、系统里名额又空着管理就乱套了。踩坑实录名额被扣了 3 个实际只有 2 个人报名现象运营发现某岗位显示剩余名额 17但报名记录只有 18 条应该剩 19名额 20。少了 2 个名额数据对不上。排查过程先查jx_apply_record的取消记录只有 1 条取消且已回滚。再看日志发现两次DuplicateKeyException的报错堆栈——异常发生后名额扣了但回滚逻辑没执行。原因报名插入用的是insert但 catch 块里的rollbackQuota依赖同一个事务而异常发生时事务已经被 Spring 标记为 rollback-only后续的 rollbackQuota SQL 虽然执行了提交时整个事务一起回滚——名额等于没加回来。定位思路问题不在 SQL在事务边界和异常处理。Transactional方法里 catch 了DuplicateKeyException之后继续执行其他写操作在 Spring 的默认机制下已抛异常的同一个事务已经脏了后面再怎么写都会被整体回滚。rollbackQuota 写在同一个事务里等于白写。最终解决把名额回滚拆到独立事务里执行用REQUIRES_NEW传播级别让回滚不受主事务状态影响Transactional(propagationPropagation.REQUIRES_NEW)publicvoidrollbackQuotaIndependent(LongjobId){jobMapper.rollbackQuota(jobId);}}catch(DuplicateKeyExceptione){rollbackQuotaIndependent(cmd.getJobId());// 独立事务必定生效thrownewBizException(你已报名过该岗位);}改完再压测100 并发抢 30 个名额最终报名记录恰好 30 条、剩余名额 0重复点击的用户全部被唯一约束拦截且名额正确回滚。事务里的补偿操作一定要用独立事务这是这次踩坑最值钱的教训。可以直接抄的结论名额扣减用条件 UPDATEWHERE quota_remaining 0数据库原子保证不超卖报名表建UNIQUE (job_id, worker_id)重复报名靠唯一约束兜底重复报名触发异常后名额回滚必须用REQUIRES_NEW独立事务否则被主事务 rollback-only 吞掉状态流转统一走枚举canTransitTo打卡后禁止取消名额释放只在取消/关闭路径里做所有释放名额的操作都要和状态变更同事务防止状态改了名额没回。并发扣减这事儿看着是 SQL 的事其实是事务边界的事。把原子性、唯一性、补偿三条线理清楚抢名额场景基本就稳了。项目源码https://gitee.com/gzqkl/xllg