Java优惠券系统设计:从数据模型到高并发实践
一、优惠券系统核心数据模型设计
优惠券系统的数据模型需满足业务灵活性与查询效率的双重需求。核心实体包括Coupon(优惠券)、CouponTemplate(优惠券模板)、UserCoupon(用户优惠券)和Order(订单),四者通过外键关联形成完整业务闭环。
CouponTemplate采用模板化设计,关键字段包含:
public class CouponTemplate {private Long id;private String name; // 模板名称private String logo; // 优惠图标private String intro; // 优惠说明private String category; // 分类:满减/折扣/无门槛private String productType; // 商品类型限制private BigDecimal conditionPrice; // 满减门槛private BigDecimal discountPrice; // 减免金额private BigDecimal ratio; // 折扣率private Date startTime; // 生效时间private Date endTime; // 过期时间private Integer rule; // 使用规则:1-单次 2-每日 3-每周private Integer status; // 状态:0-草稿 1-上线 2-下线private Integer total; // 发行总量private Integer inventory; // 剩余库存}
这种设计支持动态规则配置,例如通过category字段区分满100减20(category=”FULL_REDUCTION”)与8折优惠(category=”DISCOUNT”),productType字段实现定向商品发放。
二、优惠券状态机与生命周期管理
优惠券状态流转需严格遵循业务规则,核心状态包括:
- 未领取(INITIAL):模板创建后初始状态
- 已领取(RECEIVED):用户领取但未使用
- 已使用(USED):订单支付时核销
- 已过期(EXPIRED):超过有效期
- 已作废(INVALID):手动回收
状态转换需通过有限状态机(FSM)控制,示例代码:
public enum CouponState {INITIAL {@Overridepublic boolean canTransitionTo(CouponState newState) {return newState == RECEIVED || newState == INVALID;}},RECEIVED {@Overridepublic boolean canTransitionTo(CouponState newState) {return newState == USED || newState == EXPIRED || newState == INVALID;}},// 其他状态定义...public abstract boolean canTransitionTo(CouponState newState);}// 状态转换验证public void transitionState(Coupon coupon, CouponState newState) {if (!coupon.getState().canTransitionTo(newState)) {throw new IllegalStateException("Invalid state transition");}coupon.setState(newState);// 持久化更新...}
通过状态机确保业务规则强制执行,例如防止已使用优惠券被重复核销。
三、分布式环境下的并发控制策略
高并发场景下的优惠券领取需解决超发问题,典型解决方案包括:
数据库乐观锁:
@Update("UPDATE user_coupon SET inventory = inventory - 1 " +"WHERE template_id = #{templateId} AND inventory > 0")int reduceInventory(@Param("templateId") Long templateId);
当返回影响行数为0时触发重试机制,适合并发量较低的场景。
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
” + 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);
// 持久化…
// 更新库存couponTemplateMapper.reduceInventory(templateId);}}} finally {releaseLock(lockKey, lockValue);}
}
通过SETNX实现原子锁,结合唯一请求ID防止误删其他客户端锁。3. **Redis原子计数器**:```javapublic boolean tryReceiveWithRedis(Long templateId, Long userId) {String inventoryKey = "coupon:inventory:" + templateId;Long remaining = redisTemplate.opsForValue().decrement(inventoryKey);if (remaining >= 0) {// 创建用户优惠券createUserCoupon(userId, templateId);return true;} else {// 回滚计数器redisTemplate.opsForValue().increment(inventoryKey);return false;}}
需配合定时任务将Redis库存同步至数据库,保证数据最终一致性。
四、优惠券核销与防刷策略
核销环节需实现三重校验:
- 状态校验:仅允许RECEIVED状态优惠券核销
- 有效期校验:当前时间在[startTime, endTime]区间内
- 规则校验:
- 满减券需验证订单金额≥conditionPrice
- 商品类型券需验证订单商品∈productType
防刷策略实现:
public class AntiFraudService {// 用户维度限频public boolean checkUserFrequency(Long userId, Long templateId) {String key = "antifraud:user:" + userId + ":template:" + templateId;Long count = redisTemplate.opsForValue().increment(key);if (count == 1) {redisTemplate.expire(key, 24, TimeUnit.HOURS);}return count <= 5; // 每日最多使用5次}// 设备指纹校验public boolean checkDeviceFingerprint(String fingerprint) {// 实现设备指纹黑名单校验return true;}}
五、性能优化实践
多级缓存架构:
- Redis缓存热点优惠券模板(TTL=5分钟)
- Caffeine本地缓存用户已领取优惠券(TTL=10分钟)
- 数据库分库分表(按template_id哈希分4库)
异步化处理:
- 优惠券发放使用RabbitMQ延迟队列处理过期通知
- 核销日志通过MQ异步落库
查询优化:
-- 用户可用优惠券查询(索引优化)SELECT uc.* FROM user_coupon ucJOIN coupon_template ct ON uc.template_id = ct.idWHERE uc.user_id = ?AND uc.state = 'RECEIVED'AND ct.end_time > NOW()AND (ct.product_type IS NULL OR ct.product_type IN (?))
六、系统扩展性设计
规则引擎集成:采用Drools实现复杂优惠规则管理,示例规则:
rule "满300减50"when$order : Order(totalPrice >= 300)$coupon : Coupon(category == "FULL_REDUCTION" && conditionPrice <= 300)then$order.setDiscount($coupon.getDiscountPrice());end
动态配置中心:通过Apollo实现阈值动态调整,如:
coupon:receive:maxPerDay: 10riskThreshold: 0.8expire:notifyBeforeDays: 3
监控告警体系:
- Prometheus采集优惠券领取QPS、成功率
- Grafana展示库存预警看板
- 告警规则:连续5分钟领取失败率>10%触发告警
七、典型业务场景实现
批量发放优惠券:
@Transactionalpublic void batchIssue(List<Long> userIds, Long templateId) {CouponTemplate template = couponTemplateMapper.selectById(templateId);if (template.getInventory() < userIds.size()) {throw new BusinessException("库存不足");}List<UserCoupon> coupons = userIds.stream().map(userId -> {UserCoupon coupon = new UserCoupon();coupon.setUserId(userId);coupon.setTemplateId(templateId);// 其他字段设置...return coupon;}).collect(Collectors.toList());userCouponMapper.batchInsert(coupons);couponTemplateMapper.reduceInventory(templateId, userIds.size());}
优惠券过期自动回收:
@Scheduled(cron = "0 0 2 * * ?") // 每天凌晨2点执行public void expireCouponRecovery() {Date now = new Date();List<UserCoupon> expiredCoupons = userCouponMapper.selectExpired(now);expiredCoupons.forEach(coupon -> {coupon.setState(CouponState.EXPIRED);// 更新库存couponTemplateMapper.recoverInventory(coupon.getTemplateId());});userCouponMapper.batchUpdateState(expiredCoupons);}
八、测试验证要点
- 并发测试:使用JMeter模拟2000用户并发领取,验证库存准确性
- 异常测试:
- 数据库连接中断时系统降级处理
- Redis主从切换时数据一致性
- 性能测试:
- 单优惠券查询P99<200ms
- 批量发放10000张优惠券耗时<3s
通过上述设计,系统可支撑千万级优惠券发放,日均百万级核销请求,在618、双11等大促期间保持99.95%以上可用性。实际实施时需根据具体业务规模调整分库分表策略和缓存粒度。