1. 项目概述:黑马点评优惠券秒杀系统核心逻辑
这个项目源自黑马程序员Java课程中的经典电商案例,主要解决高并发场景下的优惠券秒杀问题。作为分布式系统的典型应用场景,优惠券秒杀涉及库存超卖、流量削峰、数据一致性等核心挑战。我在实际开发中发现,仅靠基础CRUD根本无法应对瞬时10万级请求,必须引入Redis缓存、分布式锁、异步队列等中间件技术栈。
黑马点评的秒杀模块设计精妙之处在于:它没有直接操作MySQL库存,而是通过Redis预减库存+异步下单的方式实现。当用户点击"立即抢购"时,系统先在Redis执行lua脚本原子性扣减库存,生成订单号后立刻返回"排队中"状态,实际订单创建通过RabbitMQ异步处理。这种设计将数据库写入压力从瞬时高峰分摊到消息队列的消费速率上,实测QPS从直接访问MySQL的200提升到8000+。
2. 技术架构深度解析
2.1 分层设计原则
系统采用标准的三层架构,但每层都有针对高并发的特殊处理:
- 表现层:Nginx负载均衡 + 限流(rate_limit模块)
- 服务层:
- SpringBoot + MyBatis-Plus基础框架
- Redis缓存热点数据(优惠券库存、用户抢购记录)
- Redisson分布式锁控制关键路径
- 数据层:
- MySQL主从分离(读写分离)
- 订单表按用户ID分片(避免热点问题)
关键技巧:所有缓存Key都设计为
业务前缀:ID格式(如seckill:voucher:1001),既避免冲突又方便Redis可视化工具过滤
2.2 库存扣减的原子性实现
传统方案的问题:
// 伪代码:问题版本 int stock = redis.get("stock"); if(stock > 0) { redis.set("stock", stock - 1); // 非原子操作! }在高并发下会出现多个线程同时判断stock>0导致超卖。黑马方案采用Redis Lua脚本:
-- KEYS[1]库存key ARGV[1]扣减数量 local stock = tonumber(redis.call('GET', KEYS[1])) if stock >= tonumber(ARGV[1]) then return redis.call('INCRBY', KEYS[1], -tonumber(ARGV[1])) else return -1 end实测单个商品10万库存,5000并发下无超卖,执行耗时稳定在2ms内。
3. 核心业务流程实现
3.1 秒杀令牌发放流程
- 资格校验:
- 校验用户是否登录(JWT解析)
- 校验秒杀是否开始/结束(Redis存储活动时间)
- 库存预占:
- 执行上述Lua脚本扣减Redis库存
- 失败则直接返回"已售罄"
- 生成令牌:
- 生成全局唯一订单ID(雪花算法)
- 写入Redis抢购记录(用户ID+优惠券ID为Key)
// 关键Java代码片段 public Result seckillVoucher(Long voucherId) { // 1. Lua脚本原子扣减 Long result = redisTemplate.execute( STOCK_DEDUCTION_SCRIPT, Collections.singletonList(STOCK_KEY + voucherId), "1"); if (result == null || result < 0) { return Result.fail(result == null ? "系统异常" : "库存不足"); } // 2. 创建订单消息 long orderId = redisIdWorker.nextId("order"); mqTemplate.convertAndSend("order.queue", new VoucherOrder(orderId, userId, voucherId)); return Result.ok(orderId); }3.2 异步订单处理
消息队列采用RabbitMQ的Direct模式,关键配置:
spring: rabbitmq: listener: simple: prefetch: 50 # 控制单个消费者并发量 acknowledge-mode: manual # 手动ACK消费者核心逻辑:
@RabbitListener(queues = "order.queue") public void handleOrder(VoucherOrder order, Channel channel, Message message) { try { // 1. 幂等性校验 if (orderMapper.exists(order.getOrderId())) { channel.basicAck(message.getMessageProperties().getDeliveryTag(), false); return; } // 2. 数据库事务 transactionTemplate.execute(status -> { orderMapper.insert(order); voucherMapper.updateStock(order.getVoucherId()); return null; }); channel.basicAck(message.getDeliveryTag(), false); } catch (Exception e) { // 重试3次后转死信队列 channel.basicNack(message.getDeliveryTag(), false, message.getMessageProperties().getRedeliveredCount() < 3); } }4. 性能优化实战技巧
4.1 缓存预热方案
在活动开始前1小时执行:
-- 从MySQL加载数据到Redis INSERT INTO redis_store (key, value) SELECT CONCAT('seckill:voucher:', id), stock FROM tb_voucher WHERE seckill_begin_time <= NOW() + INTERVAL 1 HOUR;4.2 热点Key处理
对于特别热门的优惠券(如iPhone折扣券),采用:
- 本地缓存:Guava Cache存储部分库存
Cache<Long, Integer> localCache = CacheBuilder.newBuilder() .expireAfterWrite(1, TimeUnit.MINUTES) .maximumSize(100) .build(); - Key分片:将库存拆分为
stock_1、stock_2等子库存
4.3 限流策略组合
- Nginx层:令牌桶算法
limit_req_zone $binary_remote_addr zone=seckill:10m rate=50r/s; location /seckill { limit_req zone=seckill burst=100 nodelay; } - 应用层:Sentinel热点参数限流
@SentinelResource(value = "seckill", blockHandler = "blockHandler") public Result seckill(Long voucherId) {...}
5. 典型问题排查实录
5.1 Redis连接池耗尽
现象:大量报错Could not get a resource from the pool
排查:
- 监控Redis连接数:
redis-cli info clients # connected_clients突增到maxclients限制 - 发现Lua脚本中有耗时操作(如KEYS命令)
解决:
- 调整Jedis连接池参数:
spring: redis: jedis: pool: max-active: 500 # 默认8根本不够 max-wait: 1000ms - 优化Lua脚本,移除所有O(n)操作
5.2 消息堆积导致延迟
现象:下单后10分钟才收到成功通知
分析:
rabbitmqctl list_queues name messages_ready # order.queue积压50万条优化方案:
- 增加消费者实例(K8s动态扩容)
- 优化消费者代码:
- 批量消费:
container.setBatchSize(50) - 关闭自动ACK
- 批量消费:
- 数据库批量插入:
INSERT INTO tb_order VALUES (...),(...); -- 批量代替单条
6. 项目进阶改造建议
6.1 升级为分库分表
当订单表超过500万行时,考虑使用ShardingSphere:
spring: shardingsphere: datasource: names: ds0,ds1 sharding: tables: tb_order: actual-data-nodes: ds$->{0..1}.tb_order_$->{0..15} table-strategy: inline: sharding-column: user_id algorithm-expression: tb_order_$->{user_id % 16}6.2 引入分布式事务
对于需要调用第三方支付的情况,采用Seata的AT模式:
@GlobalTransactional public Result pay(Long orderId) { orderService.updateStatus(orderId, PAID); paymentService.callAlipay(orderId); }6.3 压测指标参考
使用JMeter测试不同配置下的性能表现:
| 场景 | QPS | 平均耗时 | 错误率 |
|---|---|---|---|
| 纯MySQL | 218 | 4500ms | 12% |
| Redis+MySQL | 5400 | 120ms | 0.1% |
| 加本地缓存 | 7800 | 85ms | 0.01% |
| 集群模式(3节点) | 21000 | 65ms | 0% |
这个项目让我深刻体会到:高并发系统不是简单的技术堆砌,而是对每个环节的精细控制。比如在Redis库存扣减阶段,最初我们尝试用WATCH+MULTI,但在1000并发下就有明显冲突,改用Lua脚本后性能提升20倍。真正的技术价值往往体现在这些细节处理上。