Java实现简单秒杀功能

        在商城项目中,秒杀功能可以说是必不可少的,下面我将使用Spring Boot集成Redis、RabbitMQ、MyBatis-Plus和MySQL来实现一个简单的秒杀系统,系统将包含以下核心功能:

  1. 使用Redis进行库存预减和用户限流;

  2. 使用RabbitMQ进行异步下单,提高系统吞吐量;

  3. 使用MyBatis-Plus操作MySQL数据库;

  4. 利用Redis执行Lua脚本的原子性防止商品超卖;

  5. 接口限流(使用Redis或网关限流,这里使用gateway实现简单限流);

一、系统架构

Java实现简单秒杀功能_第1张图片

二、数据库设计 

1. 秒杀商品表 (seckill_goods)
CREATE TABLE `seckill_goods` (
  `id` BIGINT(20) UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '秒杀商品ID',
  `origin_id` BIGINT(20) NOT NULL COMMENT '原始商品ID',
  `name` VARCHAR(100) NOT NULL COMMENT '商品名称',
  `price` DECIMAL(10,2) NOT NULL DEFAULT '0.00' COMMENT '原价',
  `seckill_price` DECIMAL(10,2) NOT NULL COMMENT '秒杀价',
  `stock` INT(11) NOT NULL COMMENT '库存数量',
  `start_time` DATETIME NOT NULL COMMENT '开始时间',
  `end_time` DATETIME NOT NULL COMMENT '结束时间',
  `is_active` TINYINT(1) NOT NULL DEFAULT '1' COMMENT '是否启用',
  `version` INT(11) NOT NULL DEFAULT '0' COMMENT '乐观锁版本',
  `created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
  `updated_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
  PRIMARY KEY (`id`),
  KEY `idx_time` (`start_time`,`end_time`),
  KEY `idx_origin` (`origin_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='秒杀商品表';
2. 订单表 (seckill_order)
CREATE TABLE `seckill_order` (
  `id` VARCHAR(32) NOT NULL COMMENT '订单ID(时间戳+用户ID哈希)',
  `user_id` BIGINT(20) UNSIGNED NOT NULL COMMENT '用户ID',
  `goods_id` BIGINT(20) UNSIGNED NOT NULL COMMENT '秒杀商品ID',
  `quantity` INT(11) NOT NULL DEFAULT '1' COMMENT '购买数量',
  `price` DECIMAL(10,2) NOT NULL COMMENT '实际支付价格',
  `status` TINYINT(4) NOT NULL DEFAULT '0' COMMENT '0-未支付 1-已支付 2-已取消',
  `create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
  `update_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
  PRIMARY KEY (`id`),
  KEY `idx_user_goods` (`user_id`,`goods_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='秒杀订单表';

三、网关层实现(Spring Cloud Gateway)

1.Gateway配置类
@Configuration
public class GatewayConfig {
    
    private static final Logger log = LoggerFactory.getLogger(GatewayConfig.class);
    
    // 限流过滤器
    @Bean
    @Order(-1)
    public GlobalFilter rateLimitFilter() {
        return (exchange, chain) -> {
            ServerHttpRequest request = exchange.getRequest();
            String ip = Objects.requireNonNull(request.getRemoteAddress()).getAddress().getHostAddress();
            String path = request.getPath().value();
            
            // 秒杀接口特殊限流
            if (path.startsWith("/api/seckill")) {
                if (!rateLimiter.tryAcquire(ip, 1, 5, TimeUnit.SECONDS)) {
                    log.warn("IP限流触发: {}", ip);
                    exchange.getResponse().setStatusCode(HttpStatus.TOO_MANY_REQUESTS);
                    return exchange.getResponse().setComplete();
                }
            }
            return chain.filter(exchange);
        };
    }
    
    // 路由配置
    @Bean
    public RouteLocator customRouteLocator(RouteLocatorBuilder builder) {
        return builder.routes()
            .route("seckill_route", r -> r.path("/api/seckill/**")
                .filters(f -> f.stripPrefix(2)
                    // 添加请求头
                    .addRequestHeader("X-Seckill-Token", "secure_token")
                    .circuitBreaker(config -> config
                        .setName("seckillCircuitBreaker")
                        // 设置回调方法请求地址(失败时)
                        .setFallbackUri("forward:/fallback")))
                // 为秒杀请求配置负载均衡
                .uri("lb://seckill-service"))
            .route("order_route", r -> r.path("/api/order/**")
                .filters(f -> f.stripPrefix(2))
                .uri("lb://order-service"))
            .build();
    }
    
    // 断路器降级处理
    @RequestMapping("/fallback")
    public Mono> fallback() {
        return Mono.just(ResponseEntity.status(HttpStatus.SERVICE_UNAVAILABLE)
            .body("{\"code\":503,\"message\":\"服务不可用,请稍后重试\"}"));
    }
}
2.Redis限流器
@Service
public class RedisRateLimiter {
    
    private final RedisTemplate redisTemplate;
    
    public RedisRateLimiter(RedisTemplate redisTemplate) {
        this.redisTemplate = redisTemplate;
    }
    
    // 参数含义:redis的key、窗口期内可请求的最大次数、周期、时间单位
    public boolean tryAcquire(String key, int permits, int period, TimeUnit timeUnit) {
        String redisKey = "rate_limit:" + key;
        long current = System.currentTimeMillis();
        long windowStart = current - timeUnit.toMillis(period);
        
        return redisTemplate.execute(new SessionCallback() {
            @Override
            @SuppressWarnings("unchecked")
            public Boolean execute(RedisOperations operations) {
                operations.multi();
                // 创建一条记录
                operations.opsForZSet().add(redisKey, current, current);
                // 删除不在窗口期的请求记录
                operations.opsForZSet().removeRangeByScore(redisKey, 0, windowStart);
                // 计算窗口期内请求的次数
                operations.opsForZSet().zCard(redisKey);
                // 设置过期时间,提高内存利用率
                operations.expire(redisKey, period + 1, timeUnit);
                
                List results = operations.exec();
                if (results == null || results.isEmpty()) return false;
                
                Long count = (Long) results.get(2);
                return count != null && count <= permits;
            }
        });
    }
} 
  

四、秒杀服务实现

1.秒杀核心逻辑
@Service
@Slf4j
public class SeckillServiceImpl implements SeckillService {
    
    private final RedisTemplate redisTemplate;
    private final RabbitTemplate rabbitTemplate;
    private final SeckillGoodsMapper seckillGoodsMapper;
    
    private static final String STOCK_KEY = "seckill:stock:%s";
    private static final String USER_LIMIT_KEY = "seckill:user:%s:%s";
    
    // Lua脚本原子操作
    private static final DefaultRedisScript SECKILL_SCRIPT = new DefaultRedisScript<>();
    static {
        SECKILL_SCRIPT.setLocation(new ClassPathResource("lua/seckill.lua"));
        SECKILL_SCRIPT.setResultType(Long.class);
    }
    
    @Override
    @Transactional(rollbackFor = Exception.class)
    public SeckillResponse handleSeckill(Long userId, Long goodsId) {
        // 1. 校验商品有效性
        SeckillGoods goods = seckillGoodsMapper.selectById(goodsId);
        if (goods == null || !goods.getIsActive()) {
            return SeckillResponse.fail("秒杀商品不存在");
        }
        
        long now = System.currentTimeMillis();
        if (now < goods.getStartTime().getTime()) {
            return SeckillResponse.fail("秒杀未开始");
        }
        if (now > goods.getEndTime().getTime()) {
            return SeckillResponse.fail("秒杀已结束");
        }
        
        // 2. Redis原子操作
        String stockKey = String.format(STOCK_KEY, goodsId);
        String userLimitKey = String.format(USER_LIMIT_KEY, userId, goodsId);
        
        List keys = Arrays.asList(stockKey, userLimitKey);
        Long result = redisTemplate.execute(
            SECKILL_SCRIPT,
            keys,
            userId, goodsId, 1 // 每人限购1件
        );
        
        if (result == null) {
            return SeckillResponse.fail("系统异常");
        }
        
        // 3. 处理结果
        if (result == 1) {
            // 发送MQ消息
            SeckillMessage message = new SeckillMessage(userId, goodsId, goods.getSeckillPrice());
            rabbitTemplate.convertAndSend("seckill.exchange", "seckill.route", message);
            return SeckillResponse.success("秒杀成功,正在生成订单");
        } else {
            return SeckillResponse.fail(switch (result.intValue()) {
                case -1 -> "库存不足";
                case 0 -> "超出购买限制";
                case -2 -> "重复请求";
                default -> "秒杀失败";
            });
        }
    }
    
    // 初始化库存到Redis
    @PostConstruct
    public void initStockToRedis() {
        List activeGoods = seckillGoodsMapper.selectList(
            new LambdaQueryWrapper()
                .eq(SeckillGoods::getIsActive, 1)
        );
        
        for (SeckillGoods goods : activeGoods) {
            String stockKey = String.format(STOCK_KEY, goods.getId());
            redisTemplate.opsForValue().set(stockKey, goods.getStock());
            redisTemplate.expireAt(stockKey, goods.getEndTime());
        }
    }
}
2.Lua脚本 (resources/lua/seckill.lua)
-- KEYS[1]: 库存key (seckill:stock:{goodsId})
-- KEYS[2]: 用户限购key (seckill:user:{userId}:{goodsId})

-- ARGV[1]: userId
-- ARGV[2]: goodsId
-- ARGV[3]: 购买限制数量

-- 检查用户是否已经购买过
local userPurchased = redis.call('EXISTS', KEYS[2])
if userPurchased == 1 then
    local purchasedCount = tonumber(redis.call('GET', KEYS[2]))
    if purchasedCount >= tonumber(ARGV[3]) then
        return 0; -- 超出限购数量
    end

    -- 检查是否重复请求(已成功购买但未达限购)
    if purchasedCount > 0 then
        return -2; -- 重复请求
    end
end

-- 检查库存
local stock = tonumber(redis.call('GET', KEYS[1]))
if stock <= 0 then
    return -1; -- 库存不足
end

-- 扣减库存
redis.call('DECR', KEYS[1])

-- 更新用户购买记录
if userPurchased == 0 then
    redis.call('SET', KEYS[2], 1)
    redis.call('EXPIREAT', KEYS[2], 172800) -- 2天有效期
else
    redis.call('INCR', KEYS[2])
end

return 1; -- 成功
3.RabbitMQ配置
@Configuration
public class RabbitMQConfig {
    
    @Bean
    public TopicExchange seckillExchange() {
        return new TopicExchange("seckill.exchange", true, false);
    }
    
    @Bean
    public Queue seckillQueue() {
        return new Queue("seckill.queue", true);
    }
    
    @Bean
    public Binding seckillBinding() {
        return BindingBuilder.bind(seckillQueue())
                .to(seckillExchange())
                .with("seckill.route");
    }
    
    @Bean
    public Jackson2JsonMessageConverter jsonMessageConverter() {
        return new Jackson2JsonMessageConverter();
    }
    
    @Bean
    public SimpleRabbitListenerContainerFactory rabbitListenerContainerFactory(
            ConnectionFactory connectionFactory,
            Jackson2JsonMessageConverter messageConverter) {
        SimpleRabbitListenerContainerFactory factory = new SimpleRabbitListenerContainerFactory();
        factory.setConnectionFactory(connectionFactory);
        factory.setMessageConverter(messageConverter);
        factory.setPrefetchCount(100); // 预取消息数量
        return factory;
    }
    
    // 生产者确认回调
    @Bean
    public RabbitTemplate.ConfirmCallback confirmCallback() {
        return (correlationData, ack, cause) -> {
            if (!ack) {
                log.error("MQ消息发送失败: {}", cause);
                // TODO: 处理失败逻辑
            }
        };
    }
}

五、订单服务实现

1.订单创建消费者
@Service
@Slf4j
public class OrderConsumer {
    
    private final OrderService orderService;
    private final RedisTemplate redisTemplate;
    
    // 监听秒杀队列
    @RabbitListener(queues = "seckill.queue")
    public void handleSeckillMessage(SeckillMessage message) {
        log.info("收到秒杀消息: {}", message);
        
        Long userId = message.getUserId();
        Long goodsId = message.getGoodsId();
        BigDecimal price = message.getPrice();
        
        try {
            // 幂等性检查
            if (orderService.isOrderExists(userId, goodsId)) {
                log.warn("重复订单: userId={}, goodsId={}", userId, goodsId);
                return;
            }
            
            // 创建订单
            String orderId = generateOrderId(userId);
            SeckillOrder order = new SeckillOrder();
            order.setId(orderId);
            order.setUserId(userId);
            order.setGoodsId(goodsId);
            order.setQuantity(1);
            order.setPrice(price);
            
            boolean success = orderService.createOrder(order);
            
            if (success) {
                // 缓存订单
                redisTemplate.opsForValue().set(
                    "order:" + orderId, 
                    order, 
                    30, TimeUnit.MINUTES
                );
            }
        } catch (Exception e) {
            log.error("订单创建失败: {}", e.getMessage());
            // 重试机制
            throw new AmqpRejectAndDontRequeueException("处理失败,入死信队列");
        }
    }
    
    // 生成订单ID:时间戳+用户ID哈希
    private String generateOrderId(Long userId) {
        SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMddHHmmss");
        String timestamp = sdf.format(new Date());
        int hash = Math.abs(userId.hashCode()) % 10000;
        return timestamp + String.format("%04d", hash);
    }
}
2.Mybatis-Plus订单服务实现
@Service
public class OrderServiceImpl extends ServiceImpl 
        implements OrderService {
    
    private static final int MAX_RETRY = 3; // 乐观锁最大重试次数
    
    @Override
    public boolean createOrder(SeckillOrder order) {
        // 使用分布式锁防止重复创建
        RLock lock = redissonClient.getLock("order:lock:" + order.getUserId());
        try {
            lock.lock(3, TimeUnit.SECONDS);
            
            // 再次检查订单是否存在
            if (isOrderExists(order.getUserId(), order.getGoodsId())) {
                return false;
            }
            
            // 插入订单
            return save(order);
        } finally {
            if (lock.isLocked() && lock.isHeldByCurrentThread()) {
                lock.unlock();
            }
        }
    }
    
    @Override
    public boolean isOrderExists(Long userId, Long goodsId) {
        return lambdaQuery()
            .eq(SeckillOrder::getUserId, userId)
            .eq(SeckillOrder::getGoodsId, goodsId)
            .exists();
    }
    
    // 定时更新库存到数据库
    @Scheduled(fixedRate = 60000) // 每分钟同步一次
    public void syncStockToDB() {
        List goodsList = seckillGoodsMapper.selectList(
            new LambdaQueryWrapper()
                .eq(SeckillGoods::getIsActive, 1)
        );
        
        for (SeckillGoods goods : goodsList) {
            String stockKey = String.format("seckill:stock:%s", goods.getId());
            Integer redisStock = (Integer) redisTemplate.opsForValue().get(stockKey);
            if (redisStock != null && !redisStock.equals(goods.getStock())) {
                seckillGoodsMapper.updateStock(goods.getId(), redisStock, goods.getVersion());
            }
        }
    }
}

六、库存服务实现

1.库存更新SQL(乐观锁)


    UPDATE seckill_goods
    SET 
        stock = #{newStock},
        version = version + 1
    WHERE 
        id = #{goodsId}
        AND version = #{version}
2.Mybatis-Plus接口定义
public interface SeckillGoodsMapper extends BaseMapper {
    
    @Update("UPDATE seckill_goods SET stock = #{newStock}, version = version + 1 " +
            "WHERE id = #{goodsId} AND version = #{version}")
    int updateStock(@Param("goodsId") Long goodsId, 
                   @Param("newStock") Integer newStock, 
                   @Param("version") Integer oldVersion);
}

七、部署与生产配置

1.部署架构

Java实现简单秒杀功能_第2张图片

2.生产环境配置建议 

application-prod.yml

spring:
  redis:
    host: redis-cluster
    port: 6379
    password: ${REDIS_PASSWORD}
    lettuce:
      pool:
        max-active: 200
        max-idle: 50
        min-idle: 10
        max-wait: 1000ms
        
  rabbitmq:
    host: rabbitmq-cluster
    port: 5672
    username: ${RABBITMQ_USER}
    password: ${RABBITMQ_PASS}
    virtual-host: /seckill
    publisher-confirm-type: correlated
    publisher-returns: true
    listener:
      simple:
        prefetch: 100
        acknowledge-mode: manual
        
  datasource:
    url: jdbc:mysql://mysql-cluster:3306/seckill_db?useSSL=false&allowPublicKeyRetrieval=true
    username: ${DB_USER}
    password: ${DB_PASS}
    driver-class-name: com.mysql.cj.jdbc.Driver
    druid:
      initial-size: 5
      min-idle: 5
      max-active: 50
      max-wait: 3000
      validation-query: SELECT 1
      test-while-idle: true
      test-on-borrow: false
      test-on-return: false
      
  cloud:
    gateway:
      httpclient:
        connect-timeout: 1000
        response-timeout: 3s
        pool:
          max-connections: 1000
          max-idle-time: 30000
          
seckill:
  redis:
    prefix: "sec:" # Redis键前缀
  mq:
    exchange: seckill.exchange
    queue: seckill.queue
    routing-key: seckill.route
3.核心依赖

    17
    3.2.0
    2023.0.0



    
    
        org.springframework.boot
        spring-boot-starter-web
    
    
        org.springframework.boot
        spring-boot-starter-actuator
    
    
        org.springframework.boot
        spring-boot-starter-validation
    
    
    
    
        org.springframework.cloud
        spring-cloud-starter-gateway
    
    
    
    
        org.springframework.boot
        spring-boot-starter-data-redis
    
    
        org.redisson
        redisson-spring-boot-starter
        3.25.0
    
    
    
    
        org.springframework.boot
        spring-boot-starter-amqp
    
    
    
    
        com.mysql
        mysql-connector-j
        runtime
    
    
        com.baomidou
        mybatis-plus-boot-starter
        3.5.5
    
    
        com.alibaba
        druid-spring-boot-starter
        1.2.20
    
    
    
    
        org.projectlombok
        lombok
        true
    
    
        com.fasterxml.jackson.core
        jackson-databind
    
    
    
    
        org.springframework.boot
        spring-boot-starter-test
        test
    
    
        org.springframework.amqp
        spring-rabbit-test
        test
    



    
        
            org.springframework.cloud
            spring-cloud-dependencies
            ${spring-cloud.version}
            pom
            import
        
    

你可能感兴趣的:(Java实现简单秒杀功能)