Java优惠券系统设计:从核心模型到高并发实践

一、优惠券系统核心业务模型设计

1.1 实体关系建模

优惠券系统需包含四大核心实体:优惠券模板(CouponTemplate)、优惠券实例(Coupon)、用户优惠券(UserCoupon)和订单关联(OrderCoupon)。采用JPA实现时,可设计如下实体类:

  1. @Entity
  2. public class CouponTemplate {
  3. @Id @GeneratedValue(strategy = GenerationType.IDENTITY)
  4. private Long id;
  5. private String name;
  6. private String logo;
  7. private String intro;
  8. @Enumerated(EnumType.STRING)
  9. private CouponType type; // 折扣券/满减券/现金券
  10. private BigDecimal ruleValue; // 折扣值或金额
  11. private BigDecimal conditionAmount; // 满减条件
  12. private LocalDateTime startTime;
  13. private LocalDateTime endTime;
  14. private Integer total; // 发行总量
  15. private Integer remaining; // 剩余数量
  16. // 适用范围
  17. @ElementCollection
  18. private Set<Long> applicableCategories;
  19. @ElementCollection
  20. private Set<Long> applicableProducts;
  21. }
  22. @Entity
  23. public class UserCoupon {
  24. @Id @GeneratedValue(strategy = GenerationType.IDENTITY)
  25. private Long id;
  26. @ManyToOne
  27. private CouponTemplate template;
  28. private String code; // 唯一券码
  29. private LocalDateTime getTime;
  30. private LocalDateTime usedTime;
  31. @Enumerated(EnumType.STRING)
  32. private CouponStatus status; // 未使用/已使用/已过期
  33. @ManyToOne
  34. private User user;
  35. }

1.2 业务规则引擎实现

优惠券使用需满足多重条件校验,建议采用策略模式实现规则引擎:

  1. public interface CouponRule {
  2. boolean validate(UserCoupon coupon, OrderContext context);
  3. }
  4. @Component
  5. public class TimeValidityRule implements CouponRule {
  6. @Override
  7. public boolean validate(UserCoupon coupon, OrderContext context) {
  8. LocalDateTime now = LocalDateTime.now();
  9. return !now.isBefore(coupon.getTemplate().getStartTime())
  10. && !now.isAfter(coupon.getTemplate().getEndTime());
  11. }
  12. }
  13. @Service
  14. public class CouponValidator {
  15. @Autowired
  16. private List<CouponRule> rules;
  17. public boolean validate(UserCoupon coupon, OrderContext context) {
  18. return rules.stream()
  19. .allMatch(rule -> rule.validate(coupon, context));
  20. }
  21. }

二、高并发场景下的关键技术实现

2.1 优惠券发放的原子性控制

在秒杀场景下,需使用Redis分布式锁保证库存扣减的原子性:

  1. @Service
  2. public class CouponDistributionService {
  3. @Autowired
  4. private RedisTemplate<String, String> redisTemplate;
  5. public boolean acquireCoupon(Long templateId, Long userId) {
  6. String lockKey = "coupon_lock_" + templateId;
  7. try {
  8. boolean locked = redisTemplate.opsForValue()
  9. .setIfAbsent(lockKey, "locked", 10, TimeUnit.SECONDS);
  10. if (!locked) {
  11. throw new RuntimeException("操作太频繁,请稍后再试");
  12. }
  13. // 双重检查库存
  14. CouponTemplate template = couponTemplateRepo.findById(templateId)
  15. .orElseThrow(() -> new RuntimeException("优惠券不存在"));
  16. if (template.getRemaining() <= 0) {
  17. throw new RuntimeException("优惠券已领完");
  18. }
  19. // 扣减库存
  20. int updated = couponTemplateRepo.decreaseRemaining(templateId);
  21. if (updated == 0) {
  22. throw new RuntimeException("库存不足");
  23. }
  24. // 创建用户券
  25. UserCoupon coupon = new UserCoupon();
  26. coupon.setTemplate(template);
  27. coupon.setCode(generateCouponCode());
  28. coupon.setUser(userRepo.findById(userId).get());
  29. userCouponRepo.save(coupon);
  30. return true;
  31. } finally {
  32. redisTemplate.delete(lockKey);
  33. }
  34. }
  35. }

2.2 优惠券核销的幂等性设计

核销接口需处理重复调用问题,可采用Token机制:

  1. @RestController
  2. @RequestMapping("/api/coupons")
  3. public class CouponController {
  4. @PostMapping("/use")
  5. public ResponseEntity<?> useCoupon(@RequestHeader("X-Request-Token") String token,
  6. @RequestParam String couponCode) {
  7. // 验证Token唯一性
  8. String usedToken = redisTemplate.opsForValue().get("used_token_" + token);
  9. if (usedToken != null) {
  10. return ResponseEntity.badRequest().body("请求已处理");
  11. }
  12. try {
  13. couponService.useCoupon(couponCode);
  14. redisTemplate.opsForValue().set("used_token_" + token, "1", 24, TimeUnit.HOURS);
  15. return ResponseEntity.ok().build();
  16. } catch (Exception e) {
  17. return ResponseEntity.status(500).body(e.getMessage());
  18. }
  19. }
  20. }

三、系统优化与扩展设计

3.1 优惠券分片存储策略

当数据量超过单表500万条时,建议按模板ID进行分库分表:

  1. @Table(name = "user_coupon")
  2. @ShardingSphereTable(
  3. shardingAlgorithmName = "coupon-mod",
  4. databaseStrategy = "standard",
  5. databaseShardingColumn = "template_id",
  6. tableStrategy = "standard",
  7. tableShardingColumn = "template_id"
  8. )
  9. public class UserCoupon {
  10. // 实体定义同上
  11. }
  12. // 自定义分片算法
  13. public class CouponModShardingAlgorithm implements PreciseShardingAlgorithm<Long> {
  14. @Override
  15. public String doSharding(Collection<String> availableTargetNames,
  16. PreciseShardingValue<Long> shardingValue) {
  17. long templateId = shardingValue.getValue();
  18. int tableIndex = (int)(templateId % 4); // 4个分表
  19. return "user_coupon_" + tableIndex;
  20. }
  21. }

3.2 异步任务处理架构

优惠券过期检查适合使用Spring Batch+Quartz实现:

  1. @Configuration
  2. public class BatchConfig {
  3. @Bean
  4. public Job couponExpiryJob(JobRepository jobRepository,
  5. Step couponExpiryStep) {
  6. return new JobBuilder("couponExpiryJob", jobRepository)
  7. .start(couponExpiryStep)
  8. .build();
  9. }
  10. @Bean
  11. public Step couponExpiryStep(StepBuilderFactory stepBuilderFactory,
  12. ItemReader<UserCoupon> reader,
  13. ItemProcessor<UserCoupon, ExpiryResult> processor,
  14. ItemWriter<ExpiryResult> writer) {
  15. return stepBuilderFactory.get("couponExpiryStep")
  16. .<UserCoupon, ExpiryResult>chunk(100)
  17. .reader(reader)
  18. .processor(processor)
  19. .writer(writer)
  20. .build();
  21. }
  22. }
  23. // Quartz调度配置
  24. @Configuration
  25. public class QuartzConfig {
  26. @Bean
  27. public JobDetail couponExpiryJobDetail() {
  28. return JobBuilder.newJob(CouponExpiryJob.class)
  29. .withIdentity("couponExpiryJob")
  30. .storeDurably()
  31. .build();
  32. }
  33. @Bean
  34. public Trigger couponExpiryTrigger() {
  35. SimpleScheduleBuilder scheduleBuilder = SimpleScheduleBuilder
  36. .simpleSchedule()
  37. .withIntervalInHours(1) // 每小时执行
  38. .repeatForever();
  39. return TriggerBuilder.newTrigger()
  40. .forJob(couponExpiryJobDetail())
  41. .withIdentity("couponExpiryTrigger")
  42. .withSchedule(scheduleBuilder)
  43. .build();
  44. }
  45. }

四、最佳实践与经验总结

  1. 券码生成策略:采用雪花算法+业务前缀组合,如”DISC_”+snowflakeId,保证全局唯一且可读
  2. 防刷机制
    • 用户维度:同一用户10分钟内限领3张
    • IP维度:同一IP每小时限领20张
    • 设备维度:同一设备每天限领5张
  3. 数据一致性方案
    • 最终一致性:通过消息队列实现异步更新
    • 强一致性:使用Seata等分布式事务框架
  4. 监控指标
    • 发放成功率:成功数/请求数
    • 核销率:已使用数/发放总数
    • 库存预警:剩余量<10%时告警

实际项目实施中,建议采用渐进式架构演进:初期单库单表+缓存,中期引入分库分表,后期考虑服务化拆分。测试环境需模拟10万级QPS压力测试,重点关注锁竞争、数据库连接池耗尽等典型问题。