Java秒杀系统案例

wen java案例 2

本文目录导读:

Java秒杀系统案例

  1. 系统架构设计
  2. 核心代码实现
  3. 性能优化策略
  4. 测试用例
  5. 部署架构建议

我来为您设计一个完整的Java秒杀系统案例,涵盖核心业务逻辑、并发控制和数据库设计。

系统架构设计

graph TB
    A[客户端] --> B[负载均衡]
    B --> C[Web层]
    C --> D[业务层]
    D --> E[Redis缓存]
    D --> F[MQ消息队列]
    D --> G[数据库]

核心代码实现

数据库表设计

-- 秒杀商品表
CREATE TABLE seckill_goods (
    id BIGINT PRIMARY KEY AUTO_INCREMENT,
    goods_name VARCHAR(100) NOT NULL,
    price DECIMAL(10,2) NOT NULL,
    stock_count INT NOT NULL,
    seckill_price DECIMAL(10,2) NOT NULL,
    start_time DATETIME NOT NULL,
    end_time DATETIME NOT NULL,
    version INT DEFAULT 0,
    status TINYINT DEFAULT 1,
    create_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP
) ENGINE=InnoDB;
-- 秒杀订单表
CREATE TABLE seckill_order (
    id BIGINT PRIMARY KEY AUTO_INCREMENT,
    user_id BIGINT NOT NULL,
    goods_id BIGINT NOT NULL,
    order_no VARCHAR(32) UNIQUE NOT NULL,
    seckill_price DECIMAL(10,2) NOT NULL,
    status TINYINT DEFAULT 0,
    create_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    KEY idx_user_goods(user_id, goods_id)
) ENGINE=InnoDB;
-- 用户表
CREATE TABLE user (
    id BIGINT PRIMARY KEY AUTO_INCREMENT,
    username VARCHAR(50) UNIQUE NOT NULL,
    password VARCHAR(100) NOT NULL,
    phone VARCHAR(20),
    create_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP
) ENGINE=InnoDB;

项目依赖配置 (pom.xml)

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 
         http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.7.14</version>
    </parent>
    <properties>
        <java.version>1.8</java.version>
    </properties>
    <dependencies>
        <!-- Spring Boot Web -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <!-- MyBatis-Plus -->
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-boot-starter</artifactId>
            <version>3.5.3</version>
        </dependency>
        <!-- Redis -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>
        <!-- RabbitMQ -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-amqp</artifactId>
        </dependency>
        <!-- MySQL -->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
        </dependency>
        <!-- Lombok -->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
        </dependency>
        <!-- Apache Commons -->
        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-pool2</artifactId>
        </dependency>
    </dependencies>
</project>

实体类代码

// SeckillGoods.java
@Data
@TableName("seckill_goods")
public class SeckillGoods {
    @TableId(type = IdType.AUTO)
    private Long id;
    private String goodsName;
    private BigDecimal price;
    private Integer stockCount;
    private BigDecimal seckillPrice;
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    private Date startTime;
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    private Date endTime;
    @Version
    private Integer version;
    private Integer status;
    private Date createTime;
}
// SeckillOrder.java
@Data
@TableName("seckill_order")
public class SeckillOrder {
    @TableId(type = IdType.AUTO)
    private Long id;
    private Long userId;
    private Long goodsId;
    private String orderNo;
    private BigDecimal seckillPrice;
    private Integer status;
    private Date createTime;
}

Mapper接口和XML

// SeckillGoodsMapper.java
@Mapper
public interface SeckillGoodsMapper extends BaseMapper<SeckillGoods> {
    // 乐观锁更新库存
    @Update("UPDATE seckill_goods SET stock_count = stock_count - 1, " +
            "version = version + 1 WHERE id = #{goodsId} " +
            "AND stock_count > 0 AND version = #{version}")
    int deductStockWithVersion(@Param("goodsId") Long goodsId, 
                              @Param("version") Integer version);
}
// SeckillOrderMapper.java
@Mapper
public interface SeckillOrderMapper extends BaseMapper<SeckillOrder> {
    // 递归查询,防止重复秒杀
    @Select("SELECT * FROM seckill_order WHERE user_id = #{userId} " +
            "AND goods_id = #{goodsId} LIMIT 1")
    SeckillOrder findOrderByUserAndGoods(@Param("userId") Long userId, 
                                         @Param("goodsId") Long goodsId);
}

Redis配置类

@Configuration
public class RedisConfig {
    @Bean
    public RedisTemplate<String, Object> redisTemplate(
            RedisConnectionFactory connectionFactory) {
        RedisTemplate<String, Object> template = new RedisTemplate<>();
        template.setConnectionFactory(connectionFactory);
        // String序列化
        StringRedisSerializer stringSerializer = new StringRedisSerializer();
        // Jackson序列化
        Jackson2JsonRedisSerializer<?> jacksonSerializer = 
            new Jackson2JsonRedisSerializer<>(Object.class);
        template.setKeySerializer(stringSerializer);
        template.setHashKeySerializer(stringSerializer);
        template.setValueSerializer(jacksonSerializer);
        template.setHashValueSerializer(jacksonSerializer);
        template.afterPropertiesSet();
        return template;
    }
    // 秒杀库存预缓存
    @Bean
    public RedisTemplate<String, Integer> stockRedisTemplate(
            RedisConnectionFactory connectionFactory) {
        RedisTemplate<String, Integer> template = new RedisTemplate<>();
        template.setConnectionFactory(connectionFactory);
        template.setKeySerializer(new StringRedisSerializer());
        template.setValueSerializer(new GenericJackson2JsonRedisSerializer());
        return template;
    }
}

消息队列配置

@Configuration
public class RabbitMQConfig {
    public static final String SECKILL_QUEUE = "seckillQueue";
    public static final String SECKILL_EXCHANGE = "seckillExchange";
    public static final String SECKILL_ROUTING_KEY = "seckill";
    @Bean
    public Queue seckillQueue() {
        return new Queue(SECKILL_QUEUE, true);
    }
    @Bean
    public TopicExchange seckillExchange() {
        return new TopicExchange(SECKILL_EXCHANGE);
    }
    @Bean
    public Binding binding() {
        return BindingBuilder.bind(seckillQueue())
                .to(seckillExchange())
                .with(SECKILL_ROUTING_KEY);
    }
    // 消息转换器
    @Bean
    public Jackson2JsonMessageConverter messageConverter() {
        return new Jackson2JsonMessageConverter();
    }
}

秒杀服务核心实现

@Service
@Slf4j
public class SeckillService {
    @Autowired
    private SeckillGoodsMapper seckillGoodsMapper;
    @Autowired
    private SeckillOrderMapper seckillOrderMapper;
    @Autowired
    private RedisTemplate<String, Object> redisTemplate;
    @Autowired
    private StringRedisTemplate stringRedisTemplate;
    @Autowired
    private RabbitTemplate rabbitTemplate;
    // 使用ConcurrentHashMap做本地缓存(可选)
    private final ConcurrentHashMap<Long, Boolean> localizedSuccessMap = 
        new ConcurrentHashMap<>();
    /**
     * 秒杀入口(优化版本)
     */
    public SeckillResult seckill(Long userId, Long goodsId) {
        // 1. 参数校验
        if (userId == null || goodsId == null) {
            return SeckillResult.error("参数错误");
        }
        // 2. 检查秒杀时间段
        SeckillGoods goods = seckillGoodsMapper.selectById(goodsId);
        if (goods == null || goods.getStatus() != 1) {
            return SeckillResult.error("商品不存在或不在秒杀活动中");
        }
        Date now = new Date();
        if (now.before(goods.getStartTime()) || now.after(goods.getEndTime())) {
            return SeckillResult.error("不在秒杀时间段内");
        }
        // 3. 检查是否已秒杀过(Redis布隆过滤器或Set)
        String seckillKey = "seckill:user:" + userId + ":goods:" + goodsId;
        Boolean alreadySeckilled = redisTemplate.hasKey(seckillKey);
        if (Boolean.TRUE.equals(alreadySeckilled)) {
            return SeckillResult.error("您已经秒杀过该商品");
        }
        // 4. Redis预减库存(使用Lua脚本保证原子性)
        String stockKey = "seckill:goods:stock:" + goodsId;
        Long stock = stringRedisTemplate.opsForValue().decrement(stockKey);
        if (stock == null || stock < 0) {
            // 库存不足,恢复库存
            if (stock != null && stock < 0) {
                stringRedisTemplate.opsForValue().increment(stockKey);
            }
            return SeckillResult.error("商品已被抢完");
        }
        // 5. 写入成功标识,标记已抢到
        redisTemplate.opsForValue().set(seckillKey, "1", 5, TimeUnit.MINUTES);
        // 6. 异步发送MQ消息,创建订单
        SeckillMessage message = new SeckillMessage();
        message.setUserId(userId);
        message.setGoodsId(goodsId);
        message.setGoodsInfo(goods);
        try {
            rabbitTemplate.convertAndSend(
                RabbitMQConfig.SECKILL_EXCHANGE,
                RabbitMQConfig.SECKILL_ROUTING_KEY,
                message
            );
        } catch (Exception e) {
            log.error("MQ发送失败,回滚库存", e);
            // 回滚Redis库存
            stringRedisTemplate.opsForValue().increment(stockKey);
            redisTemplate.delete(seckillKey);
            return SeckillResult.error("系统繁忙,请重试");
        }
        // 7. 返回结果
        SeckillResult result = SeckillResult.success();
        result.setData(true);
        return result;
    }
    /**
     * 传统查询秒杀流程(备份方案)
     */
    @Transactional
    public SeckillResult traditionalSeckill(Long userId, Long goodsId) {
        // 校验是否已秒杀
        SeckillOrder existingOrder = seckillOrderMapper
            .findOrderByUserAndGoods(userId, goodsId);
        if (existingOrder != null) {
            return SeckillResult.error("您已经抢购过该商品");
        }
        // 数据库乐观锁扣减库存
        SeckillGoods goods = seckillGoodsMapper.selectById(goodsId);
        int result = seckillGoodsMapper.deductStockWithVersion(
            goodsId, goods.getVersion());
        if (result == 0) {
            return SeckillResult.error("库存不足");
        }
        // 创建订单
        SeckillOrder order = new SeckillOrder();
        order.setUserId(userId);
        order.setGoodsId(goodsId);
        order.setOrderNo(generateOrderNo());
        order.setSeckillPrice(goods.getSeckillPrice());
        order.setStatus(0);
        seckillOrderMapper.insert(order);
        return SeckillResult.success(order);
    }
    /**
     * 生成唯一订单号
     */
    private String generateOrderNo() {
        SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMddHHmmss");
        return sdf.format(new Date()) + 
               String.format("%04d", ThreadLocalRandom.current().nextInt(10000)) +
               String.format("%04d", (int)(Math.random() * 10000));
    }
}

MQ消息消费者

@Component
@Slf4j
public class SeckillMessageConsumer {
    @Autowired
    private SeckillOrderMapper orderMapper;
    @Autowired
    private SeckillGoodsMapper goodsMapper;
    @Autowired
    private RedisTemplate<String, Object> redisTemplate;
    @RabbitListener(queues = RabbitMQConfig.SECKILL_QUEUE)
    public void handlerSeckillMessage(SeckillMessage message) {
        log.info("收到秒杀消息: {}", JSON.toJSONString(message));
        // 幂等性处理(通过数据库唯一索引或Redis标记)
        String key = "seckill:order:" + message.getUserId() + ":" + message.getGoodsId();
        Boolean alreadyCreated = redisTemplate.hasKey(key);
        if (Boolean.TRUE.equals(alreadyCreated)) {
            log.warn("订单已创建,跳过处理");
            return;
        }
        try {
            SeckillResult result = createOrder(message);
            if (result.isSuccess()) {
                // 设置标记,5分钟内重复消息直接跳过
                redisTemplate.opsForValue().set(
                    key, "1", 5, TimeUnit.MINUTES);
                log.info("订单创建成功: userId={}, goodsId={}", 
                    message.getUserId(), message.getGoodsId());
            } else {
                log.warn("订单创建失败: {}", result.getMessage());
            }
        } catch (Exception e) {
            log.error("订单创建异常", e);
            // 可以选择重试或记录失败日志
        }
    }
    @Transactional
    public SeckillResult createOrder(SeckillMessage message) {
        SeckillGoods goods = goodsMapper.selectById(message.getGoodsId());
        if (goods == null) {
            return SeckillResult.error("商品不存在");
        }
        // 再次检查是否存在订单
        SeckillOrder existing = orderMapper.findOrderByUserAndGoods(
            message.getUserId(), message.getGoodsId());
        if (existing != null) {
            return SeckillResult.error("订单已存在");
        }
        // 创建订单(数据库层保证唯一性)
        SeckillOrder order = new SeckillOrder();
        order.setUserId(message.getUserId());
        order.setGoodsId(message.getGoodsId());
        order.setOrderNo(generateOrderNo());
        order.setSeckillPrice(goods.getSeckillPrice());
        order.setStatus(0);
        try {
            orderMapper.insert(order);
            return SeckillResult.success(order);
        } catch (DuplicateKeyException e) {
            log.error("订单重复冲突", e);
            return SeckillResult.error("订单已存在");
        }
    }
}

Redis预加载库存

@Component
public class StockInitializer implements ApplicationRunner {
    @Autowired
    private StringRedisTemplate stringRedisTemplate;
    @Autowired
    private SeckillGoodsMapper goodsMapper;
    @Override
    public void run(ApplicationArguments args) throws Exception {
        // 项目启动时加载所有进行中的秒杀商品库存到Redis
        List<SeckillGoods> goodsList = goodsMapper.selectList(
            new QueryWrapper<SeckillGoods>()
                .eq("status", 1)
                .gt("end_time", new Date())
        );
        for (SeckillGoods goods : goodsList) {
            String key = "seckill:goods:stock:" + goods.getId();
            stringRedisTemplate.opsForValue().set(
                key, String.valueOf(goods.getStockCount()));
            log.info("预加载商品库存: goodsId={}, stock={}", 
                goods.getId(), goods.getStockCount());
        }
    }
}

定时任务恢复库存

@Component
@Slf4j
public class StockRecoveryTask {
    @Autowired
    private SeckillGoodsMapper goodsMapper;
    @Scheduled(cron = "0 0/5 * * * ?") // 每5分钟执行一次
    public void recoverAbnormalOrders() {
        log.info("开始恢复异常订单的库存");
        // 查询超时未支付的订单并恢复库存
        // 可以通过关联查询或定时扫描实现
        List<SeckillOrder> expiredOrders = getExpiredOrders();
        for (SeckillOrder order : expiredOrders) {
            // 恢复库存
            goodsMapper.increaseStock(order.getGoodsId());
            // 更新订单状态为已取消
            updateOrderStatus(order, -1);
            log.info("恢复订单库存: orderId={}, goodsId={}", 
                order.getId(), order.getGoodsId());
        }
    }
    private List<SeckillOrder> getExpiredOrders() {
        // 查询超时未支付订单(例如5分钟未支付)
        return orderMapper.selectList(
            new QueryWrapper<SeckillOrder>()
                .eq("status", 0)
                .lt("create_time", new Date(System.currentTimeMillis() - 5*60*1000))
        );
    }
}

控制器层

@RestController
@RequestMapping("/api/seckill")
public class SeckillController {
    @Autowired
    private SeckillService seckillService;
    @Autowired
    private StringRedisTemplate stringRedisTemplate;
    /**
     * 获取秒杀商品列表
     */
    @GetMapping("/list")
    public ApiResponse<List<SeckillGoods>> getSeckillGoods() {
        // 从Redis或数据库查询
        return ApiResponse.success(seckillGoodsService.getActiveGoods());
    }
    /**
     * 获取商品详情
     */
    @GetMapping("/detail/{goodsId}")
    public ApiResponse<SeckillGoods> getDetail(@PathVariable Long goodsId) {
        return ApiResponse.success(seckillGoodsService.getById(goodsId));
    }
    /**
     * 秒杀接口
     * 使用限流(RateLimiter)控制并发
     */
    @PostMapping("/{goodsId}")
    @RateLimiter(limits = 100, time = 1, unit = TimeUnit.SECONDS)
    public ApiResponse<SeckillResult> seckill(
            @PathVariable Long goodsId,
            @RequestHeader("userId") Long userId) {
        SeckillResult result = seckillService.seckill(userId, goodsId);
        return ApiResponse.success(result);
    }
    /**
     * 查询秒杀结果(轮询接口)
     */
    @GetMapping("/result/{goodsId}")
    public ApiResponse<SeckillResult> querySeckillResult(
            @PathVariable Long goodsId,
            @RequestHeader("userId") Long userId) {
        return ApiResponse.success(seckillService.queryResult(userId, goodsId));
    }
}

限流配置

@Aspect
@Component
public class RateLimiterAspect {
    private static final ConcurrentHashMap<Method, com.google.common.util.concurrent.RateLimiter> 
        limiters = new ConcurrentHashMap<>();
    @Around("@annotation(rateLimiter)")
    public Object around(ProceedingJoinPoint joinPoint, 
                         RateLimiter rateLimiter) throws Throwable {
        Method method = ((MethodSignature) joinPoint.getSignature()).getMethod();
        com.google.common.util.concurrent.RateLimiter limiter = 
            limiters.computeIfAbsent(method, m -> 
                com.google.common.util.concurrent.RateLimiter.create(rateLimiter.limits()));
        if (!limiter.tryAcquire()) {
            throw new BusinessException("系统繁忙,请稍后再试");
        }
        return joinPoint.proceed();
    }
}

统一返回结果

@Data
public class ApiResponse<T> {
    private int code;
    private String message;
    private T data;
    public static <T> ApiResponse<T> success(T data) {
        ApiResponse<T> response = new ApiResponse<>();
        response.setCode(200);
        response.setMessage("success");
        response.setData(data);
        return response;
    }
    public static <T> ApiResponse<T> error(int code, String message) {
        ApiResponse<T> response = new ApiResponse<>();
        response.setCode(code);
        response.setMessage(message);
        return response;
    }
}

性能优化策略

多级缓存架构

层级 类型 失效时间
L1 本地缓存 商品基础信息 5分钟
L2 Redis 库存数据 持久
L3 数据库 订单数据 持久

并发控制措施

// 滑动窗口限流
public class SlidingWindowRateLimiter {
    private final int maxRequests;
    private final Deque<Long> timestamps = new ArrayDeque<>();
    public synchronized boolean tryAcquire() {
        long now = System.currentTimeMillis();
        // 清理过期时间戳
        while (!timestamps.isEmpty() && 
               now - timestamps.peekFirst() > 60000) {
            timestamps.pollFirst();
        }
        if (timestamps.size() < maxRequests) {
            timestamps.addLast(now);
            return true;
        }
        return false;
    }
}

测试用例

@SpringBootTest
@RunWith(SpringRunner.class)
public class SeckillServiceTest {
    @Autowired
    private SeckillService seckillService;
    @Test
    public void testConcurrentSeckill() throws InterruptedException {
        int threadCount = 1000;
        CountDownLatch latch = new CountDownLatch(threadCount);
        AtomicInteger successCount = new AtomicInteger();
        AtomicInteger failCount = new AtomicInteger();
        ExecutorService executor = Executors.newFixedThreadPool(50);
        for (int i = 0; i < threadCount; i++) {
            final int userId = i;
            executor.submit(() -> {
                try {
                    SeckillResult result = seckillService.seckill(
                        (long) userId, 1L);
                    if (result.isSuccess()) {
                        successCount.incrementAndGet();
                    } else {
                        failCount.incrementAndGet();
                    }
                } catch (Exception e) {
                    failCount.incrementAndGet();
                } finally {
                    latch.countDown();
                }
            });
        }
        latch.await(30, TimeUnit.SECONDS);
        executor.shutdown();
        log.info("成功人数: {}", successCount.get());
        log.info("失败人数: {}", failCount.get());
        assertEquals(10, successCount.get()); // 假设库存为10
    }
}

部署架构建议

  1. 应用层:独立部署秒杀服务,使用Nginx+多个实例
  2. 数据层:MySQL主从复制,Redis集群
  3. 中间件:RabbitMQ集群部署
  4. 监控:使用Prometheus+Grafana监控系统

这个案例涵盖了秒杀系统的核心要点:库存预减、异步削峰、防重复提交、限流降级等关键技术,实际生产中还需要增加更完善的监控、告警和容灾机制。

抱歉,评论功能暂时关闭!