2026/5/20 11:00:42
网站建设
项目流程
互动网站设计,wordpress mip,泸州设计公司有哪些,修改wordpress标签大小SpringBoot MyBatis-Plus Redis RabbitMQ#xff1a;电商秒杀场景下的库存预扣与订单异步创建作为一名有八年 Java 开发经验的老程序员#xff0c;我经历过从单体应用到分布式系统的各种架构演进。其中#xff0c;电商秒杀场景堪称高并发处理的 试金石#…SpringBoot MyBatis-Plus Redis RabbitMQ电商秒杀场景下的库存预扣与订单异步创建作为一名有八年 Java 开发经验的老程序员我经历过从单体应用到分布式系统的各种架构演进。其中电商秒杀场景堪称高并发处理的 试金石最能体现开发者对技术栈的综合运用能力。今天我想结合最新的技术实践聊聊如何用 SpringBoot MyBatis-Plus Redis RabbitMQ 这一套主流技术栈优雅地解决秒杀场景下的库存预扣与订单异步创建问题。秒杀场景的技术挑战秒杀业务看似简单用户抢购限量商品系统扣减库存并创建订单。但在高并发场景下这个过程会暴露出三大核心问题流量削峰秒杀瞬间的 QPS 可能是平时的 10-100 倍直接冲击数据库会导致系统崩溃库存一致性如何避免超卖和库存不足的情况下创建订单这是业务正确性的底线系统响应用户在秒杀场景下对响应速度预期极高超时会严重影响体验记得三年前我们团队第一次做大型秒杀活动时采用的是 数据库直接扣减 方案结果活动开始后 10 秒数据库就扛不住了大量连接超时最终只能紧急下线活动。那次事故让我们深刻认识到秒杀系统必须在架构层面做特殊设计。整体架构设计经过多次迭代优化我们形成了一套成熟的秒杀架构方案核心思路是 流量拦截 - 库存预扣 - 异步确认 - 最终一致markdown体验AI代码助手代码解读复制代码用户请求 → 前端限流 → SpringBoot接口 → Redis预扣库存 → 生成订单ID ↓ ↓ ↓ 按钮置灰 令牌桶限流 Lua原子操作 ↓ RabbitMQ消息队列 ↓ 订单服务消费者 ↓ MyBatis-Plus数据库操作 ↓ 库存最终扣减这套架构的关键设计决策用 Redis 做库存预扣和热点数据缓存扛住大部分读请求用 RabbitMQ 实现订单创建的异步化削峰填谷用 MyBatis-Plus 的乐观锁保证数据库层的库存一致性全程采用 一锁二判三更新 原则处理并发问题核心技术实现1. 库存预热与 Redis 预扣秒杀开始前我们需要将商品库存从数据库加载到 Redis这个过程称为 库存预热。预热时要设置合理的过期时间避免缓存雪崩typescript体验AI代码助手代码解读复制代码Service public class StockWarmUpService { Autowired private RedisTemplateString, Object redisTemplate; Autowired private ProductMapper productMapper; // 预热秒杀商品库存到Redis public void warmUpSeckillStock(Long seckillId) { Product product productMapper.selectById(seckillId); if (product null) { throw new BusinessException(商品不存在); } // 库存key设计seckill:stock:{商品ID} String stockKey seckill:stock: seckillId; // 售出计数keyseckill:sold:{商品ID} String soldKey seckill:sold: seckillId; // 设置库存过期时间设置为活动结束后1小时 redisTemplate.opsForValue().set(stockKey, product.getStock(), 1, TimeUnit.HOURS); redisTemplate.opsForValue().set(soldKey, 0, 1, TimeUnit.HOURS); } }秒杀接口中使用 Redis 进行库存预扣。这里的关键是用 Lua 脚本保证扣减操作的原子性避免并发问题typescript体验AI代码助手代码解读复制代码// Lua脚本检查并扣减库存 private static final String STOCK_DEDUCT_LUA local stockKey KEYS[1]\n local soldKey KEYS[2]\n local stock tonumber(redis.call(get, stockKey)) or 0\n local quantity tonumber(ARGV[1])\n if stock quantity then\n redis.call(decrby, stockKey, quantity)\n redis.call(incrby, soldKey, quantity)\n return 1\n end\n return 0; Service public class SeckillService { Autowired private StringRedisTemplate redisTemplate; Autowired private RabbitTemplate rabbitTemplate; public ResultString doSeckill(Long seckillId, Long userId, int quantity) { // 1. 检查用户是否已秒杀过防重复下单 String userSeckillKey seckill:user: userId : seckillId; Boolean hasSeckilled redisTemplate.hasKey(userSeckillKey); if (Boolean.TRUE.equals(hasSeckilled)) { return Result.fail(您已参与过秒杀请勿重复提交); } // 2. Redis预扣库存 String stockKey seckill:stock: seckillId; String soldKey seckill:sold: seckillId; Long result redisTemplate.execute( new DefaultRedisScript(STOCK_DEDUCT_LUA, Long.class), Arrays.asList(stockKey, soldKey), String.valueOf(quantity) ); if (result null || result 0) { return Result.fail(手慢了商品已抢完); } // 3. 记录用户秒杀记录设置过期时间 redisTemplate.opsForValue().set(userSeckillKey, 1, 24, TimeUnit.HOURS); // 4. 发送消息到RabbitMQ异步创建订单 OrderMessage message new OrderMessage(); message.setOrderId(generateOrderId()); message.setSeckillId(seckillId); message.setUserId(userId); message.setQuantity(quantity); message.setCreateTime(new Date()); rabbitTemplate.convertAndSend(seckill.order.exchange, seckill.order.key, message); return Result.success(message.getOrderId()); } }八年经验总结在 Redis 扣减库存时一定要用原子操作Lua 脚本或 Redis 命令避免先查后改的分布式问题。早期我们吃过这个亏导致少量超卖情况。2. RabbitMQ 异步创建订单为了应对秒杀高峰期的流量冲击订单创建必须异步化。我们使用 RabbitMQ 实现这一功能并利用死信队列处理超时未支付的订单。首先配置 RabbitMQtypescript体验AI代码助手代码解读复制代码Configuration public class RabbitMQConfig { // 普通交换机 public static final String SECKILL_ORDER_EXCHANGE seckill.order.exchange; // 普通队列 public static final String SECKILL_ORDER_QUEUE seckill.order.queue; // 死信交换机 public static final String SECKILL_DLX_EXCHANGE seckill.dlx.exchange; // 死信队列 public static final String SECKILL_DLX_QUEUE seckill.dlx.queue; // 声明普通交换机 Bean public DirectExchange seckillOrderExchange() { return new DirectExchange(SECKILL_ORDER_EXCHANGE, true, false); } // 声明普通队列指定死信交换机和过期时间 Bean public Queue seckillOrderQueue() { MapString, Object arguments new HashMap(); // 设置死信交换机 arguments.put(x-dead-letter-exchange, SECKILL_DLX_EXCHANGE); // 设置死信路由键 arguments.put(x-dead-letter-routing-key, seckill.dlx.key); // 设置消息过期时间15分钟未支付自动取消 arguments.put(x-message-ttl, 15 * 60 * 1000); return QueueBuilder.durable(SECKILL_ORDER_QUEUE) .withArguments(arguments) .build(); } // 绑定普通队列和交换机 Bean public Binding seckillOrderBinding() { return BindingBuilder.bind(seckillOrderQueue()) .to(seckillOrderExchange()) .with(seckill.order.key); } // 声明死信交换机和队列代码略 }订单消息消费者java体验AI代码助手代码解读复制代码Component public class OrderConsumer { Autowired private OrderService orderService; RabbitListener(queues RabbitMQConfig.SECKILL_ORDER_QUEUE) public void handleOrderMessage(OrderMessage message, Channel channel, Header(AmqpHeaders.DELIVERY_TAG) long tag) throws IOException { try { // 创建订单 orderService.createOrder(message); // 手动确认消息 channel.basicAck(tag, false); } catch (Exception e) { // 处理异常根据情况决定重试或拒绝 if (e instanceof BusinessException) { // 业务异常直接拒绝进入死信队列 channel.basicReject(tag, false); } else { // 非业务异常重试几次后进入死信队列 channel.basicNack(tag, false, false); } } } // 死信队列消费者处理超时未支付订单代码略 }八年经验总结消息队列一定要开启手动确认模式并合理设置重试策略。对于订单这类关键业务建议使用消息持久化和生产者确认机制确保消息不丢失。我们曾因未开启持久化在 MQ 重启后丢失了一批订单消息。3. MyBatis-Plus 实现数据库层库存扣减Redis 预扣只是第一步最终库存扣减需要在数据库层完成。这里我们使用 MyBatis-Plus 的乐观锁来处理并发问题。首先在实体类中添加版本号字段kotlin体验AI代码助手代码解读复制代码Data public class Product { private Long id; private String name; private Integer stock; // 乐观锁版本号 Version private Integer version; }配置乐观锁插件java体验AI代码助手代码解读复制代码Configuration public class MyBatisPlusConfig { Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor interceptor new MybatisPlusInterceptor(); // 添加乐观锁插件 interceptor.addInnerInterceptor(new OptimisticLockerInnerInterceptor()); return interceptor; } }订单服务中扣减库存scss体验AI代码助手代码解读复制代码Service Transactional public class OrderService { Autowired private ProductMapper productMapper; Autowired private OrderMapper orderMapper; Autowired private RedisTemplateString, Object redisTemplate; // 创建订单并扣减库存 public void createOrder(OrderMessage message) { // 1. 查询商品信息带乐观锁 Product product productMapper.selectById(message.getSeckillId()); if (product null) { throw new BusinessException(商品不存在); } // 2. 检查库存是否充足 if (product.getStock() message.getQuantity()) { // 库存不足需要回滚Redis预扣的库存 rollbackRedisStock(message.getSeckillId(), message.getQuantity()); throw new BusinessException(库存不足); } // 3. 扣减数据库库存乐观锁生效 int newStock product.getStock() - message.getQuantity(); product.setStock(newStock); int rows productMapper.updateById(product); // 4. 处理乐观锁更新失败的情况 if (rows 0) { // 回滚Redis库存 rollbackRedisStock(message.getSeckillId(), message.getQuantity()); throw new BusinessException(创建订单失败请重试); } // 5. 创建订单记录 Order order new Order(); order.setId(message.getOrderId()); order.setUserId(message.getUserId()); order.setProductId(message.getSeckillId()); order.setQuantity(message.getQuantity()); order.setStatus(OrderStatus.PENDING_PAYMENT); order.setCreateTime(message.getCreateTime()); orderMapper.insert(order); } // 回滚Redis库存 private void rollbackRedisStock(Long seckillId, int quantity) { String stockKey seckill:stock: seckillId; String soldKey seckill:sold: seckillId; redisTemplate.opsForValue().increment(stockKey, quantity); redisTemplate.opsForValue().decrement(soldKey, quantity); } }八年经验总结乐观锁在高并发场景下会出现更新失败的情况这时候一定要回滚 Redis 中预扣的库存否则会导致库存不一致。我们采用了 阶梯式重试 策略首次失败后间隔 100ms 重试第二次 200ms最多重试 3 次有效减少了失败率。库存一致性保障秒杀场景中库存一致性是核心问题。我们采用多层次保障机制Redis 预扣校验每次扣减前检查库存是否充足数据库乐观锁确保最终扣减的原子性定时对账任务每天凌晨比对 Redis 和数据库库存修复不一致ini体验AI代码助手代码解读复制代码Scheduled(cron 0 0 1 * * ?) // 每天凌晨1点执行 public void checkStockConsistency() { log.info(开始执行库存一致性检查); // 查询所有秒杀商品 ListProduct seckillProducts productMapper.selectSeckillProducts(); for (Product product : seckillProducts) { String stockKey seckill:stock: product.getId(); String soldKey seckill:sold: product.getId(); // 获取Redis中的库存和售出数量 Integer redisStock (Integer) redisTemplate.opsForValue().get(stockKey); Integer redisSold (Integer) redisTemplate.opsForValue().get(soldKey); // 计算Redis中的实际库存 初始库存 - 售出数量 Integer initialStock product.getInitialStock(); Integer actualRedisStock initialStock - redisSold; // 如果Redis库存与实际计算不符进行修正 if (!Objects.equals(redisStock, actualRedisStock)) { log.warn(库存不一致商品ID:{}Redis库存:{}实际应有的库存:{}, product.getId(), redisStock, actualRedisStock); redisTemplate.opsForValue().set(stockKey, actualRedisStock); } } log.info(库存一致性检查完成); }性能测试与优化为了验证系统在高并发下的表现我们使用 JMeter 进行压测。测试环境应用服务器4 核 8G2 台Redis8 核 16G主从架构MySQL8 核 16G读写分离RabbitMQ4 核 8G集群部署JMeter 配置线程数2000Ramp-Up 时间1 秒循环次数10使用 Throughput Shaping Timer 控制 QPS 在 2000 左右优化前的压测结果并不理想主要瓶颈在Redis 连接池耗尽数据库连接竞争激烈消息队列出现堆积针对这些问题我们做了以下优化Redis 优化调整连接池大小max-active200启用 Redis 集群分担压力热点数据本地缓存Caffeine数据库优化增加数据库连接池大小秒杀商品表单独分表索引优化商品 ID、订单状态等消息队列优化增加消费者实例调整 prefetchCount 参数启用消息压缩优化后的压测结果平均响应时间 200ms成功率99.9%最大 QPS3000无超卖和库存不一致情况八年开发经验总结回顾这些年处理秒杀系统的经验我总结出以下几点心得架构设计三原则能在前端拦截的绝不放到后端能在缓存处理的绝不访问数据库能异步处理的绝不同步执行技术选型要务实不要盲目追求新技术适合业务场景的才是最好的。我们曾尝试用分布式事务 Seata但发现对于秒杀场景最终一致性方案已经足够过度设计反而影响性能。容错设计很重要限流降级必须有这是系统的最后一道防线关键操作一定要有日志方便问题排查重要业务要考虑降级方案比如库存不足时返回友好提示而非系统错误性能优化是持续过程从最初的单机架构到现在的分布式系统我们经历了多次重构和优化。性能优化没有终点需要根据业务增长持续迭代。结语秒杀系统的设计与实现是一个综合性的工程涉及高并发、分布式、缓存、消息队列等多个技术领域。本文介绍的 SpringBoot MyBatis-Plus Redis RabbitMQ 方案通过库存预扣和异步订单创建有效解决了秒杀场景的核心痛点。随着业务的发展我们还将引入更多技术来优化系统比如接入 Sentinel 实现更精细的流量控制使用 Elasticsearch 存储订单日志方便分析尝试 Serverless 架构处理流量波动希望这篇文章能给正在开发秒杀系统的同行们一些参考也欢迎大家在评论区交流更多实战经验。架构之路无止境让我们一起在技术的道路上不断前行。作者天天摸鱼的java工程师链接https://juejin.cn/post/7551237527768776746来源稀土掘金著作权归作者所有。商业转载请联系作者获得授权非商业转载请注明出处。