SpringBoot+Redis实现电商秒杀方案
一、系统目标
在整个秒杀活动中的核心稳定性目标主要有以下几点:
- 系统不崩溃:保证即使在峰值流量下系统依然可用。
- 数据一致性:保证库存准确性,不出现超卖现象。
- 高性能:保证绝大多数请求在100毫秒内响应。
二、系统架构设计
前端页面 → 网关层 → 业务层 → Redis → 消息队列 → 数据库(库存校验) (异步下单)
2.1 分层削峰
-
前端层面:
- 目标: 拦截80%无效请求
- 措施:
- 按钮防重复点击
- 验证码校验
- 活动未开始前端限制
-
网关层面:
- 目标: 过滤90%恶意请求
- 措施:
- IP限流
- 用户限流
- 恶意请求识别
-
服务层面:
- 目标: 平稳处理核心业务
- 措施:
- 令牌桶限流
- 请求队列化
- 异步处理
2.2 防超卖核心策略
- Redis原子操作:保证库存扣减的原子性
- 分布式锁:防止同一用户重复抢购
- 库存预热:活动开始前将库存加载到Redis
- 异步下单:快速响应,后端异步处理订单
三、代码实现
3.1 实体类定义
// 秒杀活动实体
@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管理
@Component
public class RedisKeyManager {// 秒杀库存KEYpublic static String getStockKey(Long activityId) {return "seckill:stock:" + activityId;}// 用户秒杀记录KEY(防重复抢购)public static String getUserSeckillKey(Long activityId, Long userId) {return "seckill:user:" + activityId + ":" + userId;}// 秒杀活动信息KEYpublic static String getActivityKey(Long activityId) {return "seckill:activity:" + activityId;}// 分布式锁KEYpublic static String getSeckillLockKey(Long activityId) {return "seckill:lock:" + activityId;}
}
3.3 库存预热服务
@Service
@Slf4j
public class SeckillPreheatService {@Autowiredprivate RedisTemplate<String, Object> redisTemplate;@Autowiredprivate 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 核心秒杀服务(防超卖关键)
@Service
@Slf4j
public class SeckillService {@Autowiredprivate RedisTemplate<String, Object> redisTemplate;@Autowiredprivate RedissonClient redissonClient;@Autowiredprivate 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 消息队列消费者(异步下单)
@Component
@Slf4j
public class SeckillOrderConsumer {@Autowiredprivate SeckillOrderService orderService;@Autowiredprivate 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 订单服务
@Service
@Transactional
@Slf4j
public class SeckillOrderService {@Autowiredprivate SeckillOrderMapper orderMapper;@Autowiredprivate SeckillActivityMapper activityMapper;@Autowiredprivate 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 控制器层
@RestController
@RequestMapping("/seckill")
@Slf4j
public class SeckillController {@Autowiredprivate SeckillService seckillService;@Autowiredprivate 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原子操作
// 关键代码:原子扣减库存 Long remainStock = redisTemplate.opsForValue().decrement(stockKey);
-
Lua脚本原子性
- 将库存检查、扣减、用户记录等多个操作封装在一个Lua脚本中
- Redis单线程执行,保证原子性
-
分布式锁
// 防止极端情况下的并发问题 RLock lock = redissonClient.getLock(lockKey); lock.tryLock(100, 30000, TimeUnit.MILLISECONDS);
-
用户防重
// 记录用户参与记录 String userSeckillKey = RedisKeyManager.getUserSeckillKey(activityId, userId); redisTemplate.opsForValue().set(userSeckillKey, "1", Duration.ofMinutes(30));
-
异常恢复机制
// 下单失败时恢复库存 redisTemplate.opsForValue().increment(stockKey); redisTemplate.delete(userKey);