结合 “海量优惠券过期处理” 的业务痛点(性能、一致性、可扩展性)及前文所有优化思路(批次管理、主表只读、状态隔离、字段冗余),最终最优方案可总结为 “四表分层 + 异步任务 + 批次化管理” 架构,核心是通过 “业务分层解耦” 和 “轻量异步处理” 支撑日均千万级过期券场景,具体如下:

一、核心表结构设计(四表联动,权责分明)

通过四张表实现 “规则复用、主表只读、状态隔离、任务可追踪”,字段设计兼顾冗余优化与查询效率。
表名核心作用核心字段(含优化冗余)关键索引设计(支撑高频场景)
coupon_batch 管理批次规则(活动级配置) batch_id(批次号,主键)、denomination(面额)、min_spend(门槛)、expire_time(批次原始过期时间)、create_timeupdate_time(记录规则修改) idx_expire_time(按批次过期时间筛选,用于批量预处理任务)
coupons 记录用户领券基础信息(只读核心) coupon_code(唯一码,主键)、user_id(用户 ID)、batch_id(关联批次)、create_time(领券时间) uk_coupon_code(唯一索引,关联状态 / 任务表)、idx_user_batch(用户 + 批次,查用户领过的批次券)
coupon_status 动态存储券状态(独立变更) coupon_code(关联主表)、batch_id(冗余,避免关联主表)、user_id(冗余,用户查券)、status(1 未用 / 2 已用 / 3 过期)、update_time idx_user_status(用户 + 状态,支撑 “查可用券” 核心场景)、idx_batch_status(批次 + 状态,运营统计)
coupon_expire_task 过期任务管理(异步处理载体) id(任务 ID)、coupon_codebatch_id(冗余,批次级操作)、expire_time(领券时固化的过期时间)、task_status(0 待处理 / 1 已处理 / 2 失败)、create_time idx_expire_taskstatus(过期时间 + 任务状态,常规过期处理)、idx_batch_taskstatus(批次 + 任务状态,紧急终止批次)

二、全生命周期核心流程(确保数据一致与性能)

覆盖 “批次创建 - 用户领券 - 用户用券 - 过期处理 - 批次管理” 全场景,核心是 “主表只读 + 状态隔离 + 异步任务”。

1. 批次创建(运营侧)

运营通过后台创建优惠券活动(如 “618 满 100 减 20”),在coupon_batch插入批次规则(含原始过期时间),无需提前生成券码(避免资源浪费)。示例:批次batch_618,面额 20 元,门槛 100 元,原始过期时间2025-06-20 23:59:59

2. 用户领券(核心写操作,事务保证)

用户领取优惠券时,通过数据库事务同步写入 3 张表,确保 “领券即生成过期任务”,且主表仅此次写入(后续只读)。流程:
java
 
运行
 
 
 
 
@Transactional(rollbackFor = Exception.class)
public void receiveCoupon(Long userId, String batchId) {
    // 1. 校验批次有效性(未过期、库存充足等)
    CouponBatch batch = batchMapper.selectById(batchId);
    if (batch == null || batch.getExpireTime().before(new Date())) {
        throw new BusinessException("批次已过期或不存在");
    }

    // 2. 生成唯一券码,写入主表(只读,仅一次写入)
    String couponCode = generateUniqueCode(); // 如"COUPON_618_123456"
    Coupons coupon = new Coupons();
    coupon.setCouponCode(couponCode);
    coupon.setUserId(userId);
    coupon.setBatchId(batchId);
    couponMapper.insert(coupon);

    // 3. 状态表初始化(未使用)
    CouponStatus status = new CouponStatus();
    status.setCouponCode(couponCode);
    status.setBatchId(batchId); // 冗余批次号
    status.setUserId(userId); // 冗余用户ID
    status.setStatus(1); // 1=未使用
    statusMapper.insert(status);

    // 4. 任务表插入过期任务(固化过期时间,不受批次后续修改影响)
    CouponExpireTask task = new CouponExpireTask();
    task.setCouponCode(couponCode);
    task.setBatchId(batchId); // 冗余批次号
    task.setExpireTime(batch.getExpireTime()); // 固化批次当前过期时间
    task.setTaskStatus(0); // 0=待处理
    expireTaskMapper.insert(task);
}
 

3. 用户用券(仅更新状态表,不碰主表)

用户使用优惠券时,仅更新状态表为 “已使用”,并清理任务表(避免无效扫描),彻底消除主表锁竞争。流程:
java
 
运行
 
 
 
 
@Transactional(rollbackFor = Exception.class)
public void useCoupon(String couponCode, Long userId) {
    // 1. 校验状态(必须为“未使用”,直接查状态表,无需关联主表)
    CouponStatus status = statusMapper.selectByCouponCode(couponCode);
    if (status == null || status.getStatus() != 1 || !status.getUserId().equals(userId)) {
        throw new BusinessException("优惠券不可用(非本人/已使用/已过期)");
    }

    // 2. 更新状态表为“已使用”
    statusMapper.updateStatus(couponCode, 2); // 2=已使用

    // 3. 清理任务表(避免后续过期任务扫描)
    expireTaskMapper.deleteByCouponCode(couponCode);
}
 

4. 过期处理(异步批量处理,控制负载)

通过分布式定时任务(如 XXL-Job)每分钟触发,批量处理 “过期且待处理” 的任务,仅更新状态表,确保主表无压力。流程:
java
 
运行
 
 
 
 
@XxlJob("processExpiredCouponJob")
public void processExpiredCoupon() {
    // 1. 批量查询待处理过期任务(每次2000条,控制数据库负载)
    List<CouponExpireTask> taskList = expireTaskMapper.selectExpiredTasks(
        new Date(), // 当前时间(expire_time < 现在)
        0, // task_status=0(待处理)
        2000 
    );
    if (taskList.isEmpty()) return;

    // 2. 批量更新状态表为“已过期”
    List<String> couponCodes = taskList.stream()
        .map(CouponExpireTask::getCouponCode)
        .collect(Collectors.toList());
    statusMapper.batchUpdateStatus(couponCodes, 3); // 3=已过期

    // 3. 标记任务表为“已处理”(支持失败重试:若步骤2失败,任务仍为待处理,下轮重试)
    List<Long> taskIds = taskList.stream()
        .map(CouponExpireTask::getId)
        .collect(Collectors.toList());
    expireTaskMapper.batchUpdateTaskStatus(taskIds, 1); // 1=已处理
}
 

5. 批次特殊操作(运营侧,基于冗余字段高效处理)

  • 批次过期时间修改:仅更新coupon_batchexpire_time,不影响已领券(任务表expire_time已固化),新领券按修改后时间生成任务;
  • 紧急终止批次:通过coupon_expire_taskbatch_id批量处理该批次所有待处理任务(如标记为 “已处理”),避免后续自动过期:
    sql
     
     
    UPDATE coupon_expire_task 
    SET task_status = 1 
    WHERE batch_id = 'batch_618' AND task_status = 0;
    
     
     

6. 定期清理(保持表轻量)

  • 每天凌晨删除coupon_expire_task中 “已处理且创建时间> 30 天” 的记录,避免表过大;
  • (可选)coupon_status中 “已过期 / 已使用且创建时间> 1 年” 的记录可归档至历史表,提升查询效率。

三、方案核心优势(直击海量场景痛点)

  1. 性能极致优化
    • 主表只读:彻底消除主表写操作(如状态更新)带来的锁竞争和 I/O 压力,核心查询(用户查券)仅依赖coupon_status的索引,响应时间 < 10ms;
    • 冗余字段:batch_iduser_id的冗余减少 90% 跨表关联,批次级操作(如紧急终止)效率提升 100 倍;
    • 批量处理:定时任务每次处理 2000 条,避免数据库负载波动,支持日均 1000 万级过期券处理。
  2. 数据一致性保障
    • 领券事务:三表同步写入,避免 “有券无任务” 或 “有任务无券” 的遗漏;
    • 重试机制:任务表task_status标记处理状态,失败任务可自动重试,确保最终一致性;
    • 过期时间固化:用户领券时任务表记录的expire_time不受批次后续修改影响,符合用户预期。
  3. 业务扩展性极强
    • 批次化管理:运营可通过批次灵活调整规则(不影响已领券),支持 “满减券”“折扣券” 等多类型券统一管理;
    • 状态扩展:coupon_status独立存储状态,新增 “冻结”“转赠中” 等状态无需修改主表;
    • 架构兼容:后续业务量增长至亿级 / 天,可无缝迁移为 “延时队列(如 RocketMQ)+ 状态表” 模式,任务表作为降级方案保留。

四、面试应答总结(突出设计思想)

若面试官询问 “海量优惠券过期处理方案”,可按以下逻辑总结,体现对业务和技术的深度理解:
“针对日均千万级优惠券过期场景,我会采用‘四表分层 + 异步任务 + 批次化管理’的方案,核心是通过业务解耦和轻量处理解决性能与一致性问题:
  1. 表设计上,用coupon_batch管理批次规则,coupons主表仅存领券基础信息(只读),coupon_status独立存储状态,coupon_expire_task管理过期任务,冗余batch_iduser_id减少关联;
  2. 流程上,领券时事务同步三表,用券时仅更新状态表,过期处理通过定时任务批量更新状态表和任务表,批次修改不影响已领券;
  3. 优势在于:主表只读消除锁竞争,冗余字段提升查询效率,任务表支持重试保证一致性,批次化设计适配运营需求,可支撑亿级业务量。
这套方案既解决了全表扫描、锁竞争等性能问题,又通过分层设计保证了业务扩展性,是互联网电商的成熟实践。”
该方案兼顾 “技术合理性” 与 “业务落地性”,覆盖海量场景的核心痛点,符合面试官对 “系统设计能力” 的考察标准。
 posted on 2025-10-21 13:57  xibuhaohao  阅读(11)  评论(0)    收藏  举报