秒杀系统设计高并发与可用
好的,以下是针对“设计一个高并发、高可用的秒杀系统”面试题的完整解答,包含了设计思路、技术选型、问题应对、扩展问题解答以及对候选人能力的分析:
设计思路:
秒杀系统本质上是一个高并发、高可用、数据强一致性的分布式系统设计问题。设计时需要遵循以下原则:
- 高性能(High Performance): 秒杀瞬间会有大量用户涌入,系统必须能够承受高并发请求,保证低延迟响应。
- 高可用(High Availability): 系统不能出现单点故障,需要具备容错和故障转移能力,保证服务在部分组件失效时仍能持续可用。
- 数据一致性(Data Consistency): 秒杀商品数量有限,必须严格保证数据的一致性,避免超卖、少卖、一人多拍等问题。
- 安全性(Security): 需要防止恶意请求、脚本刷单等行为,保证系统的安全性和秒杀活动的公平性。
- 可扩展性(Scalability): 系统应该能够通过水平扩展来应对不断增长的流量。
- 可维护性(Maintainability): 系统应该易于监控、调试和维护。
整体架构设计(分层架构):
我们将系统分为以下几个层次:
- 客户端层(Client Layer): 用户使用的App、Web页面等。
- 接入层/网关层(Access/Gateway Layer):
- 负载均衡(Load Balancing): 使用Nginx、HAProxy等,将用户请求分发到不同的应用服务器。
- 服务路由(Service Routing): 根据请求内容将请求路由到正确的服务。
- 安全防护(Security): 过滤非法请求,进行初步的安全检查(如身份验证)。
- 限流熔断(Rate Limiting & Circuit Breaking): 保护后端服务,防止系统过载。
- 应用服务层(Application Service Layer):
- 秒杀服务集群(Seckill Service Cluster): 处理秒杀业务逻辑,如校验用户资格、检查库存、创建订单等。
- 商品服务(可选):提供商品详情查询。
- 用户服务(可选):提供用户登录、身份校验。
- 订单服务(可选): 提供订单查询和管理功能。
- 中间件层(Middleware Layer):
- 缓存(Cache): 使用Redis等缓存热点数据(如商品信息、库存信息),大幅减少数据库访问压力,提高响应速度。
- 消息队列(Message Queue): 使用Kafka、RabbitMQ等,对秒杀请求进行异步处理(如订单创建),实现削峰填谷,提高系统吞吐量。
- 分布式锁服务: 使用Redis或ZooKeeper实现, 用来控制对共享资源的并发访问(如库存扣减)。
- 数据存储层(Data Storage Layer):
- 数据库(Database): 使用MySQL等关系型数据库,存储订单、用户等持久化数据。考虑分库分表,提高数据库读写性能。
- 数据同步: 将数据库中的热点数据同步到缓存中。
技术选型:
- 前端: HTML, CSS, JavaScript, 以及现代前端框架(如React, Vue, Angular)。使用CDN加速静态资源(图片、CSS、JavaScript文件),减少服务器压力,提升用户体验。
- 负载均衡: Nginx 或 HAProxy。Nginx性能优秀,配置灵活,支持反向代理、负载均衡、动静分离等功能。
- 应用服务器: Java (Spring Boot/Spring Cloud) 或 Go。选择高并发性能好的语言和框架。采用集群部署,提高系统的并发处理能力和可用性。
- 缓存: Redis。Redis是高性能的键值对(Key-Value)数据库,支持多种数据结构,非常适合缓存热点数据、实现分布式锁等。
- 消息队列: Kafka 或 RabbitMQ。Kafka适合高吞吐量场景,RabbitMQ在消息可靠性方面有优势。
- 数据库: MySQL。成熟稳定,支持事务,适合存储秒杀业务中的关键数据。采用分库分表策略,提升数据库性能。
- 分布式锁: Redis (Redlock算法) 或 ZooKeeper。保证在分布式环境下,对共享资源(如库存)的互斥访问。
- 限流: Guava RateLimiter (单机), Redis + Lua (分布式), Sentinel (阿里开源), Nginx (基于连接数或请求速率)。
- 降级/熔断: Hystrix, Sentinel。
- 服务注册与发现: Consul, Eureka, Nacos (阿里开源)。
- 配置中心: Spring Cloud Config, Apollo (携程开源), Nacos。
- API网关: Spring Cloud Gateway, Zuul, Kong。
- 监控: Prometheus + Grafana, ELK (Elasticsearch, Logstash, Kibana), Zabbix。
- 安全: 防火墙, WAF (Web Application Firewall), DDoS防护。
应对可能出现的问题:
- 高并发请求:
- 负载均衡: 将请求分发到多台应用服务器。
- 缓存: 缓存热点数据(如商品信息、库存信息),减少数据库访问。
- 消息队列: 将写请求(如下单)放入消息队列异步处理,实现削峰填谷。
- 限流: 限制请求速率,防止系统过载。
- 服务降级: 暂时关闭非核心功能,保证核心秒杀流程的正常运行。
- 预热: 系统上线前, 将热点数据预先加载到缓存中。
- 超卖问题:
- 数据库悲观锁: 使用
SELECT ... FOR UPDATE
语句锁定库存记录,确保同一时刻只有一个事务可以修改库存。 - 数据库乐观锁: 在库存表中增加版本号字段,更新时检查版本号,版本号不一致则更新失败(CAS操作)。
- Redis分布式锁: 使用Redis的
SETNX
命令或Redlock算法实现分布式锁,保证同一时刻只有一个请求可以扣减库存。 - 预扣库存: 在用户下单时,先在缓存中预扣库存(原子操作),预扣成功再进行后续操作(如创建订单)。
- 库存异步扣减: 可以将库存扣减操作放入消息队列中异步执行, 即使扣减失败, 也可以通过补偿机制进行处理。
- 数据库悲观锁: 使用
- 恶意请求:
- IP黑名单: 将恶意IP地址加入黑名单,拒绝其访问。
- 验证码: 在关键操作(如下单)前要求用户输入验证码,防止机器自动操作。
- 风控系统: 建立风控系统,基于用户行为(如请求频率、历史行为等)进行分析,识别异常请求并拦截。
- 用户限流: 限制单个用户在单位时间内的请求次数。
- 接口隐藏: 将秒杀接口地址隐藏起来, 通过动态生成接口地址的方式, 增加刷单的难度。
- 系统故障:
- 集群部署: 应用服务器、数据库等都采用集群部署,避免单点故障。
- 容错机制: 使用Hystrix、Sentinel等框架实现熔断、降级,防止故障扩散。
- 数据库主从复制、读写分离: 提高数据库可用性和读性能。
- 异地多活: 在不同地理位置部署多个机房,实现更高的可用性。
- 监控告警: 实时监控系统各项指标,及时发现并处理问题。
- 数据库压力:
- 读写分离: 将数据库的读操作和写操作分离到不同的数据库服务器上。
- 分库分表: 将数据分散存储到多个数据库或多个表中,减轻单库单表的压力。
- 使用缓存: 缓存热点数据,减少数据库访问。
扩展问题解答:
-
如何保证秒杀系统的公平性?
- 排队机制: 将秒杀请求放入消息队列,按照先到先得的原则处理。
- 随机数/摇号: 对秒杀请求进行随机排序或筛选,增加每个用户的参与机会(不保证绝对公平,但能防止先到先得的弊端)。
- 防刷机制: 限制单个用户的请求频率、购买数量等,防止恶意刷单。
- 反作弊系统: 分析用户行为,识别并阻止作弊行为。
- 公平性审计: 记录所有秒杀请求和处理结果,供后续审计。
- 答题/验证码:增加参与门槛, 减缓请求峰值, 也有一定的防刷作用。
-
如何防止超卖现象? (已在"应对可能出现的问题"中详细解答)
-
如果秒杀系统遇到恶意请求攻击,您会如何处理? (已在"应对可能出现的问题"中详细解答)
-
如何对秒杀系统进行监控和告警?
- 多维度监控:
- 系统监控: CPU、内存、磁盘I/O、网络I/O等。
- 应用监控: QPS(每秒查询率)、TPS(每秒事务数)、响应时间、错误率、线程池状态、JVM状态等。
- 业务监控: 秒杀请求数、下单数、支付成功数、库存变化等。
- 中间件监控: Redis缓存命中率、消息队列堆积情况等。
- 日志监控: 收集、分析、聚合系统日志,及时发现异常。
- 监控工具:
- Prometheus + Grafana: 开源、强大的监控和可视化工具。
- ELK (Elasticsearch, Logstash, Kibana): 日志收集、分析和可视化平台。
- Zabbix: 老牌监控系统,功能全面。
- 云服务商提供的监控服务: 如阿里云的CloudMonitor、AWS的CloudWatch。
- 告警:
- 设置阈值: 为关键指标设置合理的阈值。
- 告警方式: 短信、邮件、电话、IM(如钉钉、企业微信)等。
- 告警升级: 根据问题的严重程度,设置不同的告警级别,并通知到不同的人员。
- 告警抑制: 避免重复告警。
- 多维度监控:
-
如何评估秒杀系统的性能,并进行持续优化?
- 性能测试:
- 基准测试: 测试系统在正常负载下的性能。
- 压力测试: 逐步增加负载,测试系统的极限性能和稳定性。
- 负载测试: 模拟真实场景,测试系统在预期负载下的性能。
- 稳定性测试: 长时间运行压力测试,观察系统是否稳定。
- 测试工具:
- JMeter: 开源、功能强大的性能测试工具。
- LoadRunner: 商业性能测试工具,功能更全面。
- Gatling: 基于Scala的性能测试工具,适合高并发场景。
- 瓶颈分析:
- 全链路追踪: 使用分布式追踪系统(如Zipkin、Jaeger、SkyWalking)定位性能瓶颈。
- Profiling: 使用性能分析工具(如JProfiler、YourKit)分析代码层面的性能问题。
- 分析监控数据: 结合监控数据,分析系统瓶颈所在(如数据库、缓存、网络等)。
- 优化:
- 代码优化: 优化算法、数据结构、减少不必要的对象创建等。
- SQL优化: 优化SQL语句、使用索引、避免全表扫描等。
- 缓存优化: 调整缓存策略、增加缓存容量、使用更高效的缓存组件等。
- 网络优化: 减少HTTP请求数、使用CDN、优化网络协议等。
- 数据库优化: 分库分表、读写分离、使用更快的存储介质等。
- JVM优化: 调整JVM参数、使用合适的垃圾回收器等。
- 异步处理: 将耗时操作异步化,提高系统吞吐量。
- 水平扩展: 增加服务器数量。
- 性能测试:
对候选人能力的分析:
这道题目综合考察了候选人以下几个方面的能力:
- 系统设计能力: 候选人是否能独立设计复杂系统,根据需求进行技术选型、架构设计,并考虑系统的可扩展性、可维护性。
- 高并发处理能力: 候选人是否了解高并发场景下的常见问题(如超卖、雪崩、缓存击穿等)和解决方案,能否设计出高性能、高可用的系统。
- 问题解决能力: 候选人是否具备分析和解决问题的能力,能否应对各种可能出现的挑战,并给出合理的解决方案。
- 技术广度: 候选人是否对各种技术栈(如负载均衡、缓存、消息队列、数据库、分布式锁、监控等)都有一定的了解,能否根据实际情况进行选择和应用。
- 沟通表达能力: 候选人是否能清晰、有条理地阐述设计思路、技术选型和解决方案。
- 细节关注度: 候选人是否关注细节,对扩展问题的回答是否全面、深入。
如果候选人能够清晰、有条理地阐述设计思路、技术选型,并给出合理的解决方案,说明其具备较强的架构设计能力和高并发处理经验。同时,对扩展问题的回答也能反映出候选人对细节的关注程度和解决问题的能力。如果候选人能结合实际项目经验进行阐述,则更能体现其能力的真实性。
生成mermaid、plantuml、dot代码
好的,下面分别给出秒杀系统架构图的Mermaid、PlantUML和DOT代码表示:
1. Mermaid 代码 (流程图 - Flowchart):
2. PlantUML 代码 (组件图 - Component Diagram):
@startuml
!pragma layout smetanapackage "客户端层" {[App/Web] as Client[CDN] as CDN
}package "接入层/网关层" {[负载均衡\n(Nginx/HAProxy)] as LoadBalancer[API网关\n(服务路由, 安全防护,\n限流熔断)] as APIGateway
}package "应用服务层" {[秒杀服务集群] as SeckillService[商品服务] as ProductService[用户服务] as UserService[订单服务] as OrderService
}package "中间件层" {[Redis缓存] as RedisCache[消息队列\n(Kafka/RabbitMQ)] as MessageQueue[分布式锁\n(Redis/ZooKeeper)] as DistributedLock
}package "数据存储层" {[数据库集群\n(MySQL - 分库分表)] as Database[数据同步] as DataSync
}Client -- CDN
CDN -- LoadBalancer
LoadBalancer -- APIGateway
APIGateway -- SeckillService
APIGateway -- ProductService
APIGateway -- UserService
APIGateway -- OrderServiceSeckillService -- RedisCache
SeckillService -- MessageQueue
SeckillService -- DistributedLock
SeckillService -- Database
ProductService -- RedisCache
OrderService -- Database
Database -- DataSync
DataSync -- RedisCache@enduml
3. DOT 代码 (Graphviz):
digraph SeckillSystem {rankdir=LR;node [shape=box, style="filled", color="#333333", fillcolor="#f5f5f5", fontname="Arial"];edge [fontname="Arial"];subgraph cluster_client {label = "客户端层";Client [label="App/Web"];CDN [label="CDN"];}subgraph cluster_gateway {label = "接入层/网关层";LoadBalancer [label="负载均衡\n(Nginx/HAProxy)", style="filled,bold", fillcolor="#ccddff"];APIGateway [label="API网关\n(服务路由, 安全防护,\n限流熔断)"];}subgraph cluster_app {label = "应用服务层";SeckillService [label="秒杀服务集群", style=filled, fillcolor="#ffffcc"];ProductService [label="商品服务"];UserService [label="用户服务"];OrderService [label="订单服务"]}subgraph cluster_middleware {label = "中间件层";RedisCache [label="Redis缓存"];MessageQueue [label="消息队列\n(Kafka/RabbitMQ)"];DistributedLock[label="分布式锁\n(Redis/ZooKeeper)"]}subgraph cluster_data {label = "数据存储层";Database [label="数据库集群\n(MySQL - 分库分表)", style=filled, fillcolor="#ffcccc"];DataSync [label="数据同步"]}Client -> CDN;CDN -> LoadBalancer;LoadBalancer -> APIGateway;APIGateway -> SeckillService;APIGateway -> ProductService;APIGateway -> UserService;APIGateway -> OrderServiceSeckillService -> RedisCache;SeckillService -> MessageQueue;SeckillService -> DistributedLock;SeckillService -> Database;ProductService -> RedisCacheOrderService -> Database;Database -> DataSync;DataSync -> RedisCache;
}
解释和差异:
- Mermaid (流程图): 更侧重于表示请求的处理流程,从客户端到数据存储层的整个流程。
- PlantUML (组件图): 更侧重于展示系统的各个组件及其关系,适合描述系统的静态结构。
- DOT (Graphviz): 是一种通用的图形描述语言,可以用于绘制各种类型的图形。DOT代码更底层,可以更精细地控制图形的布局和样式,但需要手动编写更多细节。
三种表示方式都展示了相同的系统架构,但侧重点略有不同。在实际工作中,你可以根据需要选择合适的表示方式。例如:
- 向非技术人员展示系统概览时,Mermaid流程图可能更直观易懂。
- 在技术文档中描述系统架构时,PlantUML组件图可能更清晰。
- 需要对图形进行更精细的控制时,可以使用DOT代码。
以上代码可以直接复制到相应的工具中生成图形。例如:
- Mermaid: 可以在支持Mermaid语法的在线编辑器(如 Mermaid Live Editor)或Markdown编辑器中直接渲染。
- PlantUML: 可以使用PlantUML的在线服务器、IDE插件(如IntelliJ IDEA的PlantUML Integration插件)或命令行工具生成图形。
- DOT: 可以使用Graphviz的命令行工具(如
dot
命令)将DOT代码转换为图片(如PNG、SVG等)。 例如:dot -Tpng seckill.dot -o seckill.png
生成核心代码
好的,下面我将提供秒杀系统核心代码的Java (Spring Boot)示例,重点展示库存扣减和防超卖的逻辑。为了简化,这里只展示关键部分,省略了与数据库交互的具体实现(可以使用Spring Data JPA 或 MyBatis)。
1. 库存扣减服务 (StockService.java):
package com.example.seckill.service;import org.redisson.Redisson;
import org.redisson.api.RLock;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;import java.util.concurrent.TimeUnit;@Service
public class StockService {@Autowiredprivate StringRedisTemplate stringRedisTemplate;@Autowiredprivate Redisson redisson; // 假设你已经配置好了 RedissonClient//private static final String STOCK_KEY_PREFIX = "stock:"; // 假设使用Redis存储库存private static final String LOCK_KEY_PREFIX = "lock:product:"; // 分布式锁的 key 前缀// 方法1: 使用 Redis incr/decr + Lua 脚本实现原子性扣减库存 (推荐)public boolean deductStockWithLua(long productId, int quantity) {String stockKey = "stock:" + productId;String script ="local stock = tonumber(redis.call('get', KEYS[1])) " +"if (stock == nil or stock < tonumber(ARGV[1])) then " +" return 0 " + // 库存不足"end " +"redis.call('decrby', KEYS[1], ARGV[1]) " +"return 1"; // 扣减成功// 执行 Lua 脚本Long result = stringRedisTemplate.execute(org.springframework.data.redis.core.script.RedisScript.of(script, Long.class),java.util.List.of(stockKey),String.valueOf(quantity));return result != null && result == 1;}// 方法2: 使用 Redis 分布式锁 (Redisson)public boolean deductStockWithRedisson(long productId, int quantity) {String lockKey = LOCK_KEY_PREFIX + productId;RLock lock = redisson.getLock(lockKey);try {// 尝试获取锁,设置超时时间 (防止死锁)boolean lockAcquired = lock.tryLock(10, 5, TimeUnit.SECONDS); // 等待10秒,锁的持有时间5秒if (!lockAcquired) {return false; // 获取锁失败}// 获取锁成功,执行库存扣减String stockKey = "stock:" + productId;Integer currentStock = Integer.valueOf(stringRedisTemplate.opsForValue().get(stockKey));if (currentStock != null && currentStock >= quantity) {stringRedisTemplate.opsForValue().decrement(stockKey, quantity); //原子减return true;} else {return false; // 库存不足}} catch (InterruptedException e) {Thread.currentThread().interrupt(); // 恢复中断状态return false;} finally {// 释放锁if (lock.isHeldByCurrentThread()) { // 确保是当前线程持有的锁lock.unlock();}}}// 方法3: 数据库乐观锁 (假设数据库表中有 version 字段)// (需要配合 DAO 层代码,这里只展示逻辑)public boolean deductStockWithOptimisticLock(long productId, int quantity) {// 1. 查询当前库存和版本号// Stock stock = stockDao.getStock(productId);// 2. 检查库存是否足够// if (stock.getQuantity() < quantity) {// return false; // 库存不足// }// 3. 更新库存,同时检查版本号// int rowsAffected = stockDao.updateStock(productId, quantity, stock.getVersion());// 4. 如果 rowsAffected > 0,表示更新成功// return rowsAffected > 0;return false;}// 方法4: 数据库悲观锁 (使用 SELECT ... FOR UPDATE)// (需要配合 DAO 层代码,这里只展示逻辑, 且需要在事务中执行)public boolean deductStockWithPessimisticLock(long productId, int quantity) {// 1. 查询库存,并锁定该行记录 (SELECT ... FOR UPDATE)// Stock stock = stockDao.getStockForUpdate(productId);// 2. 检查库存是否足够// if (stock.getQuantity() < quantity) {// return false; // 库存不足// }// 3. 更新库存// stockDao.updateStockQuantity(productId, quantity);// 4. 提交事务 (事务会自动释放锁)// return true;return false;}//预扣库存, 在 Redis 中预扣public boolean preDeductStock(long productId, int quantity) {String stockKey = "stock:" + productId;Long newStock = stringRedisTemplate.opsForValue().decrement(stockKey, quantity); // 原子减if (newStock != null && newStock >= 0) {return true;}else{// 扣减失败,需要把库存加回去 (补偿操作,保证数据一致性)stringRedisTemplate.opsForValue().increment(stockKey, quantity);return false;}}
}
2. 秒杀控制器 (SeckillController.java):
package com.example.seckill.controller;import com.example.seckill.service.StockService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;@RestController
@RequestMapping("/seckill")
public class SeckillController {@Autowiredprivate StockService stockService;// 假设的秒杀接口@PostMapping("/doSeckill")public String doSeckill(@RequestParam("productId") long productId,@RequestParam("quantity") int quantity,@RequestParam("userId") long userId) {//1. 参数校验 (productId, quantity, userId) 非空, 合法性if (productId <= 0 || quantity <= 0 || userId <= 0) {return "Invalid parameters";}// 2. (可选) 用户限流、风控等 (这里省略)// ...// 3. 库存扣减// boolean success = stockService.deductStockWithRedisson(productId, quantity); // 使用 Redisson 分布式锁boolean success = stockService.deductStockWithLua(productId, quantity); //使用lua脚本// 4. 创建订单 (这里省略,可以异步创建订单)// ...// 5. 返回结果if (success) {return "Seckill successful!";} else {return "Seckill failed, out of stock!";}}// 假设的秒杀接口 -- 预扣库存@PostMapping("/doSeckillPreDeduct")public String doSeckillPreDeduct(@RequestParam("productId") long productId,@RequestParam("quantity") int quantity,@RequestParam("userId") long userId) {if (productId <= 0 || quantity <= 0 || userId <= 0) {return "Invalid parameters";}// 预扣库存boolean success = stockService.preDeductStock(productId, quantity);if (success) {//TODO: 后续操作,比如创建订单,支付等 (可以通过消息队列异步处理)// sendOrderMessageToQueue(productId, quantity, userId); //发送到消息队列return "Seckill request submitted, processing...";} else {return "Seckill failed, out of stock!";}}
}
代码说明:
-
StockService
:deductStockWithLua()
: 使用 Redis 的incr
/decr
命令结合 Lua 脚本,实现原子性的库存扣减。这是最推荐的方式,性能高且能保证数据一致性。deductStockWithRedisson()
: 使用 Redisson 提供的分布式锁(RLock
)来保证同一时刻只有一个请求可以扣减库存。deductStockWithOptimisticLock()
和deductStockWithPessimisticLock()
: 提供了基于数据库的乐观锁和悲观锁的示例(需要配合DAO层代码)。在并发极高的场景下,数据库锁可能成为瓶颈。preDeductStock()
: 预扣库存的实现。
-
SeckillController
:doSeckill()
: 一个简单的秒杀接口,调用StockService
进行库存扣减。doSeckillPreDeduct()
: 使用预扣库存方式的秒杀接口.
重要注意事项:
- Lua 脚本: 使用 Lua 脚本可以保证多个 Redis 操作的原子性,避免了竞态条件。
- Redisson: Redisson 是一个强大的 Redis 客户端,提供了分布式锁、分布式集合等功能。使用前需要添加 Redisson 的依赖,并配置好 RedissonClient。
- 异常处理: 在实际项目中,需要更完善的异常处理,例如捕获 Redis 连接异常、数据库操作异常等,并进行相应的处理(如重试、回滚)。
- 异步处理: 创建订单等操作可以放入消息队列(如 Kafka、RabbitMQ)中异步处理,提高系统吞吐量,并实现最终一致性。
- 限流、熔断、降级: 上述代码中省略了限流、熔断、降级的处理,实际项目中需要根据情况添加。
- 预扣库存: 预扣库存可以更早地发现库存不足的情况,减少无效请求对数据库的压力。预扣库存后,通常需要通过消息队列异步创建订单。
- 事务: 如果使用数据库悲观锁, 扣减库存和创建订单等操作需要在同一个数据库事务中。
这只是一个简化的示例,实际的秒杀系统会更复杂,需要考虑更多的细节。但是,这个示例展示了核心的库存扣减和防超卖的逻辑,以及几种不同的实现方式,可以作为进一步开发的基础。
加入一些细节
好的,让我们在之前的代码基础上加入更多细节,主要包括:
- 参数校验增强: 使用 Spring Validation 进行更全面的参数校验。
- 用户限流: 基于 Redis 实现简单的用户限流(限制单个用户在一段时间内的请求次数)。
- 异常处理: 添加更具体的异常处理,并返回更友好的错误信息。
- 异步创建订单: 使用 Spring 的
@Async
注解和TaskExecutor
实现异步创建订单(模拟,不涉及真正的消息队列)。 - 商品信息缓存: 从 Redis 缓存中获取商品信息(如库存)。
- 分布式Session: 假设使用了分布式 Session (例如 Spring Session + Redis) 来管理用户登录状态。
1. 修改 pom.xml (添加依赖):
如果还没有,需要添加 Spring Validation 和 Spring Session 的依赖:
<dependencies><!-- ... 其他依赖 ... --><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-validation</artifactId></dependency><dependency><groupId>org.springframework.session</groupId><artifactId>spring-session-data-redis</artifactId></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-aop</artifactId></dependency>
</dependencies>
2. 定义 DTO (Data Transfer Object):
// SeckillRequest.java (用于接收请求参数)
package com.example.seckill.dto;import javax.validation.constraints.Min;
import javax.validation.constraints.NotNull;
import java.io.Serializable;public class SeckillRequest implements Serializable {@NotNull(message = "商品ID不能为空")@Min(value = 1, message = "商品ID必须大于0")private Long productId;@NotNull(message = "购买数量不能为空")@Min(value = 1, message = "购买数量必须大于0")private Integer quantity;//不需要校验, 从session中获取.private Long userId;// Getters and setterspublic Long getProductId() {return productId;}public void setProductId(Long productId) {this.productId = productId;}public Integer getQuantity() {return quantity;}public void setQuantity(Integer quantity) {this.quantity = quantity;}public Long getUserId() {return userId;}public void setUserId(Long userId) {this.userId = userId;}
}// ProductInfo.java (商品信息,用于缓存)
package com.example.seckill.dto;
import java.io.Serializable;public class ProductInfo implements Serializable{private Long id;private String name;private Integer stock; // 库存// 构造函数、Getters and Setterspublic ProductInfo(){}public ProductInfo(Long id, String name, Integer stock) {this.id = id;this.name = name;this.stock = stock;}public Long getId() {return id;}public void setId(Long id) {this.id = id;}public String getName() {return name;}public void setName(String name) {this.name = name;}public Integer getStock() {return stock;}public void setStock(Integer stock) {this.stock = stock;}
}
3. 定义异常类:
// SeckillException.java
package com.example.seckill.exception;public class SeckillException extends RuntimeException {public SeckillException(String message) {super(message);}
}// OutOfStockException.java
package com.example.seckill.exception;
public class OutOfStockException extends SeckillException {public OutOfStockException(String message) {super(message);}
}// RateLimitException.java
package com.example.seckill.exception;
public class RateLimitException extends SeckillException{public RateLimitException(String message){super(message);}
}
4. 定义全局异常处理器:
// GlobalExceptionHandler.java
package com.example.seckill.exception;import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;import java.util.HashMap;
import java.util.Map;@ControllerAdvice
public class GlobalExceptionHandler {// 参数校验异常@ExceptionHandler(MethodArgumentNotValidException.class)public ResponseEntity<Object> handleValidationExceptions(MethodArgumentNotValidException ex) {Map<String, String> errors = new HashMap<>();ex.getBindingResult().getAllErrors().forEach((error) -> {String fieldName = ((org.springframework.validation.FieldError) error).getField();String errorMessage = error.getDefaultMessage();errors.put(fieldName, errorMessage);});return new ResponseEntity<>(errors, HttpStatus.BAD_REQUEST);}// 自定义秒杀异常@ExceptionHandler(SeckillException.class)public ResponseEntity<String> handleSeckillException(SeckillException ex) {return new ResponseEntity<>(ex.getMessage(), HttpStatus.BAD_REQUEST);}@ExceptionHandler(RateLimitException.class)public ResponseEntity<String> handleRateLimitException(RateLimitException ex){return new ResponseEntity<>(ex.getMessage(), HttpStatus.TOO_MANY_REQUESTS); // 429}// 其他异常@ExceptionHandler(Exception.class)public ResponseEntity<String> handleOtherExceptions(Exception ex) {// 记录日志...ex.printStackTrace();return new ResponseEntity<>("Internal Server Error", HttpStatus.INTERNAL_SERVER_ERROR);}
}
5. 修改 StockService:
package com.example.seckill.service;import com.example.seckill.dto.ProductInfo;
import com.example.seckill.exception.OutOfStockException;import org.redisson.Redisson;
import org.redisson.api.RLock;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.ValueOperations;
import org.springframework.stereotype.Service;import javax.annotation.PostConstruct;
import java.util.concurrent.TimeUnit;@Service
public class StockService {@Autowiredprivate StringRedisTemplate stringRedisTemplate;@Autowiredprivate Redisson redisson; // 假设你已经配置好了 RedissonClientprivate ValueOperations<String, String> valueOps;private static final String PRODUCT_INFO_KEY_PREFIX = "product:info:";private static final String LOCK_KEY_PREFIX = "lock:product:";private static final String USER_RATE_LIMIT_PREFIX = "user:rate_limit:";@PostConstructpublic void init() {valueOps = stringRedisTemplate.opsForValue();}//用户限流public boolean isUserRateLimited(Long userId, int limit, int seconds){String key = USER_RATE_LIMIT_PREFIX + userId;Long count = stringRedisTemplate.opsForValue().increment(key, 1); //计数+1if(count == 1){//第一次访问, 设置过期时间stringRedisTemplate.expire(key, seconds, TimeUnit.SECONDS);}return count > limit;}// 从缓存中获取商品信息public ProductInfo getProductInfoFromCache(Long productId) {String key = PRODUCT_INFO_KEY_PREFIX + productId;String productInfoJson = valueOps.get(key);if (productInfoJson != null) {// 从 JSON 字符串反序列化为 ProductInfo 对象 (这里需要使用 JSON 库, 如 Jackson)try {return com.fasterxml.jackson.databind.ObjectMapper.jsonMapper().readValue(productInfoJson, ProductInfo.class);} catch (com.fasterxml.jackson.core.JsonProcessingException e) {// throw new RuntimeException(e);//处理异常... log..e.printStackTrace();return null; //或者返回一个默认值}}return null; // 缓存中没有}// 缓存商品信息public void cacheProductInfo(ProductInfo productInfo) {String key = PRODUCT_INFO_KEY_PREFIX + productInfo.getId();// 将 ProductInfo 对象序列化为 JSON 字符串 (这里需要使用 JSON 库)try {String productInfoJson = com.fasterxml.jackson.databind.ObjectMapper.jsonMapper().writeValueAsString(productInfo);valueOps.set(key, productInfoJson);} catch (com.fasterxml.jackson.core.JsonProcessingException e) {//throw new RuntimeException(e);e.printStackTrace();//log}}// Lua 脚本扣减库存 (改进版)public boolean deductStockWithLua(long productId, int quantity) {String stockKey = "stock:" + productId;String script ="local stock = tonumber(redis.call('get', KEYS[1])) " +"if (stock == nil) then " +" return -1 " + // 商品不存在"end " +"if (stock < tonumber(ARGV[1])) then " +" return 0 " + // 库存不足"end " +"redis.call('decrby', KEYS[1], ARGV[1]) " +"return 1"; // 扣减成功Long result = stringRedisTemplate.execute(org.springframework.data.redis.core.script.RedisScript.of(script, Long.class),java.util.List.of(stockKey),String.valueOf(quantity));if(result == null){throw new SeckillException("秒杀失败, 未知错误");}if (result == -1) {throw new SeckillException("商品不存在"); // 更具体的异常} else if (result == 0) {throw new OutOfStockException("商品已售罄"); // 库存不足异常}return true; //result == 1}// 使用 Redisson 分布式锁扣减库存(改进版)public boolean deductStockWithRedisson(long productId, int quantity) {String lockKey = LOCK_KEY_PREFIX + productId;RLock lock = redisson.getLock(lockKey);try {boolean lockAcquired = lock.tryLock(10, 5, TimeUnit.SECONDS);if (!lockAcquired) {throw new SeckillException("系统繁忙,请稍后再试"); // 获取锁失败}String stockKey = "stock:" + productId;Integer currentStock = Integer.valueOf(stringRedisTemplate.opsForValue().get(stockKey));if (currentStock == null) {throw new SeckillException("商品不存在"); // 商品不存在}if (currentStock < quantity) {throw new OutOfStockException("商品已售罄"); // 库存不足}stringRedisTemplate.opsForValue().decrement(stockKey, quantity);return true;} catch (InterruptedException e) {Thread.currentThread().interrupt();throw new SeckillException("秒杀请求中断"); // 线程中断} finally {if (lock.isHeldByCurrentThread()) {lock.unlock();}}}// 其他扣减库存方法 (乐观锁、悲观锁) 保持不变,但可以根据需要添加异常处理// 预扣库存(改进)public boolean preDeductStock(long productId, int quantity) {String stockKey = "stock:" + productId;Long newStock = stringRedisTemplate.opsForValue().decrement(stockKey, quantity); // 原子减if (newStock == null) {throw new SeckillException("商品不存在");}else if (newStock >= 0) {return true;}else{//还原库存stringRedisTemplate.opsForValue().increment(stockKey, quantity);throw new OutOfStockException("商品已售罄"); // 库存不足异常}}
}
6. 异步创建订单服务 (OrderService.java):
package com.example.seckill.service;import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;@Service
public class OrderService {@Async // 使用 @Async 注解,表示该方法将在 Spring 的 TaskExecutor 中异步执行public void createOrder(Long userId, Long productId, Integer quantity) {// 模拟创建订单的耗时操作 (例如数据库写入)try {Thread.sleep(200); // 模拟耗时} catch (InterruptedException e) {Thread.currentThread().interrupt();}// TODO: 实际的订单创建逻辑 (写入数据库)System.out.println(String.format("Thread: %s, User %d created order for product %d, quantity: %d",Thread.currentThread().getName(), userId, productId, quantity));}//假设扣减数据库库存成功, 更新缓存@Asyncpublic void updateCacheAfterDeductStock(long productId, int quantity){//获取缓存的商品信息ProductInfo productInfo = stockService.getProductInfoFromCache(productId);if(productInfo != null){//更新缓存中的库存productInfo.setStock(productInfo.getStock() - quantity);//重新缓存商品信息stockService.cacheProductInfo(productInfo);}}
}
7. 修改 SeckillController:
package com.example.seckill.controller;import com.example.seckill.dto.ProductInfo;
import com.example.seckill.dto.SeckillRequest;
import com.example.seckill.exception.RateLimitException;
import com.example.seckill.service.OrderService;
import com.example.seckill.service.StockService;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;import javax.servlet.http.HttpSession;@RestController
@RequestMapping("/seckill")
@Validated // 开启方法级别的参数校验
public class SeckillController {@Autowiredprivate StockService stockService;@Autowiredprivate OrderService orderService;private static final int USER_REQUEST_LIMIT = 5; // 每分钟最多请求5次private static final int RATE_LIMIT_SECONDS = 60; // 60秒@PostMapping("/doSeckill")public String doSeckill(@Validated @RequestBody SeckillRequest request, HttpSession session) {// 从 Session 中获取用户ID (假设用户已登录)// Long userId = (Long) session.getAttribute("userId"); //实际应该使用 用户对象Long userId = 123L; //测试, 假设 用户ID= 123if (userId == null) {return "用户未登录"; // 应该重定向到登录页面}request.setUserId(userId); //设置到请求对象中// 用户限流if(stockService.isUserRateLimited(userId, USER_REQUEST_LIMIT, RATE_LIMIT_SECONDS)){throw new RateLimitException("操作太频繁, 请稍后再试!");}// 从缓存中获取商品信息,检查库存ProductInfo productInfo = stockService.getProductInfoFromCache(request.getProductId());if (productInfo == null) {return "商品不存在";}if (productInfo.getStock() < request.getQuantity()) {return "商品已售罄"; // 这里可以返回更友好的提示}// 库存扣减boolean success = stockService.deductStockWithLua(request.getProductId(), request.getQuantity());// 异步创建订单if (success) {orderService.createOrder(userId, request.getProductId(), request.getQuantity());//更新缓存orderService.updateCacheAfterDeductStock(request.getProductId(),request.getQuantity());return "秒杀成功,订单创建中..."; // 给用户一个友好的提示} else {// return "Seckill failed, out of stock!"; //前面已经处理了.return "未知错误"; //}}//预扣库存的接口, 参考上面, 略.
}
8. 配置 TaskExecutor (如果需要自定义):
// AppConfig.java (或者任何 @Configuration 类)
package com.example.seckill.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;import java.util.concurrent.Executor;@Configuration
@EnableAsync // 启用异步方法支持
public class AppConfig {@Bean(name = "taskExecutor")public Executor taskExecutor() {ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();executor.setCorePoolSize(10); // 核心线程数executor.setMaxPoolSize(20); // 最大线程数executor.setQueueCapacity(100); // 队列容量executor.setThreadNamePrefix("OrderAsync-"); // 线程名前缀executor.initialize();return executor;}
}
改动说明:
- DTO 和参数校验: 使用
@Validated
和@RequestBody
注解,结合 DTO 中的校验注解(如@NotNull
,@Min
),实现对请求参数的自动校验。 - 用户限流:
StockService
中添加了isUserRateLimited()
方法,使用 Redis 的计数器来实现简单的用户限流。 - 异常处理:
- 定义了
SeckillException
、OutOfStockException
和RateLimitException
等自定义异常。 StockService
中的方法现在抛出更具体的异常。- 创建了
GlobalExceptionHandler
来统一处理异常,并返回更友好的错误信息给客户端。
- 定义了
- 异步创建订单:
OrderService
中的createOrder()
方法使用了@Async
注解,使其在 Spring 的TaskExecutor
中异步执行。- 提供了
AppConfig
来配置自定义的TaskExecutor
(如果需要)。
- 商品缓存
StockService
添加了getProductInfoFromCache
从redis缓存中获取商品信息。- 添加了
cacheProductInfo
方法,用于缓存商品信息。 - 添加了
updateCacheAfterDeductStock
用于在扣减库存成功后,异步更新缓存(避免直接操作缓存导致并发问题)。
改进后的代码具有以下优点:
- 更好的可读性和可维护性: 将不同的逻辑分离到不同的类和服务中。
- 更健壮: 添加了更全面的参数校验和异常处理。
- 更高的性能: 使用异步处理和缓存来提高系统吞吐量。
- 更易于扩展: 代码结构更清晰,更容易添加新功能或修改现有功能。
后续改进方向:
- 引入消息队列: 将异步创建订单的操作改为发送消息到消息队列(如 Kafka、RabbitMQ),由专门的消费者来处理订单创建。
- 分布式事务: 如果库存扣减和订单创建不在同一个数据库中,需要考虑分布式事务(如使用 Seata)。
- 更完善的限流: 可以使用更成熟的限流组件(如 Sentinel、Resilience4j)来实现更复杂的限流策略。
- 监控和告警: 添加对系统关键指标(如 QPS、响应时间、错误率)的监控,并设置告警。
- 压测和优化: 对系统进行压力测试,找出性能瓶颈并进行优化。
- 使用令牌桶或漏桶算法进行限流。
- 数据库优化: 使用索引、分库分表、读写分离等技术优化数据库性能。
这个更详细的版本仍然不是一个完整的生产级秒杀系统,但它展示了许多重要的细节和最佳实践。在实际开发中,你需要根据具体需求和场景进行调整和扩展。
进一步完善、改进
好的,让我们在前一版的基础上进一步完善和改进,这次的重点是:
- 完整的预扣库存 + 异步下单流程: 使用 Redis 预扣库存,成功后发送消息到消息队列(模拟),由消费者异步创建订单,并处理库存回补(如果订单创建失败)。
- 更精细的库存控制: 区分“预扣库存”和“实际库存”,避免超卖的同时,允许一定的“超卖”额度(例如,允许 10% 的超卖,用于提高秒杀成功率,但最终通过数据库事务保证一致性)。
- 幂等性处理: 在订单创建时进行幂等性校验,防止重复创建订单。
- 更友好的用户提示: 根据不同情况,返回更具体的提示信息。
- 代码模块化: 将不同的功能拆分到更小的模块中,提高代码的可读性和可维护性。
- AOP 日志: 使用 Spring AOP 记录关键操作的日志。
项目结构调整:
com.example.seckill
├── config // 配置类
│ ├── AppConfig.java
│ └── RedisConfig.java (如果需要自定义 Redis 配置)
├── controller
│ └── SeckillController.java
├── dto
│ ├── ProductInfo.java
│ ├── SeckillRequest.java
│ └── OrderDto.java (新增:订单信息 DTO)
├── exception
│ ├── GlobalExceptionHandler.java
│ ├── OutOfStockException.java
│ ├── RateLimitException.java
│ └── SeckillException.java
├── service
│ ├── OrderService.java
│ ├── StockService.java
│ └── SeckillService.java (新增:将秒杀逻辑从 Controller 移到 Service)
├── dao // 数据库访问层 (这里只给出接口定义)
│ ├── OrderDao.java
│ └── ProductDao.java
├── enums // 枚举 (可选,例如订单状态)
│ └── OrderStatus.java
├── util // 工具类
│ └── RedisKeyUtil.java (用于生成 Redis Key)
└── aop└── SeckillLogAspect.java (AOP 日志切面)
1. 定义 DTO 和 DAO 接口:
// OrderDto.java (dto 目录下)
package com.example.seckill.dto;import java.io.Serializable;
import java.math.BigDecimal;
import java.time.LocalDateTime;public class OrderDto implements Serializable {private Long id;private Long userId;private Long productId;private Integer quantity;private BigDecimal amount; // 订单金额private LocalDateTime createTime;private String status; // 使用 String 或 OrderStatus 枚举// Getters and Setters ...public OrderDto(){}public OrderDto(Long id, Long userId, Long productId, Integer quantity, BigDecimal amount, LocalDateTime createTime, String status) {this.id = id;this.userId = userId;this.productId = productId;this.quantity = quantity;this.amount = amount;this.createTime = createTime;this.status = status;}//getter and setter ...
}// OrderDao.java (dao 目录下)
package com.example.seckill.dao;import com.example.seckill.dto.OrderDto;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;@Mapper // 假设使用 MyBatis
public interface OrderDao {// 插入订单int insertOrder(OrderDto orderDto);// 根据订单 ID 查询订单OrderDto findById(Long orderId);// 根据用户 ID 和商品 ID 查询订单 (用于幂等性校验)OrderDto findByUserIdAndProductId(@Param("userId") Long userId, @Param("productId") Long productId);// 更新订单状态int updateOrderStatus(@Param("orderId") Long orderId, @Param("status") String status);
}// ProductDao.java (dao 目录下)
package com.example.seckill.dao;import com.example.seckill.dto.ProductInfo;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;@Mapper
public interface ProductDao {// 查询商品信息ProductInfo findById(Long productId);// 扣减库存 (数据库乐观锁)int deductStock(@Param("productId") Long productId, @Param("quantity") int quantity, @Param("version") int version);// 增加库存(用于回补)int increaseStock(@Param("productId") Long productId, @Param("quantity") int quantity);
}
2. 定义枚举 (OrderStatus.java):
// OrderStatus.java (enums 目录下)
package com.example.seckill.enums;public enum OrderStatus {CREATED, // 已创建PAID, // 已支付SHIPPED, // 已发货COMPLETED, // 已完成CANCELED, // 已取消FAILED // 失败(例如支付失败)
}
3. 定义工具类 (RedisKeyUtil.java):
// RedisKeyUtil.java (util 目录下)
package com.example.seckill.util;public class RedisKeyUtil {private static final String STOCK_KEY_PREFIX = "stock:";private static final String PRE_DEDUCTED_STOCK_KEY_PREFIX = "pre_deducted_stock:";private static final String PRODUCT_INFO_KEY_PREFIX = "product:info:";private static final String USER_RATE_LIMIT_PREFIX = "user:rate_limit:";private static final String ORDER_ID_SET_PREFIX = "order:id:set:"; // 订单ID集合, 用于幂等public static String getStockKey(Long productId) {return STOCK_KEY_PREFIX + productId;}public static String getPreDeductedStockKey(Long productId) {return PRE_DEDUCTED_STOCK_KEY_PREFIX + productId;}public static String getProductInfoKey(Long productId) {return PRODUCT_INFO_KEY_PREFIX + productId;}public static String getUserRateLimitKey(Long userId) {return USER_RATE_LIMIT_PREFIX + userId;}public static String getOrderIdSetKey(Long userId, Long productId) {return ORDER_ID_SET_PREFIX + userId + ":" + productId;}
}
4. 修改 StockService.java:
package com.example.seckill.service;import com.example.seckill.dto.ProductInfo;
import com.example.seckill.exception.OutOfStockException;
import com.example.seckill.exception.SeckillException;
import com.example.seckill.util.RedisKeyUtil;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.ValueOperations;
import org.springframework.stereotype.Service;
import javax.annotation.PostConstruct;
import java.util.concurrent.TimeUnit;@Service
public class StockService {@Autowiredprivate StringRedisTemplate stringRedisTemplate;private ValueOperations<String, String> valueOps;// 允许的超卖比例 (例如 10%)private static final double OVERSELL_RATIO = 0.1;@PostConstructpublic void init() {valueOps = stringRedisTemplate.opsForValue();}// 用户限流 (保持不变)public boolean isUserRateLimited(Long userId, int limit, int seconds) {String key = RedisKeyUtil.getUserRateLimitKey(userId);Long count = stringRedisTemplate.opsForValue().increment(key, 1);if (count == 1) {stringRedisTemplate.expire(key, seconds, TimeUnit.SECONDS);}return count > limit;}// 从缓存中获取商品信息 (保持不变)public ProductInfo getProductInfoFromCache(Long productId) {String key = RedisKeyUtil.getProductInfoKey(productId);String productInfoJson = valueOps.get(key);if (productInfoJson != null) {// 从 JSON 字符串反序列化为 ProductInfo 对象 (这里需要使用 JSON 库, 如 Jackson)try {return com.fasterxml.jackson.databind.ObjectMapper.jsonMapper().readValue(productInfoJson, ProductInfo.class);} catch (com.fasterxml.jackson.core.JsonProcessingException e) {//处理异常... log..e.printStackTrace();return null; //或者返回一个默认值}}return null; // 缓存中没有}// 缓存商品信息 (保持不变)public void cacheProductInfo(ProductInfo productInfo) {String key = RedisKeyUtil.getProductInfoKey(productInfo.getId());// 将 ProductInfo 对象序列化为 JSON 字符串 (这里需要使用 JSON 库)try {String productInfoJson = com.fasterxml.jackson.databind.ObjectMapper.jsonMapper().writeValueAsString(productInfo);valueOps.set(key, productInfoJson);} catch (com.fasterxml.jackson.core.JsonProcessingException e) {e.printStackTrace();}}// 预扣库存 (改进:考虑超卖)public boolean preDeductStock(Long productId, int quantity) {String stockKey = RedisKeyUtil.getStockKey(productId); // 实际库存 keyString preDeductedKey = RedisKeyUtil.getPreDeductedStockKey(productId); // 预扣库存 key// 获取实际库存和预扣库存Integer stock = getStockFromRedis(stockKey);Integer preDeducted = getStockFromRedis(preDeductedKey); // 预扣库存if (stock == null) {throw new SeckillException("商品不存在");}// 计算允许的预扣数量int allowedPreDeduct = (int) (stock + stock * OVERSELL_RATIO) - (preDeducted != null ? preDeducted : 0);if (allowedPreDeduct < quantity) {throw new OutOfStockException("商品已售罄");}// 使用 Redis incrby 命令增加预扣库存 (原子操作)Long newPreDeducted = stringRedisTemplate.opsForValue().increment(preDeductedKey, quantity);if(newPreDeducted == null){//增加预扣库存失败throw new SeckillException("系统繁忙, 请稍后重试");}return true;}private Integer getStockFromRedis(String key) {String stockStr = valueOps.get(key);return stockStr == null ? null : Integer.parseInt(stockStr);}// 还原预扣库存 (在订单创建失败时调用)public void revertPreDeductedStock(Long productId, int quantity) {String preDeductedKey = RedisKeyUtil.getPreDeductedStockKey(productId);stringRedisTemplate.opsForValue().decrement(preDeductedKey, quantity);}// 从 Redis 中获取实际库存 (用于初始化缓存)public Integer getActualStockFromRedis(Long productId) {String stockKey = RedisKeyUtil.getStockKey(productId);String stockStr = valueOps.get(stockKey);return stockStr == null ? null : Integer.parseInt(stockStr);}// 设置 Redis 中的实际库存 (用于初始化缓存)public void setActualStockInRedis(Long productId, int stock) {String stockKey = RedisKeyUtil.getStockKey(productId);valueOps.set(stockKey, String.valueOf(stock));}
}
5. 新增 SeckillService.java:
package com.example.seckill.service;import com.example.seckill.dao.OrderDao;
import com.example.seckill.dao.ProductDao;
import com.example.seckill.dto.OrderDto;
import com.example.seckill.dto.ProductInfo;
import com.example.seckill.dto.SeckillRequest;
import com.example.seckill.enums.OrderStatus;
import com.example.seckill.exception.OutOfStockException;
import com.example.seckill.exception.RateLimitException;
import com.example.seckill.exception.SeckillException;import com.example.seckill.util.RedisKeyUtil;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;import java.math.BigDecimal;
import java.time.LocalDateTime;@Service
public class SeckillService {@Autowiredprivate StockService stockService;@Autowiredprivate OrderService orderService;@Autowiredprivate ProductDao productDao;@Autowiredprivate OrderDao orderDao;@Autowiredprivate StringRedisTemplate stringRedisTemplate;private static final int USER_REQUEST_LIMIT = 5; // 每分钟最多请求 5 次private static final int RATE_LIMIT_SECONDS = 60;// 秒杀核心逻辑 (预扣库存 + 异步下单)public String doSeckill(SeckillRequest request) {Long userId = request.getUserId();Long productId = request.getProductId();Integer quantity = request.getQuantity();// 1. 用户限流if (stockService.isUserRateLimited(userId, USER_REQUEST_LIMIT, RATE_LIMIT_SECONDS)) {throw new RateLimitException("操作太频繁,请稍后再试!");}// 2. 检查并预扣库存try {stockService.preDeductStock(productId, quantity);} catch (OutOfStockException e) {return "商品已售罄";}// 3. 幂等性校验 (防止重复下单)//使用setnxString orderIdSetKey = RedisKeyUtil.getOrderIdSetKey(userId, productId);Boolean isNewOrder = stringRedisTemplate.opsForValue().setIfAbsent(orderIdSetKey, "1", 60, TimeUnit.SECONDS); // 假设订单ID有效期60秒if (Boolean.FALSE.equals(isNewOrder)) {//如果已经存在订单, 直接返回return "您已经购买过该商品,请勿重复下单!";}// 4. 异步创建订单 (通过消息队列模拟)// 构建订单信息OrderDto orderDto = new OrderDto();orderDto.setUserId(userId);orderDto.setProductId(productId);orderDto.setQuantity(quantity);orderDto.setStatus(OrderStatus.CREATED.name()); // 初始状态// ... 其他订单信息 ...// 发送消息到消息队列 (这里只是模拟)orderService.createOrderAsync(orderDto);return "秒杀请求已提交,请稍后查看订单!";}// 初始化商品信息到 Redis (可以在系统启动时调用,或者通过后台管理界面触发)@Transactional(readOnly = true)public void initProductInfoToRedis() {// 查询所有商品 (实际中可能需要分页查询)// List<ProductInfo> products = productDao.findAll();// 这里为了演示,假设只有一个商品ProductInfo product = productDao.findById(1L); // 假设商品ID为1if (product != null) {// 缓存商品信息stockService.cacheProductInfo(product);// 设置 Redis 中的实际库存stockService.setActualStockInRedis(product.getId(), product.getStock());//初始化预扣库存stringRedisTemplate.opsForValue().set(RedisKeyUtil.getPreDeductedStockKey(product.getId()), "0");}}
}
6. 修改 OrderService.java:
package com.example.seckill.service;import com.example.seckill.dao.OrderDao;
import com.example.seckill.dao.ProductDao;
import com.example.seckill.dto.OrderDto;
import com.example.seckill.dto.ProductInfo;
import com.example.seckill.enums.OrderStatus;
import com.example.seckill.exception.SeckillException;import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;import java.math.BigDecimal;
import java.time.LocalDateTime;@Service
public class OrderService {@Autowiredprivate OrderDao orderDao;@Autowiredprivate ProductDao productDao;@Autowiredprivate StockService stockService;@Autowiredprivate StringRedisTemplate stringRedisTemplate;// 异步创建订单 (模拟消息队列的消费者)@Async@Transactional // 开启数据库事务public void createOrderAsync(OrderDto orderDto) {try {// 1. 查询商品信息 (需要最新的库存信息,所以从数据库查)ProductInfo product = productDao.findById(orderDto.getProductId());if (product == null) {//商品为空,// 记录日志, 并通知相关人员...return;}//设置订单价格等orderDto.setAmount(new BigDecimal(100)); //假设价格100// 2. 扣减数据库库存 (乐观锁)int rowsAffected = productDao.deductStock(product.getId(), orderDto.getQuantity(), product.getVersion());if (rowsAffected == 0) {// 扣减库存失败,可能是并发导致,需要还原 Redis 中的预扣库存stockService.revertPreDeductedStock(product.getId(), orderDto.getQuantity());//删除幂等性处理的 keystringRedisTemplate.delete(RedisKeyUtil.getOrderIdSetKey(orderDto.getUserId(), orderDto.getProductId()));// 记录日志,并进行相应处理 (例如通知用户稍后再试)return;}// 3. 创建订单orderDto.setCreateTime(LocalDateTime.now());orderDao.insertOrder(orderDto); // 插入订单// 4. 更新订单状态 (可选)// orderDao.updateOrderStatus(orderDto.getId(), OrderStatus.PAID.name());// 5. 更新缓存 (可选,这里假设不更新缓存,而是让缓存自然过期)} catch (Exception e) {// 记录异常日志e.printStackTrace();// 数据库操作失败,需要回滚事务,并还原 Redis 中的预扣库存stockService.revertPreDeductedStock(orderDto.getProductId(), orderDto.getQuantity());//删除幂等性处理的 keystringRedisTemplate.delete(RedisKeyUtil.getOrderIdSetKey(orderDto.getUserId(), orderDto.getProductId()));// 抛出异常或进行其他处理throw new SeckillException("创建订单失败"); // 或者更具体的异常}}//假设扣减数据库库存成功, 更新缓存@Asyncpublic void updateCacheAfterDeductStock(long productId, int quantity){//获取缓存的商品信息ProductInfo productInfo = stockService.getProductInfoFromCache(productId);if(productInfo != null){//更新缓存中的库存productInfo.setStock(productInfo.getStock() - quantity);//重新缓存商品信息stockService.cacheProductInfo(productInfo);}}
}
7. 修改 SeckillController.java:
package com.example.seckill.controller;import com.example.seckill.dto.SeckillRequest;
import com.example.seckill.service.SeckillService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;import javax.servlet.http.HttpSession;@RestController
@RequestMapping("/seckill")
public class SeckillController {@Autowiredprivate SeckillService seckillService;@PostMapping("/doSeckill")public String doSeckill(@Validated @RequestBody SeckillRequest request, HttpSession session) {Long userId = 123L; //测试, 假设用户id// Long userId = (Long) session.getAttribute("userId"); // 实际应该从session中获取if (userId == null) {return "用户未登录";}request.setUserId(userId);// 调用 SeckillService 处理秒杀逻辑return seckillService.doSeckill(request);}
}
8. AOP 日志 (SeckillLogAspect.java):
package com.example.seckill.aop;import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.AfterReturning;
import org.aspectj.lang.annotation.AfterThrowing;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import java.util.Arrays;@Aspect
@Component
public class SeckillLogAspect {private static final Logger logger = LoggerFactory.getLogger(SeckillLogAspect.class);// 定义切点 (例如,拦截 SeckillService 中的所有方法)@Pointcut("execution(* com.example.seckill.service.SeckillService.*(..))")public void seckillServiceMethods() {}// 前置通知@Before("seckillServiceMethods()")public void beforeSeckill(JoinPoint joinPoint) {String methodName = joinPoint.getSignature().getName();Object[] args = joinPoint.getArgs();logger.info("Before executing method: {}, with arguments: {}", methodName, Arrays.toString(args));}// 后置通知 (正常返回)@AfterReturning(pointcut = "seckillServiceMethods()", returning = "result")public void afterReturning(JoinPoint joinPoint, Object result) {String methodName = joinPoint.getSignature().getName();logger.info("Method {} executed successfully, returning: {}", methodName, result);}// 异常通知@AfterThrowing(pointcut = "seckillServiceMethods()", throwing = "ex")public void afterThrowing(JoinPoint joinPoint, Throwable ex) {String methodName = joinPoint.getSignature().getName();logger.error("Exception in method {}: {}", methodName, ex.getMessage());}
}
主要改动:
- 项目结构: 进行了更细致的模块划分。
SeckillService
: 将秒杀的核心逻辑从SeckillController
移到了SeckillService
,使Controller
更简洁。- 库存控制:
- 区分了“实际库存”和“预扣库存”。
- 允许一定的超卖比例,提高了秒杀成功率。
- 最终通过数据库事务保证数据一致性。
- 异步下单:
- 使用 Redis 预扣库存。
- 预扣成功后,发送消息到消息队列(模拟)。
OrderService
中的createOrderAsync
方法模拟消息队列的消费者,异步创建订单。- 订单创建失败时,还原 Redis 中的预扣库存。
- 幂等性:
- 在
SeckillService
中,使用 Redis 的SETNX
命令进行幂等性校验。
- 在
- 更友好的提示: 根据不同情况,返回更具体的提示信息给用户。
- AOP 日志: 使用 Spring AOP 记录关键操作的日志,方便调试和监控。
- Redis Key 工具类: 提取Redis Key到单独的工具类中.
注意事项:
- 消息队列: 这个版本仍然使用
@Async
注解来模拟消息队列。在生产环境中,应该使用真正的消息队列(如 RabbitMQ、Kafka)。 - 数据库: 这个版本假设使用了 MyBatis 和 MySQL。你需要根据实际情况修改 DAO 接口和实现。
- 分布式事务: 如果库存和订单不在同一个数据库,需要考虑分布式事务。
- 代码中的事务: 已经添加了
@Transactional
注解来管理数据库事务。
这个版本比之前的版本更接近生产级别的秒杀系统,但仍然有很多可以改进的地方。希望这个详细的解答对你有帮助!
继续改进
好的,让我们在上一版的基础上继续深入优化和改进,这次的重点将放在以下几个方面:
- 引入 Sentinel 实现限流和熔断: 使用 Alibaba Sentinel 替代之前简单的基于 Redis 的限流,提供更强大、更灵活的限流和熔断功能。
- Lua 脚本优化: 将预扣库存、幂等性校验等操作整合到 Lua 脚本中,减少网络开销,提高性能。
- 订单超时处理: 使用 Redis 过期事件或定时任务来处理超时未支付的订单,释放预扣库存。
- 分布式锁: 使用 Redisson 替代之前的 Redis setnx 实现分布式锁,确保在分布式环境下数据的一致性。
- 接口优化: 使用Resultful 风格, 并添加接口文档 (Swagger/OpenAPI)。
- 更完善的测试: 增加单元测试和集成测试,提高代码质量。 (仅提供示例)
1. 引入 Sentinel:
- pom.xml 添加依赖:
<dependency><groupId>com.alibaba.cloud</groupId><artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
</dependency><!-- 如果使用 Sentinel Dashboard,还需要添加以下依赖 -->
<dependency><groupId>com.alibaba.csp</groupId><artifactId>sentinel-transport-simple-http</artifactId>
</dependency>
- application.yml 配置:
spring:cloud:sentinel:transport:dashboard: localhost:8080 # Sentinel Dashboard 地址port: 8719 # Sentinel 客户端与 Dashboard 通信的端口eager: true # 饥饿加载#datasource: #可以配置持久化, 生产环境强烈推荐# ds1:# nacos:# server-addr: localhost:8848# dataId: seckill-sentinel-rules# groupId: DEFAULT_GROUP# rule-type: flow
- 定义 Sentinel 资源和规则 (SeckillService.java):
package com.example.seckill.service;import com.alibaba.csp.sentinel.Entry;
import com.alibaba.csp.sentinel.SphU;
import com.alibaba.csp.sentinel.annotation.SentinelResource;
import com.alibaba.csp.sentinel.slots.block.BlockException;
import com.alibaba.csp.sentinel.slots.block.RuleConstant;
import com.alibaba.csp.sentinel.slots.block.flow.FlowRule;
import com.alibaba.csp.sentinel.slots.block.flow.FlowRuleManager;
import com.example.seckill.dao.OrderDao;
import com.example.seckill.dao.ProductDao;
import com.example.seckill.dto.OrderDto;
import com.example.seckill.dto.ProductInfo;
import com.example.seckill.dto.SeckillRequest;
import com.example.seckill.enums.OrderStatus;
import com.example.seckill.exception.OutOfStockException;
import com.example.seckill.exception.SeckillException;
import com.example.seckill.util.RedisKeyUtil;import org.redisson.Redisson;
import org.redisson.api.RLock;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;import javax.annotation.PostConstruct;
import java.math.BigDecimal;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.TimeUnit;@Service
public class SeckillService {@Autowiredprivate StockService stockService;@Autowiredprivate OrderService orderService;@Autowiredprivate ProductDao productDao;@Autowiredprivate OrderDao orderDao;@Autowiredprivate StringRedisTemplate stringRedisTemplate;@Autowiredprivate Redisson redisson; // 注入 Redissonprivate static final String SECKILL_RESOURCE_NAME = "seckill"; // Sentinel 资源名称@Value("${seckill.overSellRatio:0.1}") // 允许的超卖比例, 从配置文件读取private double overSellRatio;// 初始化 Sentinel 规则 (可以在应用启动时加载,也可以通过 Sentinel Dashboard 动态配置)@PostConstructprivate void initSentinelRules() {List<FlowRule> rules = new ArrayList<>();FlowRule rule = new FlowRule();rule.setResource(SECKILL_RESOURCE_NAME);rule.setGrade(RuleConstant.FLOW_GRADE_QPS); // 限流阈值类型 (QPS)rule.setCount(100); // QPS 阈值 (根据实际情况调整)//rule.setLimitApp("default"); // 来源应用 (可选)rule.setControlBehavior(RuleConstant.CONTROL_BEHAVIOR_DEFAULT); //流控效果, 默认快速失败rules.add(rule);FlowRuleManager.loadRules(rules);//也可以在这里初始化商品信息, 避免每次启动都手动初始化// initProductInfoToRedis();}// 秒杀核心逻辑 (使用 Sentinel 注解)@SentinelResource(value = SECKILL_RESOURCE_NAME, blockHandler = "handleSeckillBlock")public String doSeckill(SeckillRequest request) {//使用lua脚本return doSeckillByLua(request);}// Sentinel 限流/熔断处理方法public String handleSeckillBlock(SeckillRequest request, BlockException ex) {// 可以在这里记录日志、返回更友好的提示等return "系统繁忙,请稍后再试!";}//使用lua 脚本public String doSeckillByLua(SeckillRequest request){Long userId = request.getUserId();Long productId = request.getProductId();Integer quantity = request.getQuantity();String stockKey = RedisKeyUtil.getStockKey(productId); // 实际库存 keyString preDeductedKey = RedisKeyUtil.getPreDeductedStockKey(productId); // 预扣库存 keyString orderIdSetKey = RedisKeyUtil.getOrderIdSetKey(userId, productId); //幂等性key// Lua 脚本String script =// 1. 检查商品是否存在 (实际库存)"local stock = tonumber(redis.call('get', KEYS[1])) " +"if (stock == nil) then " +" return '-1' " + // 商品不存在"end " +// 2. 检查是否重复下单 (幂等性)"local orderExists = redis.call('exists', KEYS[3]) " +"if (orderExists == 1) then " +" return '-2' " + // 重复下单"end " +// 3. 检查预扣库存是否足够 , 允许超卖"local preDeducted = tonumber(redis.call('get', KEYS[2])) or 0 " + //预扣库存, 没有的话默认为0"local allowedPreDeduct = stock + stock * tonumber(ARGV[2]) " + // 允许预扣的库存"if (preDeducted + tonumber(ARGV[1]) > allowedPreDeduct) then " +" return '0' " + // 库存不足"end " +// 4. 增加预扣库存"redis.call('incrby', KEYS[2], ARGV[1]) " +// 5. 设置幂等性标记"redis.call('setex', KEYS[3], 60, '1') " + // 假设订单ID有效期60秒"return '1'"; // 预扣成功// 执行 Lua 脚本Long result = stringRedisTemplate.execute(org.springframework.data.redis.core.script.RedisScript.of(script, Long.class),java.util.List.of(stockKey, preDeductedKey, orderIdSetKey),String.valueOf(quantity),String.valueOf(overSellRatio) //传入允许的超卖比例);if(result == null){throw new SeckillException("秒杀失败, 未知错误");}if (result == -1) {return "商品不存在";} else if (result == -2) {return "您已经购买过该商品,请勿重复下单!";} else if (result == 0) {return "商品已售罄";}// Lua 脚本执行成功, 预扣库存成功,异步创建订单// 构建订单信息OrderDto orderDto = new OrderDto();orderDto.setUserId(userId);orderDto.setProductId(productId);orderDto.setQuantity(quantity);orderDto.setStatus(OrderStatus.CREATED.name()); // 初始状态// ... 其他订单信息 ...orderService.createOrderAsync(orderDto); // 异步创建订单return "秒杀请求已提交,请稍后查看订单!";}// 初始化商品信息到 Redis (可以在系统启动时调用,或者通过后台管理界面触发)@Transactional(readOnly = true)public void initProductInfoToRedis() {ProductInfo product = productDao.findById(1L); // 假设商品ID为1if (product != null) {stockService.cacheProductInfo(product);stockService.setActualStockInRedis(product.getId(), product.getStock());// 初始化预扣库存stringRedisTemplate.opsForValue().set(RedisKeyUtil.getPreDeductedStockKey(product.getId()), "0");}}
}
2. 订单超时处理 (OrderService.java):
// ... 其他代码 ...//Redis key 过期事件@org.springframework.context.event.EventListenerpublic void handleRedisKeyExpiredEvent(org.springframework.data.redis.core.event.KeyExpiredEvent event) {String key = new String(event.getSource());// 检查是否是订单超时 Keyif (key.startsWith(RedisKeyUtil.getOrderIdSetKeyPrefix())) {// 解析出 userId 和 productIdString[] parts = key.substring(RedisKeyUtil.getOrderIdSetKeyPrefix().length()).split(":");if (parts.length == 2) {Long userId = Long.parseLong(parts[0]);Long productId = Long.parseLong(parts[1]);handleTimeoutOrder(userId,productId);}}}//处理超时订单// @Transactional //避免和 createOrderAsync 产生嵌套事务public void handleTimeoutOrder(Long userId, Long productId){// 查询订单OrderDto order = orderDao.findByUserIdAndProductId(userId, productId);if (order != null && OrderStatus.CREATED.name().equals(order.getStatus())) {// 订单超时未支付,取消订单,回滚库存//更新订单状态为 CANCELEDorderDao.updateOrderStatus(order.getId(), OrderStatus.CANCELED.name());// 还原预扣库存 , 实际应该查询订单数量stockService.revertPreDeductedStock(productId, order.getQuantity());// 记录日志System.out.println("订单超时,已取消订单:" + order.getId());}}// ... 其他代码 ...
开启 Redis Key 过期事件:
-
修改 redis.conf:
notify-keyspace-events Ex
-
或者
redis-cli config set notify-keyspace-events Ex
3. Swagger/OpenAPI 集成 (可选):
- pom.xml 添加依赖:
<dependency><groupId>io.springfox</groupId><artifactId>springfox-boot-starter</artifactId><version>3.0.0</version> <!-- 使用适合你的 Spring Boot 版本的版本 -->
</dependency>
- 添加配置类 (SwaggerConfig.java):
package com.example.seckill.config;import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import springfox.documentation.builders.PathSelectors;
import springfox.documentation.builders.RequestHandlerSelectors;
import springfox.documentation.spi.DocumentationType;
import springfox.documentation.spring.web.plugins.Docket;@Configuration
public class SwaggerConfig {@Beanpublic Docket api() {return new Docket(DocumentationType.SWAGGER_2).select().apis(RequestHandlerSelectors.basePackage("com.example.seckill.controller")) // 指定 Controller 所在的包.paths(PathSelectors.any()).build();}
}
- Controller 中添加注解:
// SeckillController.java
package com.example.seckill.controller;import com.example.seckill.dto.Result;
import com.example.seckill.dto.SeckillRequest;
import com.example.seckill.service.SeckillService;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;import javax.servlet.http.HttpSession;@RestController
@RequestMapping("/seckill")
@Api(tags = "秒杀接口") // Swagger 标签
public class SeckillController {@Autowiredprivate SeckillService seckillService;@PostMapping("/doSeckill")@ApiOperation("执行秒杀") // Swagger 操作描述public Result<String> doSeckill(@Validated @RequestBody SeckillRequest request, HttpSession session) {Long userId = 123L;if (userId == null) {return Result.error("用户未登录");}request.setUserId(userId);String message = seckillService.doSeckill(request);return Result.success(message);}
}
4. Resultful 风格的 Result 类:
package com.example.seckill.dto;
import java.io.Serializable;// Result.java (dto 目录下)
public class Result<T> implements Serializable {private int code;private String message;private T data;public Result(int code, String message, T data) {this.code = code;this.message = message;this.data = data;}public static <T> Result<T> success(T data){return new Result<>(200, "success", data);}public static Result<String> success(String message){return new Result<>(200, message, null);}public static Result<String> error(String message){return new Result<>(500, message, null);}public static <T> Result<T> error(int code, String message){return new Result<>(code, message, null);}//getter and setter ...
}
5. 单元测试示例 (使用 JUnit 5 和 Mockito):
// SeckillServiceTest.java (src/test/java 下)
package com.example.seckill.service;import com.example.seckill.dao.OrderDao;
import com.example.seckill.dao.ProductDao;
import com.example.seckill.dto.SeckillRequest;
import com.example.seckill.util.RedisKeyUtil;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.ValueOperations;import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;@ExtendWith(MockitoExtension.class)
public class SeckillServiceTest {@Mockprivate StockService stockService;@Mockprivate OrderService orderService;@Mockprivate ProductDao productDao;@Mockprivate OrderDao orderDao;@Mockprivate StringRedisTemplate stringRedisTemplate; // Mock RedisTemplate@InjectMocks // 自动注入 Mock 对象private SeckillService seckillService;@Testpublic void testDoSeckill_Success() {// 模拟请求参数SeckillRequest request = new SeckillRequest();request.setUserId(1L);request.setProductId(1L);request.setQuantity(1);// 模拟 RedisTemplate 的行为ValueOperations<String, String> valueOps = mock(ValueOperations.class);when(stringRedisTemplate.opsForValue()).thenReturn(valueOps);//模拟lua脚本的返回值when(stringRedisTemplate.execute(any(org.springframework.data.redis.core.script.RedisScript.class),any(java.util.List.class), any())).thenReturn(1L);// 调用 doSeckill 方法String result = seckillService.doSeckill(request);// 断言结果assertEquals("秒杀请求已提交,请稍后查看订单!", result);// 可以添加更多断言,验证 mock 对象的方法是否被调用,以及调用的参数是否正确}// 可以添加更多测试用例,覆盖不同的场景,例如:// - 用户未登录// - 商品不存在// - 库存不足// - 重复下单// - 限流
}
改动和新增内容:
- Sentinel:
- 添加了 Sentinel 的依赖和配置。
- 在
SeckillService
中使用@SentinelResource
注解定义了 Sentinel 资源。 - 添加了
initSentinelRules
方法来初始化 Sentinel 流控规则。 - 添加了
handleSeckillBlock
方法来处理 Sentinel 限流/熔断。
- Lua 脚本:
- 将预扣库存、幂等性校验等逻辑整合到了 Lua 脚本中。
- Lua脚本中增加了对允许的超卖比例的处理
- 订单超时处理:
- 使用了 Redis key 过期事件来处理超时订单. (也可以使用定时任务)
- 分布式锁:
- 示例代码中包含了 Redisson 的注入, 实际使用时可以按需使用. 本示例中主要使用了Lua 脚本, Redisson 不是必须的.
- Swagger:
- 添加了 Swagger 的依赖和配置。
- 在
SeckillController
中添加了 Swagger 注解。
- Resultful:
- 定义了 Result 类, 用于封装接口的返回结果.
- 单元测试: 提供了一个简单的单元测试示例,使用了 JUnit 5 和 Mockito。
总结:
经过这几次的迭代,我们的秒杀系统已经具备了以下特性:
- 高并发处理能力: 使用 Redis 预扣库存、异步下单、数据库乐观锁等机制。
- 限流和熔断: 使用 Sentinel 进行限流和熔断,保护系统免受流量洪峰的冲击。
- 幂等性: 防止重复下单。
- 订单超时处理: 自动取消超时未支付的订单,释放预扣库存。
- 分布式锁: (可选) 使用 Redisson 保证分布式环境下的数据一致性。
- 良好的代码结构: 模块化设计,易于维护和扩展。
- Resultful 接口
- 可测试性: 提供了单元测试示例。
后续可能的改进方向:
- 更完善的监控和告警: 集成 Prometheus、Grafana 等工具,对系统进行更全面的监控,并设置告警规则。
- 更精细的灰度发布: 使用 A/B 测试等方式,逐步将新功能发布给部分用户。
- 数据库优化: 使用分库分表、读写分离等技术,进一步提升数据库性能。
- 引入分布式事务: 如果库存和订单不在同一个数据库,需要考虑分布式事务(例如 Seata)。
- 安全性增强: 添加防刷、防恶意攻击等安全措施。
- 全链路压测: 模拟真实的用户请求,对系统进行全链路压测,找出性能瓶颈并进行优化。
希望这个最终版本对你有所帮助! 这是一个相对完整的秒杀系统示例,但实际的生产环境系统会更复杂,需要根据具体业务需求进行调整。