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

一、优惠券系统核心数据模型设计

优惠券系统的数据模型需满足业务灵活性与查询效率的双重需求。核心实体包括Coupon(优惠券)、CouponTemplate(优惠券模板)、UserCoupon(用户优惠券)和Order(订单),四者通过外键关联形成完整业务闭环。

CouponTemplate采用模板化设计,关键字段包含:

  1. public class CouponTemplate {
  2. private Long id;
  3. private String name; // 模板名称
  4. private String logo; // 优惠图标
  5. private String intro; // 优惠说明
  6. private String category; // 分类:满减/折扣/无门槛
  7. private String productType; // 商品类型限制
  8. private BigDecimal conditionPrice; // 满减门槛
  9. private BigDecimal discountPrice; // 减免金额
  10. private BigDecimal ratio; // 折扣率
  11. private Date startTime; // 生效时间
  12. private Date endTime; // 过期时间
  13. private Integer rule; // 使用规则:1-单次 2-每日 3-每周
  14. private Integer status; // 状态:0-草稿 1-上线 2-下线
  15. private Integer total; // 发行总量
  16. private Integer inventory; // 剩余库存
  17. }

这种设计支持动态规则配置,例如通过category字段区分满100减20(category=”FULL_REDUCTION”)与8折优惠(category=”DISCOUNT”),productType字段实现定向商品发放。

二、优惠券状态机与生命周期管理

优惠券状态流转需严格遵循业务规则,核心状态包括:

  1. 未领取(INITIAL):模板创建后初始状态
  2. 已领取(RECEIVED):用户领取但未使用
  3. 已使用(USED):订单支付时核销
  4. 已过期(EXPIRED):超过有效期
  5. 已作废(INVALID):手动回收

状态转换需通过有限状态机(FSM)控制,示例代码:

  1. public enum CouponState {
  2. INITIAL {
  3. @Override
  4. public boolean canTransitionTo(CouponState newState) {
  5. return newState == RECEIVED || newState == INVALID;
  6. }
  7. },
  8. RECEIVED {
  9. @Override
  10. public boolean canTransitionTo(CouponState newState) {
  11. return newState == USED || newState == EXPIRED || newState == INVALID;
  12. }
  13. },
  14. // 其他状态定义...
  15. public abstract boolean canTransitionTo(CouponState newState);
  16. }
  17. // 状态转换验证
  18. public void transitionState(Coupon coupon, CouponState newState) {
  19. if (!coupon.getState().canTransitionTo(newState)) {
  20. throw new IllegalStateException("Invalid state transition");
  21. }
  22. coupon.setState(newState);
  23. // 持久化更新...
  24. }

通过状态机确保业务规则强制执行,例如防止已使用优惠券被重复核销。

三、分布式环境下的并发控制策略

高并发场景下的优惠券领取需解决超发问题,典型解决方案包括:

  1. 数据库乐观锁

    1. @Update("UPDATE user_coupon SET inventory = inventory - 1 " +
    2. "WHERE template_id = #{templateId} AND inventory > 0")
    3. int reduceInventory(@Param("templateId") Long templateId);

    当返回影响行数为0时触发重试机制,适合并发量较低的场景。

  2. Redis分布式锁
    ```java
    public boolean acquireLock(String key, String value, long expireTime) {
    return redisTemplate.opsForValue().setIfAbsent(key, value, expireTime, TimeUnit.SECONDS);
    }

// 领取逻辑示例
public void receiveCoupon(Long userId, Long templateId) {
String lockKey = “lock:coupon:” + templateId;
String lockValue = UUID.randomUUID().toString();
try {
if (acquireLock(lockKey, lockValue, 10)) {
// 双重检查库存
CouponTemplate template = couponTemplateMapper.selectById(templateId);
if (template.getInventory() > 0) {
// 创建用户优惠券
UserCoupon userCoupon = new UserCoupon();
userCoupon.setUserId(userId);
userCoupon.setTemplateId(templateId);
// 持久化…

  1. // 更新库存
  2. couponTemplateMapper.reduceInventory(templateId);
  3. }
  4. }
  5. } finally {
  6. releaseLock(lockKey, lockValue);
  7. }

}

  1. 通过SETNX实现原子锁,结合唯一请求ID防止误删其他客户端锁。
  2. 3. **Redis原子计数器**:
  3. ```java
  4. public boolean tryReceiveWithRedis(Long templateId, Long userId) {
  5. String inventoryKey = "coupon:inventory:" + templateId;
  6. Long remaining = redisTemplate.opsForValue().decrement(inventoryKey);
  7. if (remaining >= 0) {
  8. // 创建用户优惠券
  9. createUserCoupon(userId, templateId);
  10. return true;
  11. } else {
  12. // 回滚计数器
  13. redisTemplate.opsForValue().increment(inventoryKey);
  14. return false;
  15. }
  16. }

需配合定时任务将Redis库存同步至数据库,保证数据最终一致性。

四、优惠券核销与防刷策略

核销环节需实现三重校验:

  1. 状态校验:仅允许RECEIVED状态优惠券核销
  2. 有效期校验:当前时间在[startTime, endTime]区间内
  3. 规则校验
    • 满减券需验证订单金额≥conditionPrice
    • 商品类型券需验证订单商品∈productType

防刷策略实现:

  1. public class AntiFraudService {
  2. // 用户维度限频
  3. public boolean checkUserFrequency(Long userId, Long templateId) {
  4. String key = "antifraud:user:" + userId + ":template:" + templateId;
  5. Long count = redisTemplate.opsForValue().increment(key);
  6. if (count == 1) {
  7. redisTemplate.expire(key, 24, TimeUnit.HOURS);
  8. }
  9. return count <= 5; // 每日最多使用5次
  10. }
  11. // 设备指纹校验
  12. public boolean checkDeviceFingerprint(String fingerprint) {
  13. // 实现设备指纹黑名单校验
  14. return true;
  15. }
  16. }

五、性能优化实践

  1. 多级缓存架构

    • Redis缓存热点优惠券模板(TTL=5分钟)
    • Caffeine本地缓存用户已领取优惠券(TTL=10分钟)
    • 数据库分库分表(按template_id哈希分4库)
  2. 异步化处理

    • 优惠券发放使用RabbitMQ延迟队列处理过期通知
    • 核销日志通过MQ异步落库
  3. 查询优化

    1. -- 用户可用优惠券查询(索引优化)
    2. SELECT uc.* FROM user_coupon uc
    3. JOIN coupon_template ct ON uc.template_id = ct.id
    4. WHERE uc.user_id = ?
    5. AND uc.state = 'RECEIVED'
    6. AND ct.end_time > NOW()
    7. AND (ct.product_type IS NULL OR ct.product_type IN (?))

六、系统扩展性设计

  1. 规则引擎集成:采用Drools实现复杂优惠规则管理,示例规则:

    1. rule "满300减50"
    2. when
    3. $order : Order(totalPrice >= 300)
    4. $coupon : Coupon(category == "FULL_REDUCTION" && conditionPrice <= 300)
    5. then
    6. $order.setDiscount($coupon.getDiscountPrice());
    7. end
  2. 动态配置中心:通过Apollo实现阈值动态调整,如:

    1. coupon:
    2. receive:
    3. maxPerDay: 10
    4. riskThreshold: 0.8
    5. expire:
    6. notifyBeforeDays: 3
  3. 监控告警体系

    • Prometheus采集优惠券领取QPS、成功率
    • Grafana展示库存预警看板
    • 告警规则:连续5分钟领取失败率>10%触发告警

七、典型业务场景实现

  1. 批量发放优惠券

    1. @Transactional
    2. public void batchIssue(List<Long> userIds, Long templateId) {
    3. CouponTemplate template = couponTemplateMapper.selectById(templateId);
    4. if (template.getInventory() < userIds.size()) {
    5. throw new BusinessException("库存不足");
    6. }
    7. List<UserCoupon> coupons = userIds.stream()
    8. .map(userId -> {
    9. UserCoupon coupon = new UserCoupon();
    10. coupon.setUserId(userId);
    11. coupon.setTemplateId(templateId);
    12. // 其他字段设置...
    13. return coupon;
    14. })
    15. .collect(Collectors.toList());
    16. userCouponMapper.batchInsert(coupons);
    17. couponTemplateMapper.reduceInventory(templateId, userIds.size());
    18. }
  2. 优惠券过期自动回收

    1. @Scheduled(cron = "0 0 2 * * ?") // 每天凌晨2点执行
    2. public void expireCouponRecovery() {
    3. Date now = new Date();
    4. List<UserCoupon> expiredCoupons = userCouponMapper.selectExpired(now);
    5. expiredCoupons.forEach(coupon -> {
    6. coupon.setState(CouponState.EXPIRED);
    7. // 更新库存
    8. couponTemplateMapper.recoverInventory(coupon.getTemplateId());
    9. });
    10. userCouponMapper.batchUpdateState(expiredCoupons);
    11. }

八、测试验证要点

  1. 并发测试:使用JMeter模拟2000用户并发领取,验证库存准确性
  2. 异常测试
    • 数据库连接中断时系统降级处理
    • Redis主从切换时数据一致性
  3. 性能测试
    • 单优惠券查询P99<200ms
    • 批量发放10000张优惠券耗时<3s

通过上述设计,系统可支撑千万级优惠券发放,日均百万级核销请求,在618、双11等大促期间保持99.95%以上可用性。实际实施时需根据具体业务规模调整分库分表策略和缓存粒度。