电商系统中企业级优惠券领取的完整解决方案,包括核心流程链路、业务场景问题及应对策略,并获取基于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[返回领取成功结果]
核心参与组件
-
网关层:Spring Cloud Gateway + Sentinel(限流、路由、熔断)
-
注册配置中心:Nacos(服务注册、配置管理)
-
业务服务:优惠券服务(核心)、用户服务、库存服务、风控服务、通知服务
-
中间件:
- Redis:缓存优惠券信息、分布式锁、防重复领取、库存预扣减
- RabbitMQ:异步通知、削峰填谷
- Seata:分布式事务(保证库存扣减与领取记录一致性)
- MySQL:持久化优惠券信息、领取记录、用户数据
- 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完整代码实现
前置环境准备
- 环境要求:JDK 11+、Maven 3.6+、Nacos 2.3+、Redis 6.2+、RabbitMQ 3.9+、Seata 1.6+、MySQL 8.0+
- 核心依赖(所有服务通用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
四、总结
核心要点回顾
-
流程链路:网关限流→风控校验→用户校验→优惠券校验→分布式锁→库存扣减→记录创建→异步通知,全链路保障高可用与一致性。
-
关键问题解决方案:
- 超发:Redis预扣减+DB扣减+分布式锁+Seata事务
- 重复领取:唯一索引+Redis集合+幂等性请求ID
- 高并发:Redis缓存+异步处理+Sentinel限流
- 一致性:Seata分布式事务+定时对账
-
技术选型核心:基于Spring Cloud Alibaba生态,Nacos做注册配置,Sentinel做限流熔断,Redis做缓存与锁,Seata做分布式事务,RabbitMQ做异步削峰,整体架构满足企业级高可用、高并发需求。
扩展建议
- 优惠券预热:系统启动时,将热门优惠券信息批量加载到Redis,避免缓存穿透。
- 库存对账:定时任务对比Redis与DB库存,修复不一致数据(如Redis库存为负、DB库存未扣减等)。
- 监控告警:通过Prometheus+Grafana监控领取QPS、库存剩余量、服务响应时间,设置告警阈值。
- 灰度发布:新功能(如领取规则变更)先灰度到部分用户,避免全量风险。
- 容灾备份:Redis、MySQL、Seata等核心组件做好主从备份,确保数据不丢失。