Java秒杀系统设计与实现

Java秒杀系统设计与实现

📋 问题背景

秒杀系统是电商平台的核心功能,也是Java面试中的超高频考点。在实际工作中,秒杀场景具有以下特点:

  • 高并发:瞬间大量用户涌入
  • 库存有限:商品数量远少于用户数量
  • 时间集中:在特定时间点爆发
  • 数据一致性:防止超卖问题

🎯 核心技术挑战

1. 高并发冲击

  • 数据库连接池耗尽
  • 服务器资源不足
  • 响应时间过长

2. 超卖问题

  • 库存扣减不准确
  • 并发修改数据
  • 事务隔离级别

3. 系统稳定性

  • 服务雪崩
  • 缓存击穿
  • 数据库压力

🛠️ 技术解决方案

架构设计图

复制代码
用户请求 → 负载均衡 → 网关限流 → 秒杀服务 → Redis缓存 → 消息队列 → 数据库

核心技术栈

  • Spring Boot - 微服务框架
  • Redis - 缓存和分布式锁
  • RabbitMQ - 异步处理
  • MySQL - 数据持久化
  • Redisson - 分布式锁实现

💻 完整代码实现

1. 秒杀商品实体类

java 复制代码
@Entity
@Table(name = "seckill_goods")
@Data
public class SeckillGoods {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    
    private String goodsName;
    private BigDecimal price;
    private Integer stock;
    private LocalDateTime startTime;
    private LocalDateTime endTime;
    private Integer version; // 乐观锁版本号
    
    @CreationTimestamp
    private LocalDateTime createTime;
    
    @UpdateTimestamp
    private LocalDateTime updateTime;
}

2. 秒杀订单实体类

java 复制代码
@Entity
@Table(name = "seckill_order")
@Data
public class SeckillOrder {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    
    private Long userId;
    private Long goodsId;
    private String orderNo;
    private BigDecimal price;
    private Integer status; // 0-待支付 1-已支付 2-已取消
    
    @CreationTimestamp
    private LocalDateTime createTime;
}

3. Redis配置类

java 复制代码
@Configuration
@EnableCaching
public class RedisConfig {
    
    @Bean
    public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {
        RedisTemplate<String, Object> template = new RedisTemplate<>();
        template.setConnectionFactory(factory);
        
        // 设置序列化器
        template.setKeySerializer(new StringRedisSerializer());
        template.setValueSerializer(new GenericJackson2JsonRedisSerializer());
        template.setHashKeySerializer(new StringRedisSerializer());
        template.setHashValueSerializer(new GenericJackson2JsonRedisSerializer());
        
        return template;
    }
    
    @Bean
    public RedissonClient redissonClient() {
        Config config = new Config();
        config.useSingleServer()
              .setAddress("redis://localhost:6379")
              .setConnectionPoolSize(50)
              .setConnectionMinimumIdleSize(10);
        return Redisson.create(config);
    }
}

4. 秒杀服务核心实现

java 复制代码
@Service
@Slf4j
public class SeckillService {
    
    @Autowired
    private SeckillGoodsRepository goodsRepository;
    
    @Autowired
    private SeckillOrderRepository orderRepository;
    
    @Autowired
    private RedisTemplate<String, Object> redisTemplate;
    
    @Autowired
    private RedissonClient redissonClient;
    
    @Autowired
    private RabbitTemplate rabbitTemplate;
    
    private static final String STOCK_KEY = "seckill:stock:";
    private static final String USER_ORDER_KEY = "seckill:user:";
    private static final String LOCK_KEY = "seckill:lock:";
    
    /**
     * 预热库存到Redis
     */
    public void preloadStock(Long goodsId) {
        SeckillGoods goods = goodsRepository.findById(goodsId).orElse(null);
        if (goods != null) {
            redisTemplate.opsForValue().set(STOCK_KEY + goodsId, goods.getStock());
            log.info("预热商品库存到Redis: goodsId={}, stock={}", goodsId, goods.getStock());
        }
    }
    
    /**
     * 秒杀主流程 - 方案一:Redis + 分布式锁
     */
    @Transactional(rollbackFor = Exception.class)
    public Result<String> seckillWithRedisLock(Long goodsId, Long userId) {
        // 1. 参数校验
        if (goodsId == null || userId == null) {
            return Result.fail("参数错误");
        }
        
        // 2. 检查用户是否已经购买过
        String userOrderKey = USER_ORDER_KEY + goodsId + ":" + userId;
        if (redisTemplate.hasKey(userOrderKey)) {
            return Result.fail("您已经购买过该商品");
        }
        
        // 3. 获取分布式锁
        RLock lock = redissonClient.getLock(LOCK_KEY + goodsId);
        try {
            // 尝试获取锁,最多等待1秒,锁定10秒后自动释放
            if (lock.tryLock(1, 10, TimeUnit.SECONDS)) {
                // 4. 检查Redis库存
                String stockKey = STOCK_KEY + goodsId;
                Integer stock = (Integer) redisTemplate.opsForValue().get(stockKey);
                
                if (stock == null || stock <= 0) {
                    return Result.fail("商品已售罄");
                }
                
                // 5. 扣减Redis库存
                Long remainStock = redisTemplate.opsForValue().decrement(stockKey);
                if (remainStock < 0) {
                    // 库存不足,回滚
                    redisTemplate.opsForValue().increment(stockKey);
                    return Result.fail("商品已售罄");
                }
                
                // 6. 创建订单(异步处理)
                SeckillOrderDTO orderDTO = new SeckillOrderDTO();
                orderDTO.setGoodsId(goodsId);
                orderDTO.setUserId(userId);
                orderDTO.setOrderNo(generateOrderNo());
                
                // 发送到消息队列异步处理
                rabbitTemplate.convertAndSend("seckill.order.exchange", 
                                            "seckill.order.create", orderDTO);
                
                // 7. 标记用户已购买
                redisTemplate.opsForValue().set(userOrderKey, "1", 30, TimeUnit.MINUTES);
                
                log.info("秒杀成功: userId={}, goodsId={}, remainStock={}", 
                        userId, goodsId, remainStock);
                
                return Result.success("秒杀成功,正在生成订单...");
                
            } else {
                return Result.fail("系统繁忙,请稍后重试");
            }
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
            return Result.fail("系统异常");
        } finally {
            if (lock.isHeldByCurrentThread()) {
                lock.unlock();
            }
        }
    }
    
    /**
     * 秒杀主流程 - 方案二:Lua脚本原子操作
     */
    public Result<String> seckillWithLua(Long goodsId, Long userId) {
        // Lua脚本保证原子性
        String luaScript = 
            "local stockKey = KEYS[1] " +
            "local userKey = KEYS[2] " +
            "local userId = ARGV[1] " +
            
            "-- 检查用户是否已购买 " +
            "if redis.call('exists', userKey) == 1 then " +
            "    return -1 " +
            "end " +
            
            "-- 检查库存 " +
            "local stock = redis.call('get', stockKey) " +
            "if not stock or tonumber(stock) <= 0 then " +
            "    return 0 " +
            "end " +
            
            "-- 扣减库存并标记用户 " +
            "redis.call('decr', stockKey) " +
            "redis.call('setex', userKey, 1800, '1') " +
            "return 1";
        
        DefaultRedisScript<Long> script = new DefaultRedisScript<>();
        script.setScriptText(luaScript);
        script.setResultType(Long.class);
        
        List<String> keys = Arrays.asList(
            STOCK_KEY + goodsId,
            USER_ORDER_KEY + goodsId + ":" + userId
        );
        
        Long result = redisTemplate.execute(script, keys, userId.toString());
        
        if (result == -1) {
            return Result.fail("您已经购买过该商品");
        } else if (result == 0) {
            return Result.fail("商品已售罄");
        } else {
            // 异步创建订单
            SeckillOrderDTO orderDTO = new SeckillOrderDTO();
            orderDTO.setGoodsId(goodsId);
            orderDTO.setUserId(userId);
            orderDTO.setOrderNo(generateOrderNo());
            
            rabbitTemplate.convertAndSend("seckill.order.exchange", 
                                        "seckill.order.create", orderDTO);
            
            return Result.success("秒杀成功,正在生成订单...");
        }
    }
    
    /**
     * 异步创建订单
     */
    @RabbitListener(queues = "seckill.order.queue")
    @Transactional(rollbackFor = Exception.class)
    public void createOrderAsync(SeckillOrderDTO orderDTO) {
        try {
            // 1. 查询商品信息
            SeckillGoods goods = goodsRepository.findById(orderDTO.getGoodsId()).orElse(null);
            if (goods == null) {
                log.error("商品不存在: goodsId={}", orderDTO.getGoodsId());
                return;
            }
            
            // 2. 使用乐观锁扣减数据库库存
            int updated = goodsRepository.decreaseStock(orderDTO.getGoodsId(), goods.getVersion());
            if (updated == 0) {
                log.warn("数据库库存扣减失败: goodsId={}", orderDTO.getGoodsId());
                // 回滚Redis库存
                redisTemplate.opsForValue().increment(STOCK_KEY + orderDTO.getGoodsId());
                return;
            }
            
            // 3. 创建订单
            SeckillOrder order = new SeckillOrder();
            order.setUserId(orderDTO.getUserId());
            order.setGoodsId(orderDTO.getGoodsId());
            order.setOrderNo(orderDTO.getOrderNo());
            order.setPrice(goods.getPrice());
            order.setStatus(0); // 待支付
            
            orderRepository.save(order);
            
            log.info("订单创建成功: orderNo={}", orderDTO.getOrderNo());
            
        } catch (Exception e) {
            log.error("创建订单失败: {}", e.getMessage(), e);
            // 回滚Redis库存
            redisTemplate.opsForValue().increment(STOCK_KEY + orderDTO.getGoodsId());
        }
    }
    
    /**
     * 生成订单号
     */
    private String generateOrderNo() {
        return "SK" + System.currentTimeMillis() + 
               String.format("%04d", new Random().nextInt(10000));
    }
}

5. 秒杀控制器

java 复制代码
@RestController
@RequestMapping("/api/seckill")
@Slf4j
public class SeckillController {
    
    @Autowired
    private SeckillService seckillService;
    
    /**
     * 秒杀接口 - 限流版本
     */
    @PostMapping("/buy/{goodsId}")
    @RateLimiter(name = "seckill", fallbackMethod = "seckillFallback")
    public Result<String> seckill(@PathVariable Long goodsId, 
                                 @RequestHeader("userId") Long userId) {
        try {
            return seckillService.seckillWithLua(goodsId, userId);
        } catch (Exception e) {
            log.error("秒杀异常: goodsId={}, userId={}, error={}", 
                     goodsId, userId, e.getMessage(), e);
            return Result.fail("系统繁忙,请稍后重试");
        }
    }
    
    /**
     * 限流降级方法
     */
    public Result<String> seckillFallback(Long goodsId, Long userId, Exception e) {
        return Result.fail("当前访问人数过多,请稍后重试");
    }
    
    /**
     * 预热库存
     */
    @PostMapping("/preload/{goodsId}")
    public Result<String> preloadStock(@PathVariable Long goodsId) {
        seckillService.preloadStock(goodsId);
        return Result.success("库存预热成功");
    }
}

6. 数据库层优化

java 复制代码
@Repository
public interface SeckillGoodsRepository extends JpaRepository<SeckillGoods, Long> {
    
    /**
     * 乐观锁扣减库存
     */
    @Modifying
    @Query("UPDATE SeckillGoods g SET g.stock = g.stock - 1, g.version = g.version + 1 " +
           "WHERE g.id = :goodsId AND g.stock > 0 AND g.version = :version")
    int decreaseStock(@Param("goodsId") Long goodsId, @Param("version") Integer version);
    
    /**
     * 查询可秒杀商品
     */
    @Query("SELECT g FROM SeckillGoods g WHERE g.startTime <= :now AND g.endTime >= :now AND g.stock > 0")
    List<SeckillGoods> findAvailableGoods(@Param("now") LocalDateTime now);
}

🚀 性能优化策略

1. 多级缓存架构

java 复制代码
@Component
public class MultiLevelCache {
    
    private final LoadingCache<String, Object> localCache;
    private final RedisTemplate<String, Object> redisTemplate;
    
    public MultiLevelCache() {
        this.localCache = Caffeine.newBuilder()
            .maximumSize(1000)
            .expireAfterWrite(5, TimeUnit.MINUTES)
            .build(key -> redisTemplate.opsForValue().get(key));
    }
    
    public Object get(String key) {
        try {
            return localCache.get(key);
        } catch (Exception e) {
            return redisTemplate.opsForValue().get(key);
        }
    }
}

2. 限流策略

java 复制代码
@Component
public class RateLimitAspect {
    
    private final RedisTemplate<String, String> redisTemplate;
    
    @Around("@annotation(rateLimiter)")
    public Object around(ProceedingJoinPoint point, RateLimiter rateLimiter) throws Throwable {
        String key = "rate_limit:" + getClientId();
        String luaScript = 
            "local key = KEYS[1] " +
            "local limit = tonumber(ARGV[1]) " +
            "local window = tonumber(ARGV[2]) " +
            "local current = redis.call('incr', key) " +
            "if current == 1 then " +
            "    redis.call('expire', key, window) " +
            "end " +
            "return current";
        
        DefaultRedisScript<Long> script = new DefaultRedisScript<>();
        script.setScriptText(luaScript);
        script.setResultType(Long.class);
        
        Long count = redisTemplate.execute(script, 
            Collections.singletonList(key), 
            String.valueOf(rateLimiter.limit()), 
            String.valueOf(rateLimiter.window()));
        
        if (count > rateLimiter.limit()) {
            throw new RateLimitException("请求过于频繁");
        }
        
        return point.proceed();
    }
}

📊 监控与告警

1. 关键指标监控

java 复制代码
@Component
public class SeckillMetrics {
    
    private final MeterRegistry meterRegistry;
    private final Counter successCounter;
    private final Counter failCounter;
    private final Timer responseTimer;
    
    public SeckillMetrics(MeterRegistry meterRegistry) {
        this.meterRegistry = meterRegistry;
        this.successCounter = Counter.builder("seckill.success").register(meterRegistry);
        this.failCounter = Counter.builder("seckill.fail").register(meterRegistry);
        this.responseTimer = Timer.builder("seckill.response.time").register(meterRegistry);
    }
    
    public void recordSuccess() {
        successCounter.increment();
    }
    
    public void recordFail() {
        failCounter.increment();
    }
    
    public Timer.Sample startTimer() {
        return Timer.start(meterRegistry);
    }
}

🎯 面试要点总结

常见面试问题及答案

Q: 如何防止超卖问题?

A:

  1. 乐观锁:使用version字段,更新时检查版本号
  2. 分布式锁:Redis分布式锁保证同一时间只有一个线程操作
  3. Lua脚本:保证Redis操作的原子性
  4. 数据库约束:设置库存字段非负约束

Q: 如何应对高并发?

A:

  1. 读写分离:读操作走缓存,写操作走数据库
  2. 异步处理:使用消息队列异步创建订单
  3. 限流降级:接口限流,超出阈值直接返回失败
  4. CDN加速:静态资源使用CDN分发

Q: 缓存和数据库数据一致性如何保证?

A:

  1. 最终一致性:通过消息队列异步同步数据
  2. 双写模式:先写数据库,再写缓存
  3. 失效模式:先删缓存,再写数据库
  4. 分布式事务:使用Seata等分布式事务框架

💡 实际工作应用

1. 电商秒杀场景

  • 双11、618大促活动
  • 限时抢购商品
  • 新品首发预售

2. 票务系统

  • 演唱会门票抢购
  • 火车票抢票系统
  • 景区门票预订

3. 红包系统

  • 微信红包抢夺
  • 优惠券发放
  • 积分兑换活动

🔧 扩展优化方案

1. 预扣库存方案

java 复制代码
public class PreDeductStock {
    
    /**
     * 预扣库存,减少数据库压力
     */
    public boolean preDeductStock(Long goodsId, Integer quantity) {
        String key = "pre_deduct:" + goodsId;
        String luaScript = 
            "local stock = redis.call('get', KEYS[1]) " +
            "if not stock or tonumber(stock) < tonumber(ARGV[1]) then " +
            "    return 0 " +
            "end " +
            "redis.call('decrby', KEYS[1], ARGV[1]) " +
            "return 1";
        
        // 执行Lua脚本
        Long result = redisTemplate.execute(script, 
            Collections.singletonList(key), quantity.toString());
        
        return result == 1;
    }
}

2. 分段锁优化

java 复制代码
public class SegmentLock {
    
    private static final int SEGMENT_COUNT = 16;
    
    public String getSegmentKey(Long goodsId) {
        int segment = (int) (goodsId % SEGMENT_COUNT);
        return "seckill:lock:" + goodsId + ":segment:" + segment;
    }
}

📚 学习建议

1. 基础知识掌握

  • Redis数据结构和命令
  • 分布式锁原理
  • 消息队列使用
  • 数据库事务隔离级别

2. 实践项目

  • 搭建完整的秒杀系统
  • 进行压力测试
  • 监控系统性能指标
  • 优化系统瓶颈

3. 面试准备

  • 熟练掌握核心代码实现
  • 理解各种解决方案的优缺点
  • 准备系统设计图和架构说明
  • 练习口述技术方案

总结:秒杀系统是一个综合性很强的技术场景,涉及高并发、分布式、缓存、消息队列等多个技术领域。掌握好秒杀系统的设计和实现,对于Java开发者的技术成长和面试准备都有很大帮助。

关键是要理解每个技术方案解决的具体问题,以及在不同场景下如何选择合适的技术方案。通过实际项目练习,加深对高并发系统设计的理解。

相关推荐
情怀姑娘1 小时前
面试题---------------场景+算法
java·算法·mybatis
客梦2 小时前
Java 学生管理系统
java·笔记
e***0962 小时前
SpringBoot下获取resources目录下文件的常用方法
java·spring boot·后端
q***14642 小时前
JavaWeb项目打包、部署至Tomcat并启动的全程指南(图文详解)
java·tomcat
從南走到北2 小时前
JAVA同城信息付费系统家政服务房屋租赁房屋买卖房屋装修信息发布平台小程序APP公众号源码
java·开发语言·小程序
TechMasterPlus2 小时前
java:单例模式
java·开发语言·单例模式
JIngJaneIL2 小时前
远程在线诊疗|在线诊疗|基于java和小程序的在线诊疗系统小程序设计与实现(源码+数据库+文档)
java·数据库·vue.js·spring boot·小程序·毕设·在线诊疗小程序
will_we2 小时前
Spring Boot4正式篇:第二篇 多版本API特性
java·后端
风筝在晴天搁浅2 小时前
代码随想录 70.爬楼梯
java