Java实现优惠券领取系统:从设计到落地的完整方案

一、系统架构设计

1.1 分层架构设计

优惠券系统需遵循清晰的分层原则,推荐采用Spring Boot框架构建四层架构:

  • 表现层:RESTful API接口(Spring MVC)
  • 业务层:Service组件(@Service注解)
  • 数据访问层:MyBatis/JPA持久层
  • 工具层:Redis缓存、分布式锁等

典型接口设计示例:

  1. @RestController
  2. @RequestMapping("/api/coupon")
  3. public class CouponController {
  4. @Autowired
  5. private CouponService couponService;
  6. @PostMapping("/receive")
  7. public Result<Boolean> receiveCoupon(@RequestBody CouponReceiveRequest request) {
  8. return couponService.receiveCoupon(request);
  9. }
  10. }

1.2 微服务化考虑

对于高并发场景,建议拆分独立服务:

  • Coupon-Template-Service:管理优惠券模板
  • Coupon-Stock-Service:库存控制服务
  • Coupon-Receive-Service:领取核心服务

通过Spring Cloud实现服务注册与发现,使用Feign进行服务间调用。

二、数据库设计核心

2.1 核心表结构

  1. CREATE TABLE coupon_template (
  2. id BIGINT PRIMARY KEY AUTO_INCREMENT,
  3. name VARCHAR(50) NOT NULL,
  4. type TINYINT COMMENT '1-折扣 2-满减 3-现金券',
  5. discount DECIMAL(10,2),
  6. threshold DECIMAL(10,2),
  7. total_count INT DEFAULT 0,
  8. remaining_count INT DEFAULT 0,
  9. start_time DATETIME,
  10. end_time DATETIME,
  11. status TINYINT DEFAULT 1 COMMENT '1-有效 0-失效'
  12. );
  13. CREATE TABLE coupon_record (
  14. id BIGINT PRIMARY KEY AUTO_INCREMENT,
  15. template_id BIGINT NOT NULL,
  16. user_id BIGINT NOT NULL,
  17. order_id BIGINT,
  18. status TINYINT DEFAULT 0 COMMENT '0-未使用 1-已使用 2-已过期',
  19. receive_time DATETIME DEFAULT CURRENT_TIMESTAMP,
  20. use_time DATETIME,
  21. FOREIGN KEY (template_id) REFERENCES coupon_template(id)
  22. );

2.2 索引优化策略

  • 模板表:(status, end_time)复合索引
  • 记录表:(user_id, status)用户查询索引
  • 模板ID单列索引加速关联查询

三、核心业务实现

3.1 领取流程实现

关键步骤伪代码:

  1. public Result<Boolean> receiveCoupon(CouponReceiveRequest request) {
  2. // 1. 参数校验
  3. validateRequest(request);
  4. // 2. 分布式锁控制(基于Redis)
  5. String lockKey = "coupon:lock:" + request.getTemplateId();
  6. try {
  7. boolean locked = redisTemplate.opsForValue().setIfAbsent(lockKey, "1", 3, TimeUnit.SECONDS);
  8. if (!locked) {
  9. return Result.fail("系统繁忙,请稍后重试");
  10. }
  11. // 3. 库存校验与扣减
  12. CouponTemplate template = templateDao.selectById(request.getTemplateId());
  13. if (template.getRemainingCount() <= 0) {
  14. return Result.fail("优惠券已领完");
  15. }
  16. // 4. 创建领取记录
  17. CouponRecord record = new CouponRecord();
  18. record.setTemplateId(template.getId());
  19. record.setUserId(request.getUserId());
  20. recordDao.insert(record);
  21. // 5. 更新库存(原子操作)
  22. int updated = templateDao.decreaseStock(template.getId());
  23. if (updated == 0) {
  24. throw new RuntimeException("库存更新失败");
  25. }
  26. return Result.success(true);
  27. } finally {
  28. redisTemplate.delete(lockKey);
  29. }
  30. }

3.2 库存控制方案

方案对比:

方案 优点 缺点 适用场景
数据库乐观锁 实现简单 高并发下性能差 低并发系统
Redis原子操作 高性能 需要额外存储 中高并发
分布式锁+数据库 强一致性 实现复杂 金融级系统

推荐方案:Redis原子操作+本地缓存

  1. // Redis库存扣减示例
  2. public boolean decreaseStockWithRedis(Long templateId) {
  3. String key = "coupon:stock:" + templateId;
  4. Long stock = redisTemplate.opsForValue().get(key);
  5. if (stock == null || stock <= 0) {
  6. return false;
  7. }
  8. // 使用Lua脚本保证原子性
  9. String luaScript = "if redis.call('get', KEYS[1]) >= tonumber(ARGV[1]) then " +
  10. "return redis.call('decrby', KEYS[1], ARGV[1]) " +
  11. "else return 0 end";
  12. Long result = redisTemplate.execute(
  13. new DefaultRedisScript<>(luaScript, Long.class),
  14. Collections.singletonList(key),
  15. 1
  16. );
  17. return result != null && result >= 0;
  18. }

四、高并发优化策略

4.1 缓存架构设计

  • 多级缓存:本地缓存(Caffeine)+ 分布式缓存(Redis)
  • 缓存预热:系统启动时加载热销优惠券
  • 缓存更新:采用CANAL监听MySQL binlog实现缓存同步

4.2 异步处理方案

对于非实时性要求高的操作(如发送领取通知),采用消息队列:

  1. @Async
  2. public void sendCouponNotice(Long userId, Long couponId) {
  3. // 1. 查询用户信息
  4. User user = userDao.selectById(userId);
  5. // 2. 查询优惠券信息
  6. CouponTemplate template = templateDao.selectById(couponId);
  7. // 3. 构建消息内容
  8. String content = String.format("恭喜您领取到%s优惠券", template.getName());
  9. // 4. 发送消息(实际可接入短信/推送服务)
  10. messageService.send(user.getPhone(), content);
  11. }

4.3 限流策略实现

使用Guava RateLimiter或Redis实现:

  1. // 基于Redis的令牌桶算法
  2. public boolean tryAcquire(String key, int permits, int timeoutSeconds) {
  3. String luaScript = "local current = redis.call('get', KEYS[1]) " +
  4. "if current == false then " +
  5. " redis.call('set', KEYS[1], ARGV[2], 'EX', ARGV[3]) " +
  6. " current = ARGV[2] " +
  7. "end " +
  8. "local new = tonumber(current) - tonumber(ARGV[1]) " +
  9. "if new >= 0 then " +
  10. " redis.call('set', KEYS[1], new, 'EX', ARGV[3]) " +
  11. " return 1 " +
  12. "else " +
  13. " return 0 " +
  14. "end";
  15. Long result = redisTemplate.execute(
  16. new DefaultRedisScript<>(luaScript, Long.class),
  17. Collections.singletonList(key),
  18. permits,
  19. 100, // 初始令牌数
  20. timeoutSeconds
  21. );
  22. return result != null && result == 1;
  23. }

五、安全与监控

5.1 防刷策略实现

  • 用户维度限流:单个用户每分钟最多领取5张
  • IP维度限流:单个IP每小时最多100次请求
  • 行为分析:记录用户领取路径,识别异常模式

5.2 监控指标设计

关键监控项:
| 指标 | 告警阈值 | 监控方式 |
|———|—————|—————|
| 领取成功率 | <95% | Prometheus+Grafana |
| 平均响应时间 | >500ms | Spring Boot Actuator |
| 库存不一致率 | >0.1% | 定时核对任务 |
| 接口错误率 | >1% | ELK日志分析 |

六、部署与运维

6.1 容器化部署方案

Dockerfile示例:

  1. FROM openjdk:11-jre-slim
  2. VOLUME /tmp
  3. ARG JAR_FILE=target/coupon-service.jar
  4. COPY ${JAR_FILE} app.jar
  5. ENTRYPOINT ["java","-Djava.security.egd=file:/dev/./urandom","-jar","/app.jar"]

Kubernetes部署配置要点:

  • 资源限制:requests/limits设置
  • 健康检查:liveness/readiness探针
  • 自动扩缩:基于CPU/内存的HPA

6.2 灾备方案设计

  • 数据备份:每日全量备份+实时binlog同步
  • 多活部署:同城双活+异地灾备
  • 熔断机制:Hystrix实现服务降级

七、最佳实践建议

  1. 库存预热:活动开始前10分钟加载库存到Redis
  2. 异步削峰:将领取请求写入MQ后立即返回,后端异步处理
  3. 灰度发布:新优惠券模板先小流量测试
  4. 数据核对:每日核对库存与记录表一致性
  5. 压力测试:使用JMeter模拟5000QPS进行压测

通过以上完整方案,可构建出支持百万级日活的优惠券领取系统。实际开发中需根据具体业务场景调整技术选型,建议先实现核心领取流程,再逐步完善周边功能。