一、系统目标
在整个秒杀活动中的核心稳定性目标主要有以下几点:
- 系统不崩溃:保证即使在峰值流量下系统依然可用。
- 数据一致性:保证库存准确性,不出现超卖现象。
- 高性能:保证绝大多数请求在100毫秒内响应。
二、系统架构设计
bash
前端页面 → 网关层 → 业务层 → Redis → 消息队列 → 数据库
(库存校验) (异步下单)
2.1 分层削峰
-
前端层面:
- 目标: 拦截80%无效请求
- 措施:
- 按钮防重复点击
- 验证码校验
- 活动未开始前端限制
-
网关层面:
- 目标: 过滤90%恶意请求
- 措施:
- IP限流
- 用户限流
- 恶意请求识别
-
服务层面:
- 目标: 平稳处理核心业务
- 措施:
- 令牌桶限流
- 请求队列化
- 异步处理
2.2 防超卖核心策略
- Redis原子操作:保证库存扣减的原子性
- 分布式锁:防止同一用户重复抢购
- 库存预热:活动开始前将库存加载到Redis
- 异步下单:快速响应,后端异步处理订单
三、代码实现
3.1 实体类定义
bash
// 秒杀活动实体
@Data
public class SeckillActivity {
private Long id;
private String name;
private Long productId;
private BigDecimal seckillPrice;
private Integer stock;
private Integer initialStock;
private Date startTime;
private Date endTime;
private Integer status; // 0-未开始 1-进行中 2-已结束
}
// 秒杀订单实体
@Data
public class SeckillOrder {
private Long id;
private Long userId;
private Long activityId;
private Long productId;
private String orderNo;
private BigDecimal amount;
private Integer quantity;
private Integer status; // 0-待支付 1-已支付 2-已取消
private Date createTime;
}
3.2 Redis Key管理
bash
@Component
public class RedisKeyManager {
// 秒杀库存KEY
public static String getStockKey(Long activityId) {
return "seckill:stock:" + activityId;
}
// 用户秒杀记录KEY(防重复抢购)
public static String getUserSeckillKey(Long activityId, Long userId) {
return "seckill:user:" + activityId + ":" + userId;
}
// 秒杀活动信息KEY
public static String getActivityKey(Long activityId) {
return "seckill:activity:" + activityId;
}
// 分布式锁KEY
public static String getSeckillLockKey(Long activityId) {
return "seckill:lock:" + activityId;
}
}
3.3 库存预热服务
bash
@Service
@Slf4j
public class SeckillPreheatService {
@Autowired
private RedisTemplate<String, Object> redisTemplate;
@Autowired
private SeckillActivityMapper activityMapper;
/**
* 预热秒杀库存到Redis
*/
public void preheatStock(Long activityId) {
SeckillActivity activity = activityMapper.selectById(activityId);
if (activity == null) {
throw new RuntimeException("秒杀活动不存在");
}
String stockKey = RedisKeyManager.getStockKey(activityId);
String activityKey = RedisKeyManager.getActivityKey(activityId);
// 设置库存
redisTemplate.opsForValue().set(stockKey, activity.getStock());
// 缓存活动信息
redisTemplate.opsForValue().set(activityKey, activity);
log.info("秒杀活动{}库存预热完成,库存量:{}", activityId, activity.getStock());
}
/**
* 获取Redis中的库存
*/
public Integer getStockFromRedis(Long activityId) {
String stockKey = RedisKeyManager.getStockKey(activityId);
Object stockObj = redisTemplate.opsForValue().get(stockKey);
return stockObj != null ? Integer.parseInt(stockObj.toString()) : 0;
}
}
3.4 核心秒杀服务(防超卖关键)
bash
@Service
@Slf4j
public class SeckillService {
@Autowired
private RedisTemplate<String, Object> redisTemplate;
@Autowired
private RedissonClient redissonClient;
@Autowired
private RabbitTemplate rabbitTemplate;
/**
* 方案一:使用Redis原子操作扣减库存(推荐)
* 利用Redis单线程特性,保证原子性
*/
public SeckillResult seckillV1(Long activityId, Long userId) {
// 1. 参数校验
if (activityId == null || userId == null) {
return SeckillResult.error("参数错误");
}
// 2. 校验用户是否重复参与
String userSeckillKey = RedisKeyManager.getUserSeckillKey(activityId, userId);
if (Boolean.TRUE.equals(redisTemplate.hasKey(userSeckillKey))) {
return SeckillResult.error("请勿重复参与秒杀");
}
// 3. 原子扣减库存
String stockKey = RedisKeyManager.getStockKey(activityId);
Long remainStock = redisTemplate.opsForValue().decrement(stockKey);
if (remainStock == null) {
return SeckillResult.error("秒杀活动不存在");
}
if (remainStock < 0) {
// 库存不足,恢复库存
// 这里恢复库存意图是好的,为了确保库存不会变成负数
// 在极高并发下,可能会出现多个线程都扣减了库存,然后都发现库存为负数,
然后都恢复了库存。这样,库存最终恢复到了0,但是实际上这些线程都没有成功下单。
所以,并没有超卖。
// 但是在这个过程中, 每次扣减都要操作Redis两次(decrement和increment),
增加了Redis的负担,并不是个完美的方案。
redisTemplate.opsForValue().increment(stockKey);
return SeckillResult.error("商品已售罄");
}
try {
// 4. 记录用户秒杀成功
redisTemplate.opsForValue().set(userSeckillKey, "1", Duration.ofMinutes(30));
// 5. 发送异步下单消息
sendSeckillOrderMessage(activityId, userId);
return SeckillResult.success("秒杀成功,请尽快支付");
} catch (Exception e) {
// 异常情况恢复库存
redisTemplate.opsForValue().increment(stockKey);
redisTemplate.delete(userSeckillKey);
log.error("秒杀异常", e);
return SeckillResult.error("系统繁忙,请重试");
}
}
/**
* 方案二:使用Lua脚本保证原子性(更安全)
*/
public SeckillResult seckillV2(Long activityId, Long userId) {
String luaScript =
"local stockKey = KEYS[1] " +
"local userKey = KEYS[2] " +
"local activityId = ARGV[1] " +
"local userId = ARGV[2] " +
// 检查用户是否已参与
"if redis.call('exists', userKey) == 1 then " +
" return 2 " +
"end " +
// 检查库存
"local stock = tonumber(redis.call('get', stockKey)) " +
"if not stock or stock <= 0 then " +
" return 3 " +
"end " +
// 扣减库存
"redis.call('decr', stockKey) " +
// 记录用户参与
"redis.call('setex', userKey, 1800, 1) " +
"return 1 ";
List<String> keys = Arrays.asList(
RedisKeyManager.getStockKey(activityId),
RedisKeyManager.getUserSeckillKey(activityId, userId)
);
DefaultRedisScript<Long> script = new DefaultRedisScript<>();
script.setScriptText(luaScript);
script.setResultType(Long.class);
Long result = redisTemplate.execute(script, keys, activityId.toString(), userId.toString());
switch (result.intValue()) {
case 1:
sendSeckillOrderMessage(activityId, userId);
return SeckillResult.success("秒杀成功");
case 2:
return SeckillResult.error("请勿重复参与");
case 3:
return SeckillResult.error("商品已售罄");
default:
return SeckillResult.error("秒杀失败");
}
}
/**
* 方案三:分布式锁 + Redis原子操作(最安全,适合极端高并发)
*/
public SeckillResult seckillV3(Long activityId, Long userId) {
String lockKey = RedisKeyManager.getSeckillLockKey(activityId);
RLock lock = redissonClient.getLock(lockKey);
try {
// 尝试加锁,最多等待100ms,锁有效期30秒
boolean locked = lock.tryLock(100, 30000, TimeUnit.MILLISECONDS);
if (!locked) {
return SeckillResult.error("系统繁忙,请重试");
}
// 执行秒杀逻辑
return seckillV2(activityId, userId);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
return SeckillResult.error("系统异常");
} finally {
if (lock.isHeldByCurrentThread()) {
lock.unlock();
}
}
}
/**
* 发送秒杀订单消息到消息队列
*/
private void sendSeckillOrderMessage(Long activityId, Long userId) {
Map<String, Object> message = new HashMap<>();
message.put("activityId", activityId);
message.put("userId", userId);
message.put("createTime", System.currentTimeMillis());
rabbitTemplate.convertAndSend("seckill.order.exchange",
"seckill.order.route",
message);
log.info("发送秒杀订单消息: activityId={}, userId={}", activityId, userId);
}
}
3.5 消息队列消费者(异步下单)
bash
@Component
@Slf4j
public class SeckillOrderConsumer {
@Autowired
private SeckillOrderService orderService;
@Autowired
private RedisTemplate<String, Object> redisTemplate;
@RabbitListener(queues = "seckill.order.queue")
public void processSeckillOrder(Map<String, Object> message) {
Long activityId = Long.valueOf(message.get("activityId").toString());
Long userId = Long.valueOf(message.get("userId").toString());
try {
// 创建订单
orderService.createSeckillOrder(activityId, userId);
log.info("秒杀订单创建成功: activityId={}, userId={}", activityId, userId);
} catch (Exception e) {
log.error("创建秒杀订单失败", e);
// 创建订单失败,恢复库存
recoverStock(activityId, userId);
}
}
/**
* 恢复库存
*/
private void recoverStock(Long activityId, Long userId) {
String stockKey = RedisKeyManager.getStockKey(activityId);
String userKey = RedisKeyManager.getUserSeckillKey(activityId, userId);
redisTemplate.opsForValue().increment(stockKey);
redisTemplate.delete(userKey);
log.warn("恢复库存: activityId={}, userId={}", activityId, userId);
}
}
3.6 订单服务
bash
@Service
@Transactional
@Slf4j
public class SeckillOrderService {
@Autowired
private SeckillOrderMapper orderMapper;
@Autowired
private SeckillActivityMapper activityMapper;
@Autowired
private RedisTemplate<String, Object> redisTemplate;
/**
* 创建秒杀订单
*/
public void createSeckillOrder(Long activityId, Long userId) {
// 双重校验,防止消息重复消费
String userKey = RedisKeyManager.getUserSeckillKey(activityId, userId);
if (!Boolean.TRUE.equals(redisTemplate.hasKey(userKey))) {
log.warn("用户秒杀记录不存在: activityId={}, userId={}", activityId, userId);
return;
}
// 查询活动信息
SeckillActivity activity = getActivityFromCache(activityId);
if (activity == null) {
throw new RuntimeException("秒杀活动不存在");
}
// 生成订单号
String orderNo = generateOrderNo();
// 创建订单
SeckillOrder order = new SeckillOrder();
order.setUserId(userId);
order.setActivityId(activityId);
order.setProductId(activity.getProductId());
order.setOrderNo(orderNo);
order.setAmount(activity.getSeckillPrice());
order.setQuantity(1);
order.setStatus(0); // 待支付
order.setCreateTime(new Date());
orderMapper.insert(order);
// 更新数据库库存(可选,用于对账)
updateDatabaseStock(activityId);
log.info("创建秒杀订单成功: orderNo={}, userId={}", orderNo, userId);
}
/**
* 从缓存获取活动信息
*/
private SeckillActivity getActivityFromCache(Long activityId) {
String activityKey = RedisKeyManager.getActivityKey(activityId);
Object activityObj = redisTemplate.opsForValue().get(activityKey);
if (activityObj instanceof SeckillActivity) {
return (SeckillActivity) activityObj;
}
return activityMapper.selectById(activityId);
}
/**
* 更新数据库库存
*/
private void updateDatabaseStock(Long activityId) {
int rows = activityMapper.decreaseStock(activityId);
if (rows == 0) {
log.error("更新数据库库存失败: activityId={}", activityId);
throw new RuntimeException("库存不足");
}
}
/**
* 生成订单号
*/
private String generateOrderNo() {
return "SO" + System.currentTimeMillis() + RandomUtil.randomNumbers(6);
}
}
3.7 控制器层
bash
@RestController
@RequestMapping("/seckill")
@Slf4j
public class SeckillController {
@Autowired
private SeckillService seckillService;
@Autowired
private SeckillPreheatService preheatService;
/**
* 秒杀接口
*/
@PostMapping("/{activityId}")
public SeckillResult seckill(@PathVariable Long activityId,
@RequestHeader("userId") Long userId) {
try {
// 使用方案二:Lua脚本保证原子性
return seckillService.seckillV2(activityId, userId);
} catch (Exception e) {
log.error("秒杀异常", e);
return SeckillResult.error("系统繁忙,请重试");
}
}
/**
* 预热库存
*/
@PostMapping("/preheat/{activityId}")
public String preheat(@PathVariable Long activityId) {
preheatService.preheatStock(activityId);
return "预热成功";
}
/**
* 查询库存
*/
@GetMapping("/stock/{activityId}")
public Integer getStock(@PathVariable Long activityId) {
return preheatService.getStockFromRedis(activityId);
}
}
// 返回结果封装
@Data
class SeckillResult {
private boolean success;
private String message;
private String orderNo;
public static SeckillResult success(String message) {
SeckillResult result = new SeckillResult();
result.setSuccess(true);
result.setMessage(message);
return result;
}
public static SeckillResult error(String message) {
SeckillResult result = new SeckillResult();
result.setSuccess(false);
result.setMessage(message);
return result;
}
}
四、方案总结
-
Redis原子操作
bash// 关键代码:原子扣减库存 Long remainStock = redisTemplate.opsForValue().decrement(stockKey);
-
Lua脚本原子性
- 将库存检查、扣减、用户记录等多个操作封装在一个Lua脚本中
- Redis单线程执行,保证原子性
-
分布式锁
bash// 防止极端情况下的并发问题 RLock lock = redissonClient.getLock(lockKey); lock.tryLock(100, 30000, TimeUnit.MILLISECONDS);
-
用户防重
bash// 记录用户参与记录 String userSeckillKey = RedisKeyManager.getUserSeckillKey(activityId, userId); redisTemplate.opsForValue().set(userSeckillKey, "1", Duration.ofMinutes(30));
-
异常恢复机制
bash// 下单失败时恢复库存 redisTemplate.opsForValue().increment(stockKey); redisTemplate.delete(userKey);