一、优惠券系统核心业务模型设计
1.1 实体关系建模
优惠券系统需包含四大核心实体:优惠券模板(CouponTemplate)、优惠券实例(Coupon)、用户优惠券(UserCoupon)和订单关联(OrderCoupon)。采用JPA实现时,可设计如下实体类:
@Entitypublic class CouponTemplate {@Id @GeneratedValue(strategy = GenerationType.IDENTITY)private Long id;private String name;private String logo;private String intro;@Enumerated(EnumType.STRING)private CouponType type; // 折扣券/满减券/现金券private BigDecimal ruleValue; // 折扣值或金额private BigDecimal conditionAmount; // 满减条件private LocalDateTime startTime;private LocalDateTime endTime;private Integer total; // 发行总量private Integer remaining; // 剩余数量// 适用范围@ElementCollectionprivate Set<Long> applicableCategories;@ElementCollectionprivate Set<Long> applicableProducts;}@Entitypublic class UserCoupon {@Id @GeneratedValue(strategy = GenerationType.IDENTITY)private Long id;@ManyToOneprivate CouponTemplate template;private String code; // 唯一券码private LocalDateTime getTime;private LocalDateTime usedTime;@Enumerated(EnumType.STRING)private CouponStatus status; // 未使用/已使用/已过期@ManyToOneprivate User user;}
1.2 业务规则引擎实现
优惠券使用需满足多重条件校验,建议采用策略模式实现规则引擎:
public interface CouponRule {boolean validate(UserCoupon coupon, OrderContext context);}@Componentpublic class TimeValidityRule implements CouponRule {@Overridepublic boolean validate(UserCoupon coupon, OrderContext context) {LocalDateTime now = LocalDateTime.now();return !now.isBefore(coupon.getTemplate().getStartTime())&& !now.isAfter(coupon.getTemplate().getEndTime());}}@Servicepublic class CouponValidator {@Autowiredprivate List<CouponRule> rules;public boolean validate(UserCoupon coupon, OrderContext context) {return rules.stream().allMatch(rule -> rule.validate(coupon, context));}}
二、高并发场景下的关键技术实现
2.1 优惠券发放的原子性控制
在秒杀场景下,需使用Redis分布式锁保证库存扣减的原子性:
@Servicepublic class CouponDistributionService {@Autowiredprivate RedisTemplate<String, String> redisTemplate;public boolean acquireCoupon(Long templateId, Long userId) {String lockKey = "coupon_lock_" + templateId;try {boolean locked = redisTemplate.opsForValue().setIfAbsent(lockKey, "locked", 10, TimeUnit.SECONDS);if (!locked) {throw new RuntimeException("操作太频繁,请稍后再试");}// 双重检查库存CouponTemplate template = couponTemplateRepo.findById(templateId).orElseThrow(() -> new RuntimeException("优惠券不存在"));if (template.getRemaining() <= 0) {throw new RuntimeException("优惠券已领完");}// 扣减库存int updated = couponTemplateRepo.decreaseRemaining(templateId);if (updated == 0) {throw new RuntimeException("库存不足");}// 创建用户券UserCoupon coupon = new UserCoupon();coupon.setTemplate(template);coupon.setCode(generateCouponCode());coupon.setUser(userRepo.findById(userId).get());userCouponRepo.save(coupon);return true;} finally {redisTemplate.delete(lockKey);}}}
2.2 优惠券核销的幂等性设计
核销接口需处理重复调用问题,可采用Token机制:
@RestController@RequestMapping("/api/coupons")public class CouponController {@PostMapping("/use")public ResponseEntity<?> useCoupon(@RequestHeader("X-Request-Token") String token,@RequestParam String couponCode) {// 验证Token唯一性String usedToken = redisTemplate.opsForValue().get("used_token_" + token);if (usedToken != null) {return ResponseEntity.badRequest().body("请求已处理");}try {couponService.useCoupon(couponCode);redisTemplate.opsForValue().set("used_token_" + token, "1", 24, TimeUnit.HOURS);return ResponseEntity.ok().build();} catch (Exception e) {return ResponseEntity.status(500).body(e.getMessage());}}}
三、系统优化与扩展设计
3.1 优惠券分片存储策略
当数据量超过单表500万条时,建议按模板ID进行分库分表:
@Table(name = "user_coupon")@ShardingSphereTable(shardingAlgorithmName = "coupon-mod",databaseStrategy = "standard",databaseShardingColumn = "template_id",tableStrategy = "standard",tableShardingColumn = "template_id")public class UserCoupon {// 实体定义同上}// 自定义分片算法public class CouponModShardingAlgorithm implements PreciseShardingAlgorithm<Long> {@Overridepublic String doSharding(Collection<String> availableTargetNames,PreciseShardingValue<Long> shardingValue) {long templateId = shardingValue.getValue();int tableIndex = (int)(templateId % 4); // 4个分表return "user_coupon_" + tableIndex;}}
3.2 异步任务处理架构
优惠券过期检查适合使用Spring Batch+Quartz实现:
@Configurationpublic class BatchConfig {@Beanpublic Job couponExpiryJob(JobRepository jobRepository,Step couponExpiryStep) {return new JobBuilder("couponExpiryJob", jobRepository).start(couponExpiryStep).build();}@Beanpublic Step couponExpiryStep(StepBuilderFactory stepBuilderFactory,ItemReader<UserCoupon> reader,ItemProcessor<UserCoupon, ExpiryResult> processor,ItemWriter<ExpiryResult> writer) {return stepBuilderFactory.get("couponExpiryStep").<UserCoupon, ExpiryResult>chunk(100).reader(reader).processor(processor).writer(writer).build();}}// Quartz调度配置@Configurationpublic class QuartzConfig {@Beanpublic JobDetail couponExpiryJobDetail() {return JobBuilder.newJob(CouponExpiryJob.class).withIdentity("couponExpiryJob").storeDurably().build();}@Beanpublic Trigger couponExpiryTrigger() {SimpleScheduleBuilder scheduleBuilder = SimpleScheduleBuilder.simpleSchedule().withIntervalInHours(1) // 每小时执行.repeatForever();return TriggerBuilder.newTrigger().forJob(couponExpiryJobDetail()).withIdentity("couponExpiryTrigger").withSchedule(scheduleBuilder).build();}}
四、最佳实践与经验总结
- 券码生成策略:采用雪花算法+业务前缀组合,如”DISC_”+snowflakeId,保证全局唯一且可读
- 防刷机制:
- 用户维度:同一用户10分钟内限领3张
- IP维度:同一IP每小时限领20张
- 设备维度:同一设备每天限领5张
- 数据一致性方案:
- 最终一致性:通过消息队列实现异步更新
- 强一致性:使用Seata等分布式事务框架
- 监控指标:
- 发放成功率:成功数/请求数
- 核销率:已使用数/发放总数
- 库存预警:剩余量<10%时告警
实际项目实施中,建议采用渐进式架构演进:初期单库单表+缓存,中期引入分库分表,后期考虑服务化拆分。测试环境需模拟10万级QPS压力测试,重点关注锁竞争、数据库连接池耗尽等典型问题。