简介:本文详细探讨Java环境下优惠券系统的设计要点,涵盖数据库建模、业务规则实现、分布式锁应用及高并发场景优化,提供可落地的技术方案。
优惠券系统需包含四大核心实体:优惠券模板(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;}
优惠券使用需满足多重条件校验,建议采用策略模式实现规则引擎:
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));}}
在秒杀场景下,需使用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);}}}
核销接口需处理重复调用问题,可采用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());}}}
当数据量超过单表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;}}
优惠券过期检查适合使用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();}}
实际项目实施中,建议采用渐进式架构演进:初期单库单表+缓存,中期引入分库分表,后期考虑服务化拆分。测试环境需模拟10万级QPS压力测试,重点关注锁竞争、数据库连接池耗尽等典型问题。