项目背景
拼团是电商平台重要的营销手段,通过社交裂变实现用户增长。本文分享拼团系统的核心设计与实现,支撑高并发场景下的库存扣减和订单处理。
系统架构
整体架构
1 2 3 4 5
| 用户发起拼团 → 库存预扣 → 创建拼团单 → 分享邀请 → 成团/失败 ↓ Redis 库存层 ↓ 异步订单处理(MQ)
|
核心流程
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
| @RestController public class GroupBuyController { @Autowired private GroupBuyService groupBuyService;
@PostMapping("/group-buy/create") public Result<GroupBuyOrder> createGroupBuy( @RequestBody CreateGroupBuyRequest request) { validateRequest(request); GroupBuyOrder order = groupBuyService.createGroupBuy( request.getUserId(), request.getProductId(), request.getQuantity() ); return Result.success(order); } }
|
核心功能实现
1. 库存预扣设计
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60
| @Service public class GroupBuyStockService { @Autowired private StringRedisTemplate redisTemplate; @Autowired private RedissonClient redissonClient; private static final String STOCK_KEY_PREFIX = "groupbuy:stock:"; private static final String LOCK_KEY_PREFIX = "lock:groupbuy:";
public boolean preDeductStock(Long productId, Integer quantity) { String stockKey = STOCK_KEY_PREFIX + productId; String luaScript = """ local stock = tonumber(redis.call('get', KEYS[1])) local deduct = tonumber(ARGV[1]) if stock == nil then return -1 -- 库存未初始化 end if stock >= deduct then redis.call('decrby', KEYS[1], deduct) return 1 -- 扣减成功 else return 0 -- 库存不足 end """; Long result = redisTemplate.execute( new DefaultRedisScript<>(luaScript, Long.class), Collections.singletonList(stockKey), quantity.toString() ); if (result == null || result == 0) { return false; } if (result == -1) { throw new BusinessException("库存未初始化"); } return true; }
public void rollbackStock(Long productId, Integer quantity) { String stockKey = STOCK_KEY_PREFIX + productId; redisTemplate.opsForValue().increment(stockKey, quantity); } }
|
2. 拼团单管理
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64
| @Service public class GroupBuyOrderService { @Autowired private GroupBuyOrderMapper orderMapper; @Autowired private GroupBuyStockService stockService; @Autowired private RabbitTemplate rabbitTemplate;
@Transactional public GroupBuyOrder createGroupBuyOrder( Long userId, Long productId, Integer quantity) { boolean success = stockService.preDeductStock(productId, quantity); if (!success) { throw new BusinessException("库存不足"); } try { GroupBuyOrder order = new GroupBuyOrder(); order.setOrderNo(generateOrderNo()); order.setUserId(userId); order.setProductId(productId); order.setQuantity(quantity); order.setStatus(GroupBuyStatus.CREATED); order.setExpireTime(LocalDateTime.now().plusHours(24)); orderMapper.insert(order); GroupBuyDelayMessage message = new GroupBuyDelayMessage(); message.setOrderId(order.getId()); message.setExpireTime(order.getExpireTime()); rabbitTemplate.convertAndSend( "groupbuy.delay.exchange", "groupbuy.delay.routingkey", message, msg -> { msg.getMessageProperties() .setDelay(24 * 60 * 60 * 1000); return msg; } ); return order; } catch (Exception e) { stockService.rollbackStock(productId, quantity); throw e; } } }
|
3. 分布式任务协调
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49
| @Component public class GroupBuyExpireTask { @Autowired private RedissonClient redissonClient; @Autowired private GroupBuyOrderService orderService;
@Scheduled(fixedRate = 5 * 60 * 1000) public void processExpiredOrders() { String lockKey = "lock:groupbuy:expire-task"; RLock lock = redissonClient.getLock(lockKey); try { boolean acquired = lock.tryLock(10, 5 * 60, TimeUnit.SECONDS); if (!acquired) { log.info("未获取到分布式锁,跳过本次执行"); return; } List<GroupBuyOrder> expiredOrders = orderService .getExpiredOrders(LocalDateTime.now()); for (GroupBuyOrder order : expiredOrders) { try { orderService.closeOrder(order.getId()); log.info("关闭过期拼团单: {}", order.getId()); } catch (Exception e) { log.error("处理过期拼团单失败: {}", order.getId(), e); } } } catch (InterruptedException e) { Thread.currentThread().interrupt(); } finally { if (lock.isHeldByCurrentThread()) { lock.unlock(); } } } }
|
4. 异步回调处理
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33
| @Component public class GroupBuyCallbackListener { @Autowired private GroupBuyOrderService orderService; @RabbitListener(queues = "groupbuy.callback.queue") public void handleCallback(GroupBuyCallbackMessage message) { log.info("收到拼团回调: {}", message.getOrderId()); try { switch (message.getEventType()) { case "GROUP_SUCCESS": orderService.handleGroupSuccess(message.getOrderId()); break; case "GROUP_FAIL": orderService.handleGroupFail(message.getOrderId()); break; case "PAY_SUCCESS": orderService.handlePaySuccess(message.getOrderId()); break; case "PAY_FAIL": orderService.handlePayFail(message.getOrderId()); break; default: log.warn("未知事件类型: {}", message.getEventType()); } } catch (Exception e) { log.error("处理回调失败: {}", message.getOrderId(), e); } } }
|
5. 用户标签与活动投放
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67
| @Service public class UserTagService { @Autowired private StringRedisTemplate redisTemplate;
public void tagUser(Long userId, String tag) { String bitmapKey = "user:tag:" + tag; redisTemplate.opsForValue().setBit(bitmapKey, userId, true); }
public boolean hasTag(Long userId, String tag) { String bitmapKey = "user:tag:" + tag; return Boolean.TRUE.equals( redisTemplate.opsForValue().getBit(bitmapKey, userId) ); }
public Long countTaggedUsers(String tag) { String bitmapKey = "user:tag:" + tag; return redisTemplate.execute( new DefaultRedisScript<>( "return redis.call('bitcount', KEYS[1])", Long.class ), Collections.singletonList(bitmapKey) ); }
public List<Long> getUsersWithAllTags(List<String> tags) { String tempKey = "user:tag:temp:" + System.currentTimeMillis(); String[] bitmapKeys = tags.stream() .map(tag -> "user:tag:" + tag) .toArray(String[]::new); redisTemplate.execute((RedisCallback<Void>) connection -> { connection.bitOp( RedisStringCommands.BitOperation.AND, tempKey.getBytes(), bitmapKeys[0].getBytes(), Arrays.copyOfRange(bitmapKeys, 1, bitmapKeys.length) .stream() .map(String::getBytes) .toArray(byte[][]::new) ); return null; }); return userIds; } }
|
性能优化
异步多线程优化
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32
| @Service public class GroupBuyCalculationService { @Autowired private ExecutorService executorService;
public GroupBuyCalculation calculateAsync(Long productId, Integer quantity) { CompletableFuture<Product> productFuture = CompletableFuture .supplyAsync(() -> productService.getById(productId), executorService); CompletableFuture<Promotion> promotionFuture = CompletableFuture .supplyAsync(() -> promotionService.getCurrentPromotion(productId), executorService); CompletableFuture<UserLevel> levelFuture = CompletableFuture .supplyAsync(() -> userService.getUserLevel(), executorService); CompletableFuture.allOf(productFuture, promotionFuture, levelFuture).join(); return assembleCalculation( productFuture.join(), promotionFuture.join(), levelFuture.join(), quantity ); } }
|
总结
拼团系统的核心设计要点:
- 库存控制:Redis 预扣 + 异步同步,保证不超卖
- 状态管理:状态机模型,清晰定义流转规则
- 任务调度:分布式锁 + 延迟队列,处理过期订单
- 异步处理:MQ 解耦,提升系统吞吐量
- 数据存储:Bitmap 存储标签,节省空间
通过合理的设计,系统可以支撑高并发场景下的拼团业务。
本文基于电商营销系统实践,支撑日均数万级拼团订单