大家好呀!今天咱们来聊一个特别刺激的话题——如何用Java和Spring框架打造一个能抗住百万流量的电商秒杀系统!🛒⚡
想象一下双11零点,几万人同时抢购限量商品,你的系统会不会直接"扑街"?别担心,跟着我一步步来,保证你能做出一个稳如老狗的秒杀系统!🐶💻
一、秒杀系统到底难在哪?🤔
首先咱们得明白,秒杀系统为啥这么难搞?主要是这四大"怪兽":
- 高并发:几万人同时点"立即购买",服务器要炸💥
- 超卖问题:库存就100件,结果卖出去120件,老板要哭😭
- 恶意请求:黄牛用脚本疯狂刷单,真用户买不到🤖
- 系统雪崩:一个服务挂了,整个系统跟着挂⛄
二、技术选型:咱们的"武器库"🛠️
工欲善其事,必先利其器!这是咱们要用到的技术栈:
- 后端:Java 11 + Spring Boot 2.7 + MyBatis Plus
- 中间件:Redis 6(缓存)+ RabbitMQ(消息队列)
- 数据库:MySQL 8.0(主从分离)
- 其他:Lua脚本 + 分布式锁 + 限流组件
三、系统架构设计🏗️
先上个架构图,让大家有个整体概念:
用户 → Nginx → 网关 → [服务层] → [缓存层] → [队列层] → [数据库层]
具体来说是这样的:
- 前端:静态资源CDN加速,按钮防重复点击
- 网关层:限流、黑名单过滤
- 服务层:业务逻辑处理
- 缓存层:Redis扛住大部分读请求
- 队列层:削峰填谷,异步处理
- 数据层: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 | 错误率 | 备注 |
---|---|---|---|
纯数据库 | 120 | 98% | 直接挂掉 |
Redis+队列 | 8500 | 0.5% | 稳定运行 |
全优化方案 | 12000 | 0.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+消息持久化
九、总结🎯
构建高并发的秒杀系统,核心思路就是:
- 分层削峰:用Redis抗住读,队列抗住写
- 减少数据库压力:能不用数据库就不用
- 预防为主:限流、降级、熔断提前做好
- 监控到位:实时发现瓶颈
按照这个方案,你的秒杀系统就能稳稳抗住双11级别的流量啦!如果觉得有用,记得点赞收藏哦~👍✨