### 你需要的是电商系统中企业级优惠券领取的完整解决方案,包括核心流程链路、业务场景问题及应对策略

6 阅读16分钟

电商系统中企业级优惠券领取的完整解决方案,包括核心流程链路、业务场景问题及应对策略,并获取基于Spring Cloud架构的可落地代码实现。

一、企业级优惠券领取核心流程与链路设计

企业级优惠券领取需满足 高并发、防超发、防重复、数据一致、可扩展 五大核心要求,整体架构基于Spring Cloud Alibaba生态,核心流程链路如下:

graph TD
    A[用户发起领取请求] --> B[Gateway网关]
    B --> C[Sentinel限流校验]
    C --> D[路由到优惠券服务]
    D --> E[调用风控服务校验]
    E -->|风险用户| F[返回领取失败]
    E -->|正常用户| G[调用用户服务校验]
    G -->|用户异常/不满足条件| F
    G -->|用户正常| H[查询Redis缓存优惠券信息]
    H -->|优惠券无效/库存为0| F
    H -->|优惠券有效| I[获取Redis分布式锁]
    I -->|获取失败| J[返回请稍后重试]
    I -->|获取成功| K[Redis预扣减库存]
    K --> L[Seata开启分布式事务]
    L --> M[调用库存服务扣减DB库存]
    M --> N[创建用户优惠券领取记录]
    N --> O[释放分布式锁]
    O --> P[异步发送领取成功消息到RabbitMQ]
    P --> Q[通知服务消费并推送消息]
    Q --> R[返回领取成功结果]

核心参与组件

  1. 网关层:Spring Cloud Gateway + Sentinel(限流、路由、熔断)

  2. 注册配置中心:Nacos(服务注册、配置管理)

  3. 业务服务:优惠券服务(核心)、用户服务、库存服务、风控服务、通知服务

  4. 中间件

    1. Redis:缓存优惠券信息、分布式锁、防重复领取、库存预扣减
    2. RabbitMQ:异步通知、削峰填谷
    3. Seata:分布式事务(保证库存扣减与领取记录一致性)
    4. MySQL:持久化优惠券信息、领取记录、用户数据
    5. Sentinel:限流、熔断、降级

二、核心业务场景问题及解决方案

1. 优惠券超发问题(最核心风险)

问题原因

高并发下,库存判断与扣减非原子操作,导致实际扣减超过初始库存。

解决方案
  • 双重库存校验:Redis预扣减(快速响应)+ DB最终扣减(数据一致)
  • 分布式锁:Redis Redlock保证同一优惠券并发扣减的原子性
  • 库存预热:优惠券创建时同步库存到Redis,避免DB查询压力
  • 定时对账:定时对比Redis与DB库存,修复不一致数据
  • Seata事务:保证DB库存扣减与领取记录创建的原子性

2. 重复领取问题

问题原因

用户快速多次点击、网络延迟导致重试、恶意刷券。

解决方案
  • DB唯一索引:用户ID + 优惠券ID联合唯一索引,从持久化层防重
  • Redis缓存校验:用户领取后,将<user_id:coupon_id>存入Redis集合,领取前先查询
  • 接口幂等性:基于请求ID(前端生成),Redis存储已处理的请求ID,重复请求直接返回结果

3. 高并发性能问题

问题原因

大量请求直接打DB,导致DB连接耗尽、响应缓慢。

解决方案
  • 缓存优先:优惠券信息(状态、库存、领取规则)全量缓存到Redis,热点优惠券单独分片
  • 异步化处理:领取记录同步DB,通知、日志等非核心流程异步通过RabbitMQ处理
  • 限流削峰:Sentinel对领取接口限流,根据服务能力设置QPS阈值
  • Redis集群:主从+哨兵模式,保证缓存高可用;热点数据分片存储

4. 分布式事务一致性问题

问题原因

领取流程涉及优惠券服务(创建记录)、库存服务(扣减库存),跨服务操作需保证原子性。

解决方案
  • Seata AT模式:基于XA事务协议,自动生成undo_log,保证跨服务事务一致性
  • 可靠消息最终一致性:库存扣减成功后,发送消息到RabbitMQ,创建领取记录失败则重试消费

5. 风控安全问题

问题原因

恶意用户通过脚本刷券,占用大量优惠券资源,影响正常用户。

解决方案
  • 多层校验:网关层(IP/设备限流)+ 业务层(领取频率、黑名单)
  • 领取规则限制:单用户单日领取次数、同一优惠券最多领取次数、会员等级限制
  • 黑名单机制:Redis存储黑名单用户ID,风控服务实时更新,领取前校验

6. 服务可用性问题

问题原因

依赖服务(如用户服务、风控服务)宕机或响应缓慢。

解决方案
  • Feign熔断降级:Sentinel配置熔断规则,依赖服务异常时触发降级(如简化用户校验逻辑)
  • 服务降级策略:核心流程(领取优惠券)优先保障,非核心流程(如会员等级校验)降级为默认通过
  • 服务冗余:核心服务多实例部署,Nacos服务健康检查,自动剔除异常实例

三、Spring Cloud完整代码实现

前置环境准备

  1. 环境要求:JDK 11+、Maven 3.6+、Nacos 2.3+、Redis 6.2+、RabbitMQ 3.9+、Seata 1.6+、MySQL 8.0+
  2. 核心依赖(所有服务通用pom.xml片段):
<dependencies>
    <!-- Spring Boot核心 -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <!-- Spring Cloud Alibaba Nacos -->
    <dependency>
        <groupId>com.alibaba.cloud</groupId>
        <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
    </dependency>
    <dependency>
        <groupId>com.alibaba.cloud</groupId>
        <artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
    </dependency>
    <!-- Spring Cloud OpenFeign -->
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-openfeign</artifactId>
    </dependency>
    <!-- Sentinel -->
    <dependency>
        <groupId>com.alibaba.cloud</groupId>
        <artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
    </dependency>
    <!-- Redis -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-data-redis</artifactId>
    </dependency>
    <!-- MyBatis-Plus -->
    <dependency>
        <groupId>com.baomidou</groupId>
        <artifactId>mybatis-plus-boot-starter</artifactId>
        <version>3.5.3.1</version>
    </dependency>
    <!-- Seata -->
    <dependency>
        <groupId>io.seata</groupId>
        <artifactId>seata-spring-boot-starter</artifactId>
        <version>1.6.1</version>
    </dependency>
    <!-- RabbitMQ -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-amqp</artifactId>
    </dependency>
    <!-- 数据库驱动 -->
    <dependency>
        <groupId>com.mysql</groupId>
        <artifactId>mysql-connector-j</artifactId>
        <scope>runtime</scope>
    </dependency>
</dependencies>

<dependencyManagement>
    <dependencies>
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-alibaba-dependencies</artifactId>
            <version>2021.0.5.0</version>
            <type>pom</type>
            <scope>import</scope>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-dependencies</artifactId>
            <version>2021.0.5</version>
            <type>pom</type>
            <scope>import</scope>
        </dependency>
    </dependencies>
</dependencyManagement>

1. 公共模块(common)核心代码

1.1 枚举类
// 优惠券状态枚举
public enum CouponStatusEnum {
    NOT_STARTED(0, "未开始"),
    AVAILABLE(1, "可领取"),
    EXPIRED(2, "已过期"),
    OFFLINE(3, "已下架");

    private final Integer code;
    private final String desc;

    // 构造器、getter省略
}

// 领取结果枚举
public enum ReceiveResultEnum {
    SUCCESS(200, "领取成功"),
    FAIL_COUPON_INVALID(400, "优惠券无效"),
    FAIL_STOCK_OUT(401, "优惠券已领完"),
    FAIL_REPEAT_RECEIVE(402, "已领取过该优惠券"),
    FAIL_RISK_CONTROL(403, "存在刷券风险,领取失败"),
    FAIL_USER_INVALID(404, "用户状态异常或不满足领取条件"),
    FAIL_SYSTEM_ERROR(500, "系统繁忙,请稍后重试");

    private final Integer code;
    private final String msg;

    // 构造器、getter省略
}
1.2 DTO类
// 领取请求DTO
@Data
public class CouponReceiveRequest {
    // 幂等性请求ID(前端生成)
    @NotNull(message = "请求ID不能为空")
    private String requestId;
    // 用户ID
    @NotNull(message = "用户ID不能为空")
    private Long userId;
    // 优惠券ID
    @NotNull(message = "优惠券ID不能为空")
    private Long couponId;
    // 设备ID(风控用)
    private String deviceId;
    // IP地址(风控用)
    private String ip;
}

// 领取响应DTO
@Data
public class CouponReceiveResponse {
    private Integer code;
    private String msg;
    private CouponReceiveVO data;

    // 成功响应静态方法
    public static CouponReceiveResponse success(CouponReceiveVO data) {
        CouponReceiveResponse response = new CouponReceiveResponse();
        response.setCode(ReceiveResultEnum.SUCCESS.getCode());
        response.setMsg(ReceiveResultEnum.SUCCESS.getMsg());
        response.setData(data);
        return response;
    }

    // 失败响应静态方法
    public static CouponReceiveResponse fail(ReceiveResultEnum resultEnum) {
        CouponReceiveResponse response = new CouponReceiveResponse();
        response.setCode(resultEnum.getCode());
        response.setMsg(resultEnum.getMsg());
        return response;
    }
}

// 领取结果VO
@Data
public class CouponReceiveVO {
    private Long userCouponId; // 用户优惠券ID
    private Long couponId;     // 优惠券ID
    private String couponName; // 优惠券名称
    private Date validStartTime; // 生效时间
    private Date validEndTime;   // 过期时间
    private BigDecimal amount;  // 优惠金额
}
1.3 分布式锁工具类(Redis)
@Component
public class RedisLockUtil {
    @Autowired
    private StringRedisTemplate redisTemplate;

    // 锁前缀
    private static final String LOCK_PREFIX = "coupon:lock:";
    // 锁默认过期时间(30秒)
    private static final long DEFAULT_EXPIRE = 30;
    // 锁等待时间(5秒)
    private static final long DEFAULT_WAIT = 5;
    // 自旋间隔(100毫秒)
    private static final long SPIN_INTERVAL = 100;

    /**
     * 获取分布式锁
     * @param key 锁key(优惠券ID)
     * @return 是否获取成功
     */
    public boolean tryLock(Long key) {
        return tryLock(key, DEFAULT_WAIT, DEFAULT_EXPIRE);
    }

    /**
     * 获取分布式锁(自定义等待时间和过期时间)
     * @param key 锁key
     * @param waitTime 等待时间(秒)
     * @param expireTime 过期时间(秒)
     * @return 是否获取成功
     */
    public boolean tryLock(Long key, long waitTime, long expireTime) {
        String lockKey = LOCK_PREFIX + key;
        String lockValue = UUID.randomUUID().toString();
        long startTime = System.currentTimeMillis();

        try {
            // 循环等待获取锁
            while (System.currentTimeMillis() - startTime < waitTime * 1000) {
                // Redis SET NX EX 原子操作:不存在则设置,过期时间expireTime
                Boolean success = redisTemplate.opsForValue()
                        .setIfAbsent(lockKey, lockValue, expireTime, TimeUnit.SECONDS);
                if (Boolean.TRUE.equals(success)) {
                    // 存储锁值到ThreadLocal,释放时验证
                    ThreadLocalUtil.set(lockKey, lockValue);
                    return true;
                }
                // 自旋等待
                Thread.sleep(SPIN_INTERVAL);
            }
            return false;
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
            return false;
        }
    }

    /**
     * 释放分布式锁(防止误释放)
     * @param key 锁key
     */
    public void unlock(Long key) {
        String lockKey = LOCK_PREFIX + key;
        String lockValue = ThreadLocalUtil.get(lockKey);
        if (StringUtils.hasText(lockValue)) {
            // 对比锁值,一致才删除(Lua脚本保证原子性)
            String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
            redisTemplate.execute(new DefaultRedisScript<>(script, Integer.class),
                    Collections.singletonList(lockKey), lockValue);
            ThreadLocalUtil.remove(lockKey);
        }
    }
}

// ThreadLocal工具类
class ThreadLocalUtil {
    private static final ThreadLocal<Map<String, String>> THREAD_LOCAL = ThreadLocal.withInitial(HashMap::new);

    public static void set(String key, String value) {
        THREAD_LOCAL.get().put(key, value);
    }

    public static String get(String key) {
        return THREAD_LOCAL.get().get(key);
    }

    public static void remove(String key) {
        THREAD_LOCAL.get().remove(key);
    }
}
1.4 幂等性工具类
@Component
public class IdempotentUtil {
    @Autowired
    private StringRedisTemplate redisTemplate;

    // 幂等性key前缀
    private static final String IDEMPOTENT_PREFIX = "coupon:idempotent:";
    // 过期时间(24小时)
    private static final long EXPIRE_TIME = 86400;

    /**
     * 校验幂等性:判断请求是否已处理
     * @param requestId 请求ID
     * @return true-已处理,false-未处理
     */
    public boolean checkIdempotent(String requestId) {
        String key = IDEMPOTENT_PREFIX + requestId;
        Boolean exists = redisTemplate.hasKey(key);
        return Boolean.TRUE.equals(exists);
    }

    /**
     * 标记请求已处理
     * @param requestId 请求ID
     */
    public void markProcessed(String requestId) {
        String key = IDEMPOTENT_PREFIX + requestId;
        redisTemplate.opsForValue().set(key, "1", EXPIRE_TIME, TimeUnit.SECONDS);
    }
}

2. 优惠券服务(coupon-service)核心代码

2.1 配置文件(application.yml)
spring:
  application:
    name: coupon-service
  cloud:
    nacos:
      discovery:
        server-addr: 127.0.0.1:8848
      config:
        server-addr: 127.0.0.1:8848
        file-extension: yaml
    sentinel:
      transport:
        dashboard: 127.0.0.1:8080
  redis:
    host: 127.0.0.1
    port: 6379
    password: 123456
    database: 0
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://127.0.0.1:3306/coupon_db?useSSL=false&serverTimezone=Asia/Shanghai
    username: root
    password: 123456
  rabbitmq:
    host: 127.0.0.1
    port: 5672
    username: guest
    password: guest
    virtual-host: /
    listener:
      simple:
        acknowledge-mode: manual # 手动ACK

# Seata配置
seata:
  enabled: true
  application-id: ${spring.application.name}
  tx-service-group: coupon-service-group
  registry:
    type: nacos
    nacos:
      server-addr: 127.0.0.1:8848
      group: SEATA_GROUP
      username: nacos
      password: nacos
  config:
    type: nacos
    nacos:
      server-addr: 127.0.0.1:8848
      group: SEATA_GROUP
      username: nacos
      password: nacos

# MyBatis-Plus配置
mybatis-plus:
  mapper-locations: classpath:mapper/*.xml
  type-aliases-package: com.example.coupon.entity
  configuration:
    map-underscore-to-camel-case: true

# 日志配置
logging:
  level:
    com.example.coupon.mapper: debug
2.2 Feign客户端(调用其他服务)
// 风控服务Feign客户端
@FeignClient(name = "risk-control-service", fallback = RiskControlFallback.class)
public interface RiskControlFeignClient {
    /**
     * 校验用户领取风险
     * @param userId 用户ID
     * @param deviceId 设备ID
     * @param ip IP地址
     * @return true-无风险,false-有风险
     */
    @GetMapping("/risk/check")
    boolean checkUserRisk(@RequestParam("userId") Long userId,
                          @RequestParam("deviceId") String deviceId,
                          @RequestParam("ip") String ip);
}

// 风控服务降级实现
@Component
public class RiskControlFallback implements RiskControlFeignClient {
    @Override
    public boolean checkUserRisk(Long userId, String deviceId, String ip) {
        // 服务降级:默认无风险(核心流程优先)
        return true;
    }
}

// 用户服务Feign客户端
@FeignClient(name = "user-service", fallback = UserFallback.class)
public interface UserFeignClient {
    /**
     * 校验用户是否满足领取条件
     * @param userId 用户ID
     * @param couponId 优惠券ID
     * @return true-满足,false-不满足
     */
    @GetMapping("/user/check-receive-condition")
    boolean checkReceiveCondition(@RequestParam("userId") Long userId,
                                  @RequestParam("couponId") Long couponId);
}

// 用户服务降级实现
@Component
public class UserFallback implements UserFeignClient {
    @Override
    public boolean checkReceiveCondition(Long userId, Long couponId) {
        // 服务降级:简化校验,默认满足条件
        return true;
    }
}

// 库存服务Feign客户端
@FeignClient(name = "stock-service")
public interface StockFeignClient {
    /**
     * 扣减优惠券库存
     * @param couponId 优惠券ID
     * @return true-扣减成功,false-扣减失败
     */
    @PostMapping("/stock/deduct")
    boolean deductCouponStock(@RequestParam("couponId") Long couponId);
}
2.3 实体类与Mapper
// 优惠券实体
@Data
@TableName("t_coupon")
public class Coupon {
    @TableId(type = IdType.AUTO)
    private Long id; // 优惠券ID
    private String name; // 优惠券名称
    private BigDecimal amount; // 优惠金额
    private Integer status; // 状态(参考CouponStatusEnum)
    private Integer totalStock; // 总库存
    private Integer remainingStock; // 剩余库存
    private Integer receiveLimit; // 单用户领取限制
    private Integer userLevelLimit; // 会员等级限制(0-无限制)
    private Date validStartTime; // 生效时间
    private Date validEndTime; // 过期时间
    private Date createTime; // 创建时间
}

// 用户优惠券实体
@Data
@TableName("t_user_coupon")
public class UserCoupon {
    @TableId(type = IdType.AUTO)
    private Long id; // 主键ID
    private Long userId; // 用户ID
    private Long couponId; // 优惠券ID
    private Integer status; // 状态(0-未使用,1-已使用,2-已过期)
    private Date receiveTime; // 领取时间
    private Date useTime; // 使用时间
    private Date expireTime; // 过期时间

    // 联合唯一索引(防重复领取)
    @TableUniqueInfo(unique = true)
    private List<UniqueIndex> uniqueIndexes = Arrays.asList(
            new UniqueIndex("uk_user_coupon", "user_id", "coupon_id")
    );
}

// 优惠券Mapper
@Mapper
public interface CouponMapper extends BaseMapper<Coupon> {
    @Select("select * from t_coupon where id = #{id}")
    Coupon selectById(Long id);
}

// 用户优惠券Mapper
@Mapper
public interface UserCouponMapper extends BaseMapper<UserCoupon> {
    @Insert("insert into t_user_coupon (user_id, coupon_id, status, receive_time, expire_time) " +
            "values (#{userId}, #{couponId}, 0, now(), #{expireTime})")
    int insertUserCoupon(UserCoupon userCoupon);

    @Select("select count(1) from t_user_coupon where user_id = #{userId} and coupon_id = #{couponId}")
    int countByUserAndCoupon(@Param("userId") Long userId, @Param("couponId") Long couponId);
}
2.4 核心Service实现(领取逻辑)
@Service
@Slf4j
public class CouponReceiveService {
    @Autowired
    private CouponMapper couponMapper;
    @Autowired
    private UserCouponMapper userCouponMapper;
    @Autowired
    private RedisTemplate<String, Object> redisTemplate;
    @Autowired
    private RedisLockUtil redisLockUtil;
    @Autowired
    private IdempotentUtil idempotentUtil;
    @Autowired
    private RiskControlFeignClient riskControlFeignClient;
    @Autowired
    private UserFeignClient userFeignClient;
    @Autowired
    private StockFeignClient stockFeignClient;
    @Autowired
    private RabbitTemplate rabbitTemplate;

    // Redis缓存key前缀
    private static final String COUPON_INFO_KEY = "coupon:info:";
    private static final String COUPON_STOCK_KEY = "coupon:stock:";
    private static final String USER_RECEIVED_COUPON_KEY = "coupon:received:";

    /**
     * 优惠券领取核心方法
     */
    @GlobalTransactional(rollbackFor = Exception.class) // Seata分布式事务
    public CouponReceiveResponse receiveCoupon(CouponReceiveRequest request) {
        Long userId = request.getUserId();
        Long couponId = request.getCouponId();
        String requestId = request.getRequestId();
        String deviceId = request.getDeviceId();
        String ip = request.getIp();

        try {
            // 1. 幂等性校验:避免重复请求
            if (idempotentUtil.checkIdempotent(requestId)) {
                log.warn("请求已处理,requestId:{}", requestId);
                return CouponReceiveResponse.fail(ReceiveResultEnum.FAIL_REPEAT_RECEIVE);
            }

            // 2. 风控校验:调用风控服务
            boolean isRiskFree = riskControlFeignClient.checkUserRisk(userId, deviceId, ip);
            if (!isRiskFree) {
                log.warn("用户存在刷券风险,userId:{}, couponId:{}", userId, couponId);
                return CouponReceiveResponse.fail(ReceiveResultEnum.FAIL_RISK_CONTROL);
            }

            // 3. 用户条件校验:调用用户服务
            boolean isUserQualified = userFeignClient.checkReceiveCondition(userId, couponId);
            if (!isUserQualified) {
                log.warn("用户不满足领取条件,userId:{}, couponId:{}", userId, couponId);
                return CouponReceiveResponse.fail(ReceiveResultEnum.FAIL_USER_INVALID);
            }

            // 4. 优惠券信息校验(Redis缓存优先)
            Coupon coupon = getCouponFromCache(couponId);
            if (coupon == null) {
                // 缓存未命中,查询DB
                coupon = couponMapper.selectById(couponId);
                if (coupon == null) {
                    log.warn("优惠券不存在,couponId:{}", couponId);
                    return CouponReceiveResponse.fail(ReceiveResultEnum.FAIL_COUPON_INVALID);
                }
                // 缓存到Redis(设置过期时间:优惠券过期时间-当前时间)
                long expireSeconds = (coupon.getValidEndTime().getTime() - System.currentTimeMillis()) / 1000;
                if (expireSeconds > 0) {
                    redisTemplate.opsForValue().set(COUPON_INFO_KEY + couponId, coupon, expireSeconds, TimeUnit.SECONDS);
                    redisTemplate.opsForValue().set(COUPON_STOCK_KEY + couponId, coupon.getRemainingStock(), expireSeconds, TimeUnit.SECONDS);
                } else {
                    log.warn("优惠券已过期,couponId:{}", couponId);
                    return CouponReceiveResponse.fail(ReceiveResultEnum.FAIL_COUPON_INVALID);
                }
            }

            // 校验优惠券状态和时间
            if (!CouponStatusEnum.AVAILABLE.getCode().equals(coupon.getStatus()) ||
                    coupon.getValidStartTime().after(new Date()) ||
                    coupon.getValidEndTime().before(new Date())) {
                log.warn("优惠券无效,couponId:{}, status:{}", couponId, coupon.getStatus());
                return CouponReceiveResponse.fail(ReceiveResultEnum.FAIL_COUPON_INVALID);
            }

            // 5. 重复领取校验(Redis+DB双重校验)
            if (isUserReceivedCoupon(userId, couponId)) {
                log.warn("用户已领取该优惠券,userId:{}, couponId:{}", userId, couponId);
                return CouponReceiveResponse.fail(ReceiveResultEnum.FAIL_REPEAT_RECEIVE);
            }

            // 6. 获取分布式锁:防止并发超发
            boolean lockAcquired = redisLockUtil.tryLock(couponId);
            if (!lockAcquired) {
                log.warn("获取锁失败,couponId:{}, userId:{}", couponId, userId);
                return CouponReceiveResponse.fail(ReceiveResultEnum.FAIL_SYSTEM_ERROR);
            }

            try {
                // 7. 库存校验与预扣减(Redis)
                Long remainingStock = redisTemplate.opsForValue().decrement(COUPON_STOCK_KEY + couponId);
                if (remainingStock == null || remainingStock < 0) {
                    // 库存不足,回滚Redis操作
                    redisTemplate.opsForValue().increment(COUPON_STOCK_KEY + couponId);
                    log.warn("优惠券库存不足,couponId:{}, remainingStock:{}", couponId, remainingStock);
                    return CouponReceiveResponse.fail(ReceiveResultEnum.FAIL_STOCK_OUT);
                }

                // 8. 扣减DB库存(调用库存服务,Seata保证事务)
                boolean stockDeductSuccess = stockFeignClient.deductCouponStock(couponId);
                if (!stockDeductSuccess) {
                    // 库存扣减失败,回滚Redis库存
                    redisTemplate.opsForValue().increment(COUPON_STOCK_KEY + couponId);
                    log.error("DB库存扣减失败,couponId:{}", couponId);
                    throw new RuntimeException("库存扣减失败");
                }

                // 9. 创建用户优惠券领取记录
                UserCoupon userCoupon = new UserCoupon();
                userCoupon.setUserId(userId);
                userCoupon.setCouponId(couponId);
                userCoupon.setExpireTime(coupon.getValidEndTime());
                int insertCount = userCouponMapper.insertUserCoupon(userCoupon);
                if (insertCount <= 0) {
                    log.error("创建领取记录失败,userId:{}, couponId:{}", userId, couponId);
                    throw new RuntimeException("创建领取记录失败");
                }

                // 10. 缓存用户领取记录(Redis)
                redisTemplate.opsForSet().add(USER_RECEIVED_COUPON_KEY + userId, couponId.toString());

                // 11. 标记请求已处理(幂等性)
                idempotentUtil.markProcessed(requestId);

                // 12. 异步发送领取成功消息
                sendCouponReceiveMsg(userCoupon, coupon);

                // 13. 构造响应结果
                CouponReceiveVO vo = new CouponReceiveVO();
                vo.setUserCouponId(userCoupon.getId());
                vo.setCouponId(couponId);
                vo.setCouponName(coupon.getName());
                vo.setAmount(coupon.getAmount());
                vo.setValidStartTime(coupon.getValidStartTime());
                vo.setValidEndTime(coupon.getValidEndTime());
                return CouponReceiveResponse.success(vo);

            } finally {
                // 释放分布式锁(必须在finally中,防止死锁)
                redisLockUtil.unlock(couponId);
            }

        } catch (Exception e) {
            log.error("领取优惠券异常,userId:{}, couponId:{}", userId, couponId, e);
            return CouponReceiveResponse.fail(ReceiveResultEnum.FAIL_SYSTEM_ERROR);
        }
    }

    /**
     * 从Redis缓存获取优惠券信息
     */
    private Coupon getCouponFromCache(Long couponId) {
        return (Coupon) redisTemplate.opsForValue().get(COUPON_INFO_KEY + couponId);
    }

    /**
     * 校验用户是否已领取该优惠券
     */
    private boolean isUserReceivedCoupon(Long userId, Long couponId) {
        // 先查Redis
        Boolean isMember = redisTemplate.opsForSet().isMember(USER_RECEIVED_COUPON_KEY + userId, couponId.toString());
        if (Boolean.TRUE.equals(isMember)) {
            return true;
        }
        // Redis未命中,查DB(兜底)
        int count = userCouponMapper.countByUserAndCoupon(userId, couponId);
        if (count > 0) {
            // 同步到Redis
            redisTemplate.opsForSet().add(USER_RECEIVED_COUPON_KEY + userId, couponId.toString());
            return true;
        }
        return false;
    }

    /**
     * 发送领取成功消息到RabbitMQ
     */
    private void sendCouponReceiveMsg(UserCoupon userCoupon, Coupon coupon) {
        try {
            Map<String, Object> msg = new HashMap<>();
            msg.put("userId", userCoupon.getUserId());
            msg.put("userCouponId", userCoupon.getId());
            msg.put("couponName", coupon.getName());
            msg.put("amount", coupon.getAmount());
            msg.put("expireTime", coupon.getValidEndTime());

            // 发送消息到交换机
            rabbitTemplate.convertAndSend(
                    "coupon.exchange", // 交换机名称
                    "coupon.receive.success", // 路由键
                    msg,
                    correlationData -> {
                        // 设置消息ID,用于确认机制
                        correlationData.setId(UUID.randomUUID().toString());
                        return correlationData;
                    }
            );
            log.info("发送领取成功消息,userId:{}, userCouponId:{}", userCoupon.getUserId(), userCoupon.getId());
        } catch (Exception e) {
            log.error("发送领取成功消息失败", e);
            // 消息发送失败可存入本地表,定时重试
        }
    }
}
2.5 Controller层
@RestController
@RequestMapping("/coupon")
@Slf4j
public class CouponReceiveController {
    @Autowired
    private CouponReceiveService couponReceiveService;

    /**
     * 优惠券领取接口
     * @param request 领取请求
     * @return 领取结果
     */
    @PostMapping("/receive")
    @SentinelResource(value = "couponReceive", blockHandler = "couponReceiveBlockHandler")
    public CouponReceiveResponse receiveCoupon(@Valid @RequestBody CouponReceiveRequest request) {
        log.info("收到优惠券领取请求,request:{}", JSON.toJSONString(request));
        return couponReceiveService.receiveCoupon(request);
    }

    /**
     * Sentinel限流降级处理
     */
    public CouponReceiveResponse couponReceiveBlockHandler(CouponReceiveRequest request, BlockException e) {
        log.warn("领取接口被限流,request:{}", JSON.toJSONString(request), e);
        return CouponReceiveResponse.fail(ReceiveResultEnum.FAIL_SYSTEM_ERROR);
    }
}

3. 库存服务(stock-service)核心代码

3.1 实体类与Mapper
@Data
@TableName("t_coupon_stock")
public class CouponStock {
    @TableId(type = IdType.AUTO)
    private Long id;
    private Long couponId; // 优惠券ID
    private Integer totalStock; // 总库存
    private Integer remainingStock; // 剩余库存
    private Date updateTime; // 更新时间
}

@Mapper
public interface CouponStockMapper extends BaseMapper<CouponStock> {
    @Update("update t_coupon_stock set remaining_stock = remaining_stock - 1, update_time = now() " +
            "where coupon_id = #{couponId} and remaining_stock > 0")
    int deductStock(@Param("couponId") Long couponId);

    @Select("select remaining_stock from t_coupon_stock where coupon_id = #{couponId}")
    Integer getRemainingStock(@Param("couponId") Long couponId);
}
3.2 Service与Controller
@Service
@Slf4j
public class CouponStockService {
    @Autowired
    private CouponStockMapper couponStockMapper;

    /**
     * 扣减优惠券库存(Seata事务参与)
     */
    @Transactional
    public boolean deductStock(Long couponId) {
        log.info("开始扣减优惠券库存,couponId:{}", couponId);
        int updateCount = couponStockMapper.deductStock(couponId);
        if (updateCount > 0) {
            log.info("库存扣减成功,couponId:{}", couponId);
            return true;
        }
        log.error("库存扣减失败,couponId:{}", couponId);
        return false;
    }
}

@RestController
@RequestMapping("/stock")
public class CouponStockController {
    @Autowired
    private CouponStockService couponStockService;

    @PostMapping("/deduct")
    public boolean deductCouponStock(@RequestParam("couponId") Long couponId) {
        return couponStockService.deductStock(couponId);
    }
}

4. 其他服务核心代码(简化)

4.1 风控服务(risk-control-service)
@RestController
@RequestMapping("/risk")
public class RiskControlController {
    @Autowired
    private StringRedisTemplate redisTemplate;

    // 黑名单key
    private static final String BLACKLIST_KEY = "risk:blacklist";
    // 领取频率key(用户ID:yyyyMMdd)
    private static final String RECEIVE_FREQUENCY_KEY = "risk:receive:frequency:";

    @GetMapping("/check")
    public boolean checkUserRisk(@RequestParam("userId") Long userId,
                                 @RequestParam("deviceId") String deviceId,
                                 @RequestParam("ip") String ip) {
        // 1. 黑名单校验
        Boolean isBlacklisted = redisTemplate.opsForSet().isMember(BLACKLIST_KEY, userId.toString());
        if (Boolean.TRUE.equals(isBlacklisted)) {
            return false;
        }

        // 2. 领取频率校验(单日最多领取5张)
        String date = new SimpleDateFormat("yyyyMMdd").format(new Date());
        String frequencyKey = RECEIVE_FREQUENCY_KEY + userId + ":" + date;
        Long count = redisTemplate.opsForValue().increment(frequencyKey, 0);
        if (count != null && count >= 5) {
            return false;
        }

        // 3. IP/设备校验(简化:同一IP单日最多10个用户领取)
        String ipKey = "risk:ip:frequency:" + ip + ":" + date;
        Long ipCount = redisTemplate.opsForValue().increment(ipKey, 0);
        if (ipCount != null && ipCount >= 10) {
            return false;
        }

        // 记录领取频率(24小时过期)
        redisTemplate.opsForValue().increment(frequencyKey, 1);
        redisTemplate.expire(frequencyKey, 86400, TimeUnit.SECONDS);
        redisTemplate.opsForValue().increment(ipKey, 1);
        redisTemplate.expire(ipKey, 86400, TimeUnit.SECONDS);

        return true;
    }
}
4.2 通知服务(notify-service)
@Component
@RabbitListener(queuesToDeclare = @Queue(value = "coupon.receive.success.queue", durable = "true"))
@Slf4j
public class CouponReceiveNotifyListener {
    @Autowired
    private UserFeignClient userFeignClient; // 调用用户服务获取用户手机号/推送ID

    @RabbitHandler
    public void handleCouponReceiveMsg(Map<String, Object> msg, Channel channel, Message message) {
        try {
            log.info("收到优惠券领取成功消息,msg:{}", JSON.toJSONString(msg));
            Long userId = Long.valueOf(msg.get("userId").toString());
            String couponName = msg.get("couponName").toString();
            BigDecimal amount = new BigDecimal(msg.get("amount").toString());

            // 1. 调用用户服务获取用户推送信息(简化)
            String pushId = userFeignClient.getUserPushId(userId);

            // 2. 推送消息给用户(短信/APP推送,此处简化)
            log.info("向用户{}推送领取成功消息:您已成功领取{}({}元),请尽快使用",
                    userId, couponName, amount);

            // 3. 手动ACK确认消息
            channel.basicAck(message.getMessageProperties().getDeliveryTag(), false);
        } catch (Exception e) {
            log.error("处理领取成功消息失败", e);
            // 消息重试(最多3次)
            try {
                channel.basicNack(message.getMessageProperties().getDeliveryTag(), false, true);
            } catch (IOException ex) {
                log.error("消息NACK失败", ex);
            }
        }
    }
}

5. 网关配置(gateway-service)

spring:
  application:
    name: gateway-service
  cloud:
    nacos:
      discovery:
        server-addr: 127.0.0.1:8848
      config:
        server-addr: 127.0.0.1:8848
    gateway:
      routes:
        - id: coupon-service-route
          uri: lb://coupon-service
          predicates:
            - Path=/coupon/**
          filters:
            - StripPrefix=1
            - name: Sentinel
              args:
                sentinel.config: classpath:sentinel-gateway.json
    sentinel:
      transport:
        dashboard: 127.0.0.1:8080
      datasource:
        ds1:
          nacos:
            server-addr: 127.0.0.1:8848
            dataId: gateway-sentinel-rule
            groupId: DEFAULT_GROUP
            rule-type: flow

四、总结

核心要点回顾

  1. 流程链路:网关限流→风控校验→用户校验→优惠券校验→分布式锁→库存扣减→记录创建→异步通知,全链路保障高可用与一致性。

  2. 关键问题解决方案

    1. 超发:Redis预扣减+DB扣减+分布式锁+Seata事务
    2. 重复领取:唯一索引+Redis集合+幂等性请求ID
    3. 高并发:Redis缓存+异步处理+Sentinel限流
    4. 一致性:Seata分布式事务+定时对账
  3. 技术选型核心:基于Spring Cloud Alibaba生态,Nacos做注册配置,Sentinel做限流熔断,Redis做缓存与锁,Seata做分布式事务,RabbitMQ做异步削峰,整体架构满足企业级高可用、高并发需求。

扩展建议

  1. 优惠券预热:系统启动时,将热门优惠券信息批量加载到Redis,避免缓存穿透。
  2. 库存对账:定时任务对比Redis与DB库存,修复不一致数据(如Redis库存为负、DB库存未扣减等)。
  3. 监控告警:通过Prometheus+Grafana监控领取QPS、库存剩余量、服务响应时间,设置告警阈值。
  4. 灰度发布:新功能(如领取规则变更)先灰度到部分用户,避免全量风险。
  5. 容灾备份:Redis、MySQL、Seata等核心组件做好主从备份,确保数据不丢失。