资讯动态

零工抢单:一个单子 3000 人同时抢,Redis 分布式锁 + 乐观锁才稳

发布时间:2026/10/2 5:56:33 来源:尧图企业网站定制
导读xllg 日结零工一个打扫卫生的单子发出去 10 秒被抢完刚开始用 synchronized 锁单机测试没问题部署两台后直接超卖一个单子被两个人同时抢走。后来换成 Redis 分布式锁 数据库乐观锁双保险才稳住。这篇记录抢单高并发的完整改造过程。零工抢单一个单子 3000 人同时抢Redis 分布式锁 乐观锁才稳先说事故。零工平台上线第三天某老板发了 5 个单子每单 1 个名额结果后台数据显示5 个单子被抢走了 9 次同一个单子两个阿姨都显示抢单成功。客户打电话过来骂了一顿。第一版synchronized单机够用多机直接废第一版代码很简单接单方法用synchronized锁publicsynchronizedbooleangrabOrder(LongorderId,LonguserId){// 1. 查订单当前状态OrderorderorderMapper.selectById(orderId);if(order.getStatus()!OrderStatus.WAITING.getCode()){returnfalse;// 已经被抢了}// 2. 更新为已抢orderMapper.updateStatus(orderId,OrderStatus.GRABBED.getCode());// 3. 记录抢单关系grabMapper.insert(newOrderGrab(orderId,userId));returntrue;}单机压测 2000 并发全过了上生产两台服务器synchronized 锁的是各自 JVM 里的对象两台机器同时进入方法查到的都是 WAITING双双更新成功。超卖就是这么来的。第二版Redis 分布式锁锁跨 JVM先上 Redis 分布式锁setIfAbsent原子加锁AutowiredprivateStringRedisTemplateredisTemplate;privatestaticfinalStringLOCK_KEY_PREFIXgrab:lock:;publicbooleangrabOrder(LongorderId,LonguserId){// 用订单 ID 做锁 key抢单串行化StringlockKeyLOCK_KEY_PREFIXorderId;booleanlockedBoolean.TRUE.equals(redisTemplate.opsForValue().setIfAbsent(lockKey,String.valueOf(userId),10,TimeUnit.SECONDS));if(!locked){returnfalse;// 别人正在抢直接失败}try{returndoGrab(orderId,userId);}finally{// 释放锁校验是自己的锁才删防止误删别人的StringownerredisTemplate.opsForValue().get(lockKey);if(String.valueOf(userId).equals(owner)){redisTemplate.delete(lockKey);}}}分布式锁保证同一时间只有一个 JVM 能进入抢单逻辑超卖问题解决了。第三版数据库乐观锁兜底防重但光有分布式锁不够。抢单不止一条链路后台管理员改单、运营直接改库、MQ 异步补偿都可能绕过 Redis 锁。数据库层面必须兜底用乐观锁或条件更新-- 抢单核心status 条件是 CAS影响行数1 才算抢到UPDATEorder_SETstatus2,grab_user_id#{userId}, grab_time NOW()WHEREid#{orderId} AND status 1;Java 侧判断影响行数publicbooleandoGrab(LongorderId,LonguserId){// 条件更新status1待抢才更新成功天然防超卖introwsorderMapper.grabOrder(orderId,userId);if(rows0){returnfalse;// 被并发抢走了}// 抢单成功记录明细grabMapper.insert(newOrderGrab(orderId,userId));returntrue;}UPDATE ... WHERE status 1是数据库行的原子操作并发下只有一个事务能更新成功即使绕过 Redis 锁数据库也拦得住。踩坑一锁过期时间 10 秒任务跑了 15 秒现象高峰期偶发一个单子被抢两次排查发现是锁提前过期。抢单流程里有查积分、扣余额、发消息耗时超过了锁的 10 秒 TTL锁自动释放第二个请求又进来了。排查过程看日志两次抢单间隔 12 秒恰好大于锁 TTL。定位思路锁的过期时间必须大于业务最慢路径但设太长锁出问题又会卡死线程崩溃不释放。最终解决两种方案二选一——① 用 Redisson 的看门狗自动续期② 锁 TTL 按业务耗时 3 倍设置 finally 释放。我项目里用了 RedissonRLocklockredissonClient.getLock(grab:lock:orderId);booleanlockedlock.tryLock(0,30,TimeUnit.SECONDS);// 看门狗会自动续期踩坑二抢单成功但消息发不出去用户以为没抢到现象抢单接口返回成功用户却收到抢单失败通知。排查过程抢单事务提交后发 MQMQ 发送异常被 catch 吞掉前端没收到状态推送。日志里 MQ 报错connection refused恰好赶上 Redis 故障演练。定位思路抢单状态以数据库为准消息只是通知不能因为通知失败回滚抢单结果。最终解决抢单成功先落库消息发送失败走重试表不阻塞主流程TransactionalpublicbooleangrabOrder(LongorderId,LonguserId){introwsorderMapper.grabOrder(orderId,userId);if(rows0)returnfalse;try{mqTemplate.convertAndSend(grab.success,newGrabMsg(orderId,userId));}catch(Exceptione){// 消息发送失败不阻塞抢单进重试表凌晨补偿notifyRetryMapper.save(orderId,userId);log.error(抢单通知发送失败 orderId{},orderId,e);}returntrue;}可直接复用的要点抢单/秒杀这类高并发写场景分布式锁 数据库条件更新双保险别只靠一层。分布式锁 key 用业务 IDvalue 存持有者标识释放时校验是自己的锁。锁 TTL 必须大于业务最慢路径或用 Redisson 看门狗自动续期。数据库 CAS 更新UPDATE ... WHERE status 待抢影响行数1 才算成功。抢单结果以数据库为准通知类副作用失败不影响主流程。加单测压一把多线程并发抢同一单断言只有一人成功。项目源码https://gitee.com/gzqkl/xllg

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑