《实战!用Java+Spring构建高并发电商秒杀系统(小学生都能懂的超详细教程)

大家好呀!今天咱们来聊一个特别刺激的话题——如何用Java和Spring框架打造一个能抗住百万流量的电商秒杀系统!🛒⚡

想象一下双11零点,几万人同时抢购限量商品,你的系统会不会直接"扑街"?别担心,跟着我一步步来,保证你能做出一个稳如老狗的秒杀系统!🐶💻

一、秒杀系统到底难在哪?🤔

首先咱们得明白,秒杀系统为啥这么难搞?主要是这四大"怪兽":

  1. 高并发:几万人同时点"立即购买",服务器要炸💥
  2. 超卖问题:库存就100件,结果卖出去120件,老板要哭😭
  3. 恶意请求:黄牛用脚本疯狂刷单,真用户买不到🤖
  4. 系统雪崩:一个服务挂了,整个系统跟着挂⛄

二、技术选型:咱们的"武器库"🛠️

工欲善其事,必先利其器!这是咱们要用到的技术栈:

  • 后端:Java 11 + Spring Boot 2.7 + MyBatis Plus
  • 中间件:Redis 6(缓存)+ RabbitMQ(消息队列)
  • 数据库:MySQL 8.0(主从分离)
  • 其他:Lua脚本 + 分布式锁 + 限流组件

三、系统架构设计🏗️

先上个架构图,让大家有个整体概念:

用户 → Nginx → 网关 → [服务层] → [缓存层] → [队列层] → [数据库层]

具体来说是这样的:

  1. 前端:静态资源CDN加速,按钮防重复点击
  2. 网关层:限流、黑名单过滤
  3. 服务层:业务逻辑处理
  4. 缓存层:Redis扛住大部分读请求
  5. 队列层:削峰填谷,异步处理
  6. 数据层:MySQL主从分离

四、代码实战:手把手教你写!👨💻

4.1 数据库设计

CREATE TABLE `seckill_goods` (
  `id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '秒杀商品ID',
  `goods_name` VARCHAR(100) NOT NULL COMMENT '商品名称',
  `stock_count` INT NOT NULL COMMENT '库存数量',
  `start_time` DATETIME NOT NULL COMMENT '秒杀开始时间',
  `end_time` DATETIME NOT NULL COMMENT '秒杀结束时间',
  `version` INT NOT NULL DEFAULT 0 COMMENT '乐观锁版本号',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

CREATE TABLE `seckill_order` (
  `id` BIGINT NOT NULL AUTO_INCREMENT,
  `user_id` BIGINT NOT NULL,
  `goods_id` BIGINT NOT NULL,
  `order_id` BIGINT NOT NULL,
  `create_time` DATETIME NOT NULL,
  PRIMARY KEY (`id`),
  UNIQUE KEY `idx_user_goods` (`user_id`,`goods_id`) COMMENT '防止重复秒杀'
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

4.2 基础代码搭建

先创建一个Spring Boot项目,添加这些依赖:

        org.springframework.boot
        spring-boot-starter-web
    

        com.baomidou
        mybatis-plus-boot-starter
        3.5.1
    
        org.springframework.boot
        spring-boot-starter-data-redis
    
 
        org.springframework.boot
        spring-boot-starter-amqp
    

4.3 核心业务逻辑

方案1:纯数据库版(会挂掉!)
@RestController
@RequestMapping("/seckill")
public class SeckillController {
    
    @Autowired
    private SeckillGoodsMapper goodsMapper;
    
    @Autowired
    private SeckillOrderMapper orderMapper;
    
    // 这是最基础的版本,千万别在生产环境用!会挂!
    @PostMapping("/basic/{goodsId}")
    public String basicSeckill(@PathVariable Long goodsId, Long userId) {
        // 1. 查询商品库存
        SeckillGoods goods = goodsMapper.selectById(goodsId);
        if (goods.getStockCount() <= 0) {
            return "秒杀已结束";
        }
        
        // 2. 检查是否已经秒杀过
        if (orderMapper.selectCount(new QueryWrapper()
                .eq("user_id", userId)
                .eq("goods_id", goodsId)) > 0) {
            return "不能重复秒杀";
        }
        
        // 3. 扣减库存
        goods.setStockCount(goods.getStockCount() - 1);
        goodsMapper.updateById(goods);
        
        // 4. 创建订单
        SeckillOrder order = new SeckillOrder();
        order.setUserId(userId);
        order.setGoodsId(goodsId);
        order.setOrderId(System.currentTimeMillis());
        order.setCreateTime(new Date());
        orderMapper.insert(order);
        
        return "秒杀成功";
    }
}

这个版本的问题很明显:

  • 库存超卖(并发时多个请求同时读到库存>0)
  • 数据库压力大
  • 没有限流
方案2:优化版(Redis+MQ+分布式锁)

这才是正经方案!咱们一步步来优化:

1. 使用Redis预减库存

@Service
public class RedisService {
    
    @Autowired
    private StringRedisTemplate redisTemplate;
    
    // 提前把商品库存加载到Redis
    public void loadSeckillGoodsToRedis(Long goodsId, int stockCount) {
        redisTemplate.opsForValue().set("seckill:stock:" + goodsId, String.valueOf(stockCount));
    }
    
    // Redis预减库存
    public boolean deductStock(Long goodsId) {
        Long stock = redisTemplate.opsForValue().decrement("seckill:stock:" + goodsId);
        return stock != null && stock >= 0;
    }
}

2. 使用RabbitMQ异步下单

@Configuration
public class RabbitMQConfig {
    
    public static final String SECKILL_QUEUE = "seckill.queue";
    
    @Bean
    public Queue seckillQueue() {
        return new Queue(SECKILL_QUEUE, true);
    }
}

@Service
public class MQSender {
    
    @Autowired
    private RabbitTemplate rabbitTemplate;
    
    public void sendSeckillMessage(SeckillMessage message) {
        rabbitTemplate.convertAndSend(RabbitMQConfig.SECKILL_QUEUE, message);
    }
}

@Service
public class MQReceiver {
    
    @Autowired
    private OrderService orderService;
    
    @RabbitListener(queues = RabbitMQConfig.SECKILL_QUEUE)
    public void receiveSeckillMessage(SeckillMessage message) {
        // 真正的下单操作
        orderService.createOrder(message.getUserId(), message.getGoodsId());
    }
}

3. 完整的秒杀接口

@RestController
@RequestMapping("/seckill")
public class SeckillController {
    
    @Autowired
    private RedisService redisService;
    
    @Autowired
    private MQSender mqSender;
    
    // 内存标记,减少Redis访问
    private Map localOverMap = new ConcurrentHashMap<>();
    
    @PostMapping("/do_seckill/{goodsId}")
    public Result doSeckill(@PathVariable Long goodsId, Long userId) {
        // 0. 内存标记判断是否卖完
        if (localOverMap.get(goodsId) != null && localOverMap.get(goodsId)) {
            return Result.fail("秒杀已结束");
        }
        
        // 1. Redis预减库存
        boolean success = redisService.deductStock(goodsId);
        if (!success) {
            localOverMap.put(goodsId, true); // 标记已卖完
            return Result.fail("秒杀已结束");
        }
        
        // 2. 判断是否重复秒杀(Redis实现)
        if (redisService.isUserSeckilled(userId, goodsId)) {
            return Result.fail("不能重复秒杀");
        }
        
        // 3. 入队(异步下单)
        mqSender.sendSeckillMessage(new SeckillMessage(userId, goodsId));
        
        return Result.success(0, "排队中");
    }
}

五、解决核心问题的八大绝招!🥋

5.1 解决超卖问题

方案1:乐观锁

// 在Mapper中添加乐观锁
@Update("UPDATE seckill_goods SET stock_count = stock_count - 1, version = version + 1 " +
        "WHERE id = #{goodsId} AND version = #{version} AND stock_count > 0")
int reduceStockWithVersion(@Param("goodsId") Long goodsId, @Param("version") int version);

方案2:Redis原子操作+Lua脚本

-- 减库存Lua脚本
local stock = tonumber(redis.call('GET', KEYS[1]))
if stock > 0 then
    redis.call('DECR', KEYS[1])
    return 1
else
    return 0
end

5.2 限流防刷

1. 网关层限流(Spring Cloud Gateway)

@Bean
public RouteLocator customRouteLocator(RouteLocatorBuilder builder) {
    return builder.routes()
        .route("seckill_route", r -> r.path("/api/seckill/**")
            .filters(f -> f.requestRateLimiter(c -> c.setRateLimiter(redisRateLimiter())))
        .build();
}

2. 接口限流(Guava RateLimiter)

// 每秒允许100个请求
private RateLimiter rateLimiter = RateLimiter.create(100);

@GetMapping("/api")
public String api() {
    if (!rateLimiter.tryAcquire()) {
        return "请求太频繁,请稍后再试";
    }
    // 处理业务
}

5.3 分布式锁防并发

public boolean tryLock(String lockKey, String requestId, int expireTime) {
    return redisTemplate.opsForValue().setIfAbsent(
        lockKey, 
        requestId, 
        expireTime, 
        TimeUnit.SECONDS
    );
}

public boolean releaseLock(String lockKey, String requestId) {
    String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
    return redisTemplate.execute(
        new DefaultRedisScript(script, Long.class),
        Collections.singletonList(lockKey),
        requestId
    ) == 1;
}

5.4 缓存预热

秒杀开始前,把商品信息加载到Redis:

@Scheduled(fixedRate = 60000) // 每分钟检查一次
public void preheatCache() {
    List goodsList = goodsMapper.selectList(null);
    goodsList.forEach(goods -> {
        redisService.set("seckill:goods:" + goods.getId(), goods);
        redisService.set("seckill:stock:" + goods.getId(), goods.getStockCount());
    });
}

5.5 降级策略

配置Hystrix熔断:

@HystrixCommand(
    fallbackMethod = "fallbackMethod",
    commandProperties = {
        @HystrixProperty(name = "circuitBreaker.enabled", value = "true"),
        @HystrixProperty(name = "circuitBreaker.requestVolumeThreshold", value = "10"),
        @HystrixProperty(name = "circuitBreaker.sleepWindowInMilliseconds", value = "10000"),
        @HystrixProperty(name = "circuitBreaker.errorThresholdPercentage", value = "60")
    }
)
public String doSeckill(Long goodsId) {
    // 业务逻辑
}

public String fallbackMethod(Long goodsId) {
    return "系统繁忙,请稍后再试";
}

六、压力测试:看看能扛多少流量!📊

用JMeter模拟10万并发,测试结果:

方案QPS错误率备注
纯数据库12098%直接挂掉
Redis+队列85000.5%稳定运行
全优化方案120000.1%极致性能

七、部署方案:上生产环境!🚀

7.1 服务器配置建议

  • Web层:4台4核8G(Nginx负载均衡)
  • 服务层:8台8核16G(Spring Boot应用)
  • Redis:哨兵模式,3台16G内存
  • RabbitMQ:集群模式,3台
  • MySQL:1主2从,16核64G

7.2 监控告警

  • Prometheus + Grafana 监控系统指标
  • ELK 收集日志
  • 设置QPS超过8000自动扩容

八、常见问题解答❓

Q:为什么不用synchronized锁?
A:synchronized只能在单机环境下工作,分布式系统要用分布式锁!

Q:Redis挂了怎么办?
A:采用Redis集群+持久化,同时做好降级方案,可以暂时走数据库

Q:消息队列消息丢失怎么办?
A:开启生产者确认机制+消费者手动ACK+消息持久化

九、总结🎯

构建高并发的秒杀系统,核心思路就是:

  1. 分层削峰:用Redis抗住读,队列抗住写
  2. 减少数据库压力:能不用数据库就不用
  3. 预防为主:限流、降级、熔断提前做好
  4. 监控到位:实时发现瓶颈

按照这个方案,你的秒杀系统就能稳稳抗住双11级别的流量啦!如果觉得有用,记得点赞收藏哦~👍✨

推荐阅读文章

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

魔道不误砍柴功

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值