clean-java-project-structure-意在clean&standard
断WAN手撕了一个平平无奇的秒杀系统,crud过载,赶紧多看看源码缓缓
秒杀系统实现 - 前言
在互联网高速发展的时代,电商平台的各种促销活动层出不穷,其中“秒杀”活动以其低价、限时、限量的特点吸引了大量用户,成为电商平台吸引流量、提升销量的有效手段。然而,秒杀系统由于其高并发、瞬间流量巨大的特点,对系统架构和技术选型提出了极高的要求。实现一个稳定、高效、公平的秒杀系统,并非易事。
秒杀系统的挑战:
- 高并发: 秒杀活动开启瞬间,会涌入海量请求,系统需要具备处理高并发请求的能力。
- 资源竞争: 多个用户同时抢购有限的商品,如何保证数据的一致性和避免超卖是关键问题。
- 性能瓶颈: 数据库、缓存、网络带宽等都可能成为系统的性能瓶颈,需要进行合理的优化和设计。
- 安全性: 秒杀系统容易成为攻击目标,需要防范恶意请求和刷单行为。
- 公平性: 如何保证所有用户都有公平的参与机会,防止机器人或恶意脚本抢占资源,也是一个重要挑战。
构建秒杀系统的关键点:
- 架构设计: 选择合适的架构模式,例如微服务架构、分布式架构,以应对高并发和海量数据。
- 缓存策略: 利用缓存技术减轻数据库压力,提高系统响应速度。
- 异步处理: 将一些耗时操作异步处理,例如订单生成、库存扣减等。
- 限流降级: 通过限流和降级策略,保护系统在高并发情况下不崩溃。
- 安全防护: 采用多种安全措施,例如验证码、风控系统等,防止恶意攻击和刷单行为。
- 公平性保障: 设计合理的抢购策略和算法,确保用户公平参与秒杀活动。
本系列文章将深入探讨秒杀系统的实现,涵盖以下内容:
- 需求分析与系统设计: 明确秒杀系统的功能需求、性能指标和技术选型。
- 高并发架构设计: 介绍如何利用负载均衡、分布式缓存、消息队列等技术构建高并发架构。
- 缓存策略与数据一致性: 探讨如何选择合适的缓存方案,并保证数据一致性。
- 异步处理与性能优化: 介绍如何利用异步处理技术提高系统性能和响应速度。
- 限流降级与熔断机制: 讨论如何设计限流、降级和熔断策略,保证系统稳定性。
- 安全防护与反作弊: 介绍如何利用验证码、风控系统等技术防范恶意攻击和刷单行为。
- 性能测试与调优: 通过性能测试发现系统瓶颈,并进行针对性的优化。
目标:
通过本系列文章,希望能够帮助读者理解秒杀系统的设计原理和实现方法,掌握构建高性能、高可用、安全可靠的秒杀系统的关键技术,并能够将其应用到实际项目中。
项目开发过程
1. 使用 Maven Archetype 创建新项目
-
打开终端/命令提示符:
输入mvn -v
来验证安装加粗样式。 -
运行 Maven 命令:
使用 Maven 的 Archetype 插件来生成一个新的项目结构。 Maven 提供了多种 Archetype,可以通过mvn archetype:generate
命令查看和选择不同的模板。运行以下命令:mvn archetype:generate -DgroupId=com.example -DartifactId=my-app -DarchetypeArtifactId=maven-archetype-quickstart -DinteractiveMode=false
这里的参数解释:
-DgroupId=com.example
:指定项目的组 ID,一般是公司域名的倒序。-DartifactId=my-app
:指定项目的唯一标识符。-DarchetypeArtifactId=maven-archetype-quickstart
:使用的原型模板,maven-archetype-quickstart
是一个简单的 Java 项目模板。-DinteractiveMode=false
:非交互模式,避免在执行过程中询问输入。
-
项目结构:
运行上述命令后,Maven 会创建一个目录结构如下:my-app ├── pom.xml └── src ├── main │ └── java │ └── com │ └── example │ └── App.java └── test └── java └── com └── example └── AppTest.java
-
项目配置文件 (
pom.xml
):
pom.xml
是 Maven 项目的核心配置文件,包含了项目的基本信息、依赖管理、构建信息等。 -
构建和运行项目:
- 构建项目: 在项目根目录下运行
mvn package
,Maven 会编译代码并打包成一个 JAR 文件。 - 运行项目: 打包完成后,可以运行生成的 JAR 文件,或者直接使用
mvn springboot:run
来运行(需要在pom.xml
中配置springboot-maven-plugin
插件)。
- 构建项目: 在项目根目录下运行
2.逐步写出项目结构和依赖配置:
- Maven依赖 (pom.xml)
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0">
<modelVersion>4.0.0</modelVersion>
<groupId>com.example</groupId>
<artifactId>seckill-system</artifactId>
<version>1.0.0</version>
<packaging>jar</packaging>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.7.0</version>
</parent>
<properties>
<java.version>11</java.version>
<kafka.version>3.3.1</kafka.version>
<redisson.version>3.17.0</redisson.version>
<guava.version>31.1-jre</guava.version>
</properties>
<dependencies>
<!-- Spring Boot -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<!-- Kafka -->
<dependency>
<groupId>org.springframework.kafka</groupId>
<artifactId>spring-kafka</artifactId>
</dependency>
<!-- Database -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<!-- Redis -->
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson</artifactId>
<version>${redisson.version}</version>
</dependency>
<!-- Utils -->
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>${guava.version}</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
</dependency>
<!-- Test -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.kafka</groupId>
<artifactId>spring-kafka-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
- 项目结构
seckill-system/
├── src/
│ ├── main/
│ │ ├── java/
│ │ │ └── com/example/seckill/
│ │ │ ├── SeckillApplication.java
│ │ │ ├── config/
│ │ │ │ ├── KafkaConfig.java
│ │ │ │ ├── RedisConfig.java
│ │ │ │ ├── RateLimitConfig.java
│ │ │ │ └── WebConfig.java
│ │ │ ├── controller/
│ │ │ │ └── SeckillController.java
│ │ │ ├── service/
│ │ │ │ ├── SeckillService.java
│ │ │ │ ├── OrderService.java
│ │ │ │ └── StockService.java
│ │ │ ├── consumer/
│ │ │ │ └── SeckillConsumer.java
│ │ │ ├── domain/
│ │ │ │ ├── Order.java
│ │ │ │ ├── Product.java
│ │ │ │ └── SeckillMessage.java
│ │ │ ├── repository/
│ │ │ │ ├── OrderRepository.java
│ │ │ │ └── ProductRepository.java
│ │ │ ├── exception/
│ │ │ │ ├── SeckillException.java
│ │ │ │ └── GlobalExceptionHandler.java
│ │ │ ├── monitor/
│ │ │ │ ├── SeckillMonitor.java
│ │ │ │ └── AlertService.java
│ │ │ └── util/
│ │ │ ├── JsonUtil.java
│ │ │ └── RedisUtil.java
│ │ └── resources/
│ │ ├── application.yml
│ │ ├── application-dev.yml
│ │ └── application-prod.yml
│ └── test/
│ └── java/
│ └── com/example/seckill/
│ ├── service/
│ │ └── SeckillServiceTest.java
│ └── controller/
│ └── SeckillControllerTest.java
├── pom.xml
└── README.md
- 配置文件 (application.yml)
spring:
application:
name: seckill-system
datasource:
url: jdbc:mysql://localhost:3306/seckill?useSSL=false
username: root
password: root
driver-class-name: com.mysql.cj.jdbc.Driver
jpa:
hibernate:
ddl-auto: update
show-sql: true
redis:
host: localhost
port: 6379
kafka:
bootstrap-servers: localhost:9092
producer:
key-serializer: org.apache.kafka.common.serialization.StringSerializer
value-serializer: org.apache.kafka.common.serialization.StringSerializer
batch-size: 16384
buffer-memory: 33554432
consumer:
group-id: seckill-group
auto-offset-reset: earliest
key-deserializer: org.apache.kafka.common.serialization.StringDeserializer
value-deserializer: org.apache.kafka.common.serialization.StringDeserializer
enable-auto-commit: false
server:
port: 8080
management:
endpoints:
web:
exposure:
include: "*"
metrics:
tags:
application: ${spring.application.name}
seckill:
rateLimit:
enabled: true
permits-per-second: 1000
order:
timeout-minutes: 30
3.开始开发主要代码
不是全部代码,只是举例说明
这个秒杀系统实现了:
-
高并发支持(Kafka削峰)
-
库存一致性保证
-
防重复提交
-
限流保护
-
系统架构设计
@Configuration
public class SeckillConfig {
// 秒杀相关的Topic配置
public static final String SECKILL_TOPIC = "seckill-orders";
public static final String DEAD_LETTER_TOPIC = "seckill-dead-letter";
public static final String GROUP_ID = "seckill-group";
@Bean
public NewTopic seckillTopic() {
// 创建秒杀订单Topic,设置分区数和副本数
return TopicBuilder.name(SECKILL_TOPIC)
.partitions(3)
.replicas(2)
.build();
}
}
- 库存预热和检查
@Service
@Slf4j
public class StockService {
@Autowired
private RedisTemplate<String, String> redisTemplate;
private static final String STOCK_KEY = "seckill:stock:";
private static final String LOCK_KEY = "seckill:lock:";
// 预热库存到Redis
public void preloadStock(Long productId, Integer stock) {
String key = STOCK_KEY + productId;
redisTemplate.opsForValue().set(key, String.valueOf(stock));
}
// 检查并扣减库存
public boolean checkAndDeductStock(Long productId) {
String key = STOCK_KEY + productId;
String lockKey = LOCK_KEY + productId;
// 分布式锁
try {
Boolean locked = redisTemplate.opsForValue()
.setIfAbsent(lockKey, "1", 3, TimeUnit.SECONDS);
if (Boolean.TRUE.equals(locked)) {
// 检查库存
Long stock = redisTemplate.opsForValue()
.decrement(key);
if (stock != null && stock >= 0) {
return true;
} else {
// 库存不足,恢复库存
redisTemplate.opsForValue().increment(key);
return false;
}
}
} finally {
redisTemplate.delete(lockKey);
}
return false;
}
}
- 秒杀请求处理
@RestController
@RequestMapping("/seckill")
@Slf4j
public class SeckillController {
@Autowired
private KafkaTemplate<String, String> kafkaTemplate;
@Autowired
private StockService stockService;
@PostMapping("/order")
public ResponseEntity<String> seckill(
@RequestParam Long productId,
@RequestParam Long userId) {
// 1. 防重复请求
String requestId = String.format("%d_%d", userId, productId);
if (!checkRepeatedRequest(requestId)) {
return ResponseEntity.badRequest().body("重复请求");
}
// 2. 检查库存
if (!stockService.checkAndDeductStock(productId)) {
return ResponseEntity.badRequest().body("库存不足");
}
// 3. 发送消息到Kafka
SeckillMessage message = new SeckillMessage(userId, productId);
kafkaTemplate.send(SeckillConfig.SECKILL_TOPIC,
JsonUtil.toJson(message))
.addCallback(
result -> log.info("消息发送成功"),
ex -> log.error("消息发送失败", ex)
);
return ResponseEntity.ok("订单处理中");
}
private boolean checkRepeatedRequest(String requestId) {
return redisTemplate.opsForValue()
.setIfAbsent(requestId, "1", 1, TimeUnit.MINUTES);
}
}
- 消息消费处理
@Service
@Slf4j
public class SeckillConsumer {
@Autowired
private OrderService orderService;
@Autowired
private StockService stockService;
@KafkaListener(
topics = SeckillConfig.SECKILL_TOPIC,
groupId = SeckillConfig.GROUP_ID,
containerFactory = "seckillKafkaListenerContainerFactory"
)
public void handleSeckillMessage(
@Payload String message,
@Header(KafkaHeaders.RECEIVED_PARTITION_ID) int partition,
@Header(KafkaHeaders.OFFSET) long offset) {
try {
SeckillMessage seckillMessage = JsonUtil.fromJson(
message, SeckillMessage.class);
// 创建订单
createOrder(seckillMessage);
} catch (Exception e) {
log.error("处理秒杀消息失败", e);
// 发送到死信队列
kafkaTemplate.send(SeckillConfig.DEAD_LETTER_TOPIC, message);
}
}
@Transactional
public void createOrder(SeckillMessage message) {
// 1. 检查订单是否已存在
if (orderService.checkOrderExists(message.getUserId(),
message.getProductId())) {
return;
}
// 2. 创建订单
Order order = new Order();
order.setUserId(message.getUserId());
order.setProductId(message.getProductId());
order.setStatus(OrderStatus.CREATED);
orderService.createOrder(order);
// 3. 发送订单创建成功的消息
kafkaTemplate.send("order-created", JsonUtil.toJson(order));
}
}
- 消费者配置优化
@Configuration
public class KafkaConsumerConfig {
@Bean
public ConsumerFactory<String, String> consumerFactory() {
Map<String, Object> props = new HashMap<>();
props.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, "localhost:9092");
props.put(ConsumerConfig.GROUP_ID_CONFIG, SeckillConfig.GROUP_ID);
props.put(ConsumerConfig.MAX_POLL_RECORDS_CONFIG, "50");
props.put(ConsumerConfig.MAX_POLL_INTERVAL_MS_CONFIG, "5000");
props.put(ConsumerConfig.ENABLE_AUTO_COMMIT_CONFIG, false);
return new DefaultKafkaConsumerFactory<>(props);
}
@Bean
public ConcurrentKafkaListenerContainerFactory<String, String>
seckillKafkaListenerContainerFactory() {
ConcurrentKafkaListenerContainerFactory<String, String> factory =
new ConcurrentKafkaListenerContainerFactory<>();
factory.setConsumerFactory(consumerFactory());
// 设置并发消费者数量
factory.setConcurrency(3);
// 批量消费
factory.setBatchListener(true);
// 手动提交
factory.getContainerProperties().setAckMode(AckMode.MANUAL);
return factory;
}
}
- 限流配置
@Configuration
public class RateLimitConfig {
@Bean
public RateLimiter rateLimiter() {
// 每秒允许1000个请求
return RateLimiter.create(1000);
}
@Bean
public RedisRateLimiter redisRateLimiter() {
return new RedisRateLimiter(redisTemplate);
}
}
- 限流配置
@Aspect
@Component
public class SeckillRateLimiter {
@Autowired
private RateLimiter rateLimiter;
@Autowired
private RedisTemplate<String, String> redisTemplate;
private static final String RATE_LIMIT_KEY = "seckill:rate:";
@Around("@annotation(SeckillLimit)")
public Object limitRate(ProceedingJoinPoint point) throws Throwable {
// 1. JVM级别限流
if (!rateLimiter.tryAcquire(100, TimeUnit.MILLISECONDS)) {
throw new RuntimeException("系统繁忙");
}
// 2. 分布式限流
String key = RATE_LIMIT_KEY + getRequestKey(point);
String count = redisTemplate.opsForValue().get(key);
if (count != null && Integer.parseInt(count) >= 1000) {
throw new RuntimeException("访问频率过高");
}
redisTemplate.opsForValue().increment(key);
redisTemplate.expire(key, 1, TimeUnit.SECONDS);
return point.proceed();
}
}
- 订单处理服务
@Service
@Slf4j
public class OrderService {
@Autowired
private OrderRepository orderRepository;
@Autowired
private KafkaTemplate<String, String> kafkaTemplate;
@Transactional
public void createOrder(Order order) {
try {
// 1. 保存订单
orderRepository.save(order);
// 2. 发送订单创建事件
OrderEvent event = new OrderEvent(
order.getId(),
OrderEventType.CREATED
);
kafkaTemplate.send("order-events", JsonUtil.toJson(event));
// 3. 启动订单超时检查
scheduleOrderTimeout(order);
} catch (Exception e) {
log.error("创建订单失败", e);
throw new OrderException("创建订单失败", e);
}
}
private void scheduleOrderTimeout(Order order) {
String key = "order:timeout:" + order.getId();
redisTemplate.opsForValue().set(key, "1", 30, TimeUnit.MINUTES);
}
@Scheduled(fixedRate = 60000)
public void checkTimeoutOrders() {
Set<String> timeoutKeys = redisTemplate.keys("order:timeout:*");
for (String key : timeoutKeys) {
String orderId = key.substring(key.lastIndexOf(":") + 1);
cancelOrder(Long.parseLong(orderId));
}
}
}
- 库存一致性处理
@Service
public class StockConsistencyService {
@Autowired
private StockRepository stockRepository;
@Autowired
private RedisTemplate<String, String> redisTemplate;
@Scheduled(fixedRate = 300000) // 每5分钟执行一次
public void checkStockConsistency() {
List<Product> products = stockRepository.findAll();
for (Product product : products) {
String redisStock = redisTemplate.opsForValue()
.get(StockService.STOCK_KEY + product.getId());
if (redisStock != null) {
int dbStock = product.getStock();
int cacheStock = Integer.parseInt(redisStock);
if (dbStock != cacheStock) {
log.warn("库存不一致,商品ID: {}, DB库存: {}, Cache库存: {}",
product.getId(), dbStock, cacheStock);
// 以数据库为准进行修正
redisTemplate.opsForValue()
.set(StockService.STOCK_KEY + product.getId(),
String.valueOf(dbStock));
}
}
}
}
}
- 监控和告警
@Component
@Slf4j
public class SeckillMonitor {
private final Counter seckillCounter =
Counter.build()
.name("seckill_requests_total")
.help("Total seckill requests")
.register();
private final Gauge stockGauge =
Gauge.build()
.name("seckill_stock_remaining")
.help("Remaining stock for seckill")
.register();
@Autowired
private AlertService alertService;
public void recordSeckillRequest() {
seckillCounter.inc();
}
public void updateStockMetrics(Long productId, int stock) {
stockGauge.set(stock);
// 库存告警
if (stock < 10) {
alertService.sendStockAlert(productId, stock);
}
}
@Scheduled(fixedRate = 60000)
public void checkSystemMetrics() {
// 检查系统指标
double successRate = calculateSuccessRate();
if (successRate < 0.95) {
alertService.sendSystemAlert("秒杀成功率过低: " + successRate);
}
}
}
- 性能优化
@Configuration
public class SeckillOptimization {
@Bean
public Cache<String, Boolean> localCache() {
return CacheBuilder.newBuilder()
.maximumSize(10000)
.expireAfterWrite(1, TimeUnit.MINUTES)
.build();
}
@Bean
public ThreadPoolExecutor asyncExecutor() {
return new ThreadPoolExecutor(
10,
20,
60L,
TimeUnit.SECONDS,
new LinkedBlockingQueue<>(1000),
new ThreadPoolExecutor.CallerRunsPolicy()
);
}
@Bean
public RedissonClient redissonClient() {
Config config = new Config();
config.useSingleServer().setAddress("redis://localhost:6379");
return Redisson.create(config);
}
}
- 压测支持
@Component
public class SeckillBenchmark {
@Autowired
private WebClient webClient;
public void runBenchmark(int concurrency, int totalRequests) {
CountDownLatch latch = new CountDownLatch(totalRequests);
AtomicInteger success = new AtomicInteger(0);
for (int i = 0; i < totalRequests; i++) {
int userId = i;
CompletableFuture.runAsync(() -> {
try {
webClient.post()
.uri("/seckill/order")
.body(BodyInserters.fromFormData("userId",
String.valueOf(userId))
.with("productId", "1"))
.retrieve()
.toBodilessEntity()
.block();
success.incrementAndGet();
} catch (Exception e) {
log.error("请求失败", e);
} finally {
latch.countDown();
}
});
}
latch.await();
log.info("压测完成,成功率: {}",
(double)success.get() / totalRequests);
}
}
- 启动类
@SpringBootApplication
@EnableScheduling
@EnableAsync
@EnableCaching
public class SeckillApplication {
public static void main(String[] args) {
SpringApplication.run(SeckillApplication.class, args);
}
}
- 主要功能类实现
domain/Product.java:
@Entity
@Table(name = "products")
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class Product {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
private BigDecimal price;
private Integer stock;
private LocalDateTime startTime;
private LocalDateTime endTime;
@Version
private Integer version;
}
domain/Order.java:
@Entity
@Table(name = "orders")
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class Order {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private Long userId;
private Long productId;
private BigDecimal amount;
@Enumerated(EnumType.STRING)
private OrderStatus status;
private LocalDateTime createTime;
private LocalDateTime payTime;
}
domain/SeckillMessage.java:
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class SeckillMessage {
private Long userId;
private Long productId;
private LocalDateTime timestamp;
}
repository/ProductRepository.java:
@Repository
public interface ProductRepository extends JpaRepository<Product, Long> {
@Lock(LockModeType.PESSIMISTIC_WRITE)
@Query("select p from Product p where p.id = :id")
Optional<Product> findByIdWithLock(@Param("id") Long id);
@Modifying
@Query("update Product p set p.stock = p.stock - 1 where p.id = :id and p.stock > 0")
int decrementStock(@Param("id") Long id);
}
service/SeckillService.java:
@Service
@Slf4j
public class SeckillService {
@Autowired
private ProductRepository productRepository;
@Autowired
private RedisTemplate<String, String> redisTemplate;
@Autowired
private KafkaTemplate<String, String> kafkaTemplate;
@Value("${seckill.order.timeout-minutes}")
private int orderTimeoutMinutes;
public boolean trySecKill(Long userId, Long productId) {
// 1. 检查用户是否已经购买
String purchaseKey = String.format("seckill:purchase:%d:%d", userId, productId);
if (Boolean.TRUE.equals(redisTemplate.hasKey(purchaseKey))) {
throw new SeckillException("您已经参与过此次秒杀");
}
// 2. 检查商品是否在秒杀时间内
Product product = productRepository.findById(productId)
.orElseThrow(() -> new SeckillException("商品不存在"));
if (!isInSeckillTime(product)) {
throw new SeckillException("不在秒杀时间内");
}
// 3. 扣减Redis库存
String stockKey = "seckill:stock:" + productId;
Long remainStock = redisTemplate.opsForValue().decrement(stockKey);
if (remainStock != null && remainStock >= 0) {
// 4. 发送创建订单消息
SeckillMessage message = new SeckillMessage(userId, productId, LocalDateTime.now());
kafkaTemplate.send("seckill-orders", JsonUtil.toJson(message));
// 5. 标记用户已购买
redisTemplate.opsForValue().set(purchaseKey, "1", orderTimeoutMinutes, TimeUnit.MINUTES);
return true;
} else {
// 库存不足,恢复库存
redisTemplate.opsForValue().increment(stockKey);
throw new SeckillException("商品已售罄");
}
}
}
util/RedisUtil.java:
@Component
public class RedisUtil {
@Autowired
private StringRedisTemplate redisTemplate;
public boolean tryLock(String key, long timeout, TimeUnit unit) {
return Boolean.TRUE.equals(redisTemplate.opsForValue()
.setIfAbsent(key, "1", timeout, unit));
}
public void unlock(String key) {
redisTemplate.delete(key);
}
public boolean tryAcquireToken(String key, int limit) {
String countStr = redisTemplate.opsForValue().get(key);
int count = countStr == null ? 0 : Integer.parseInt(countStr);
if (count >= limit) {
return false;
}
redisTemplate.opsForValue().increment(key);
redisTemplate.expire(key, 1, TimeUnit.SECONDS);
return true;
}
}
monitor/MetricsService.java:
@Service
public class MetricsService {
private final MeterRegistry registry;
private final Counter seckillCounter;
private final Counter successCounter;
private final Timer seckillTimer;
public MetricsService(MeterRegistry registry) {
this.registry = registry;
this.seckillCounter = Counter.builder("seckill.requests")
.description("Total seckill requests")
.register(registry);
this.successCounter = Counter.builder("seckill.success")
.description("Successful seckill requests")
.register(registry);
this.seckillTimer = Timer.builder("seckill.latency")
.description("Seckill request latency")
.register(registry);
}
public void recordRequest() {
seckillCounter.increment();
}
public void recordSuccess() {
successCounter.increment();
}
public Timer.Sample startTimer() {
return Timer.start(registry);
}
public void stopTimer(Timer.Sample sample) {
sample.stop(seckillTimer);
}
}
exception/SeckillException.java:
public class SeckillException extends RuntimeException {
public SeckillException(String message) {
super(message);
}
public SeckillException(String message, Throwable cause) {
super(message, cause);
}
}
utils/JsonUtil.java
- 基于 Jackson 的实现(推荐)
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import lombok.extern.slf4j.Slf4j;
@Slf4j
public class JsonUtil {
private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
static {
// 配置ObjectMapper
OBJECT_MAPPER.registerModule(new JavaTimeModule()); // 支持Java8时间类型
OBJECT_MAPPER.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS); // 日期序列化为ISO格式
OBJECT_MAPPER.disable(SerializationFeature.FAIL_ON_EMPTY_BEANS); // 允许序列化空对象
}
private JsonUtil() {
// 私有构造函数,防止实例化
}
/**
* 对象转JSON字符串
*/
public static String toJson(Object obj) {
if (obj == null) {
return null;
}
try {
return OBJECT_MAPPER.writeValueAsString(obj);
} catch (Exception e) {
log.error("Convert object to json error", e);
throw new RuntimeException("Convert object to json error", e);
}
}
/**
* JSON字符串转对象
*/
public static <T> T fromJson(String json, Class<T> clazz) {
if (json == null || json.isEmpty()) {
return null;
}
try {
return OBJECT_MAPPER.readValue(json, clazz);
} catch (Exception e) {
log.error("Parse json to object error", e);
throw new RuntimeException("Parse json to object error", e);
}
}
/**
* JSON字符串转复杂对象(如List<T>)
*/
public static <T> T fromJson(String json, TypeReference<T> typeReference) {
if (json == null || json.isEmpty()) {
return null;
}
try {
return OBJECT_MAPPER.readValue(json, typeReference);
} catch (Exception e) {
log.error("Parse json to object error", e);
throw new RuntimeException("Parse json to object error", e);
}
}
/**
* 对象转byte数组
*/
public static byte[] toBytes(Object obj) {
if (obj == null) {
return null;
}
try {
return OBJECT_MAPPER.writeValueAsBytes(obj);
} catch (Exception e) {
log.error("Convert object to bytes error", e);
throw new RuntimeException("Convert object to bytes error", e);
}
}
/**
* byte数组转对象
*/
public static <T> T fromBytes(byte[] bytes, Class<T> clazz) {
if (bytes == null || bytes.length == 0) {
return null;
}
try {
return OBJECT_MAPPER.readValue(bytes, clazz);
} catch (Exception e) {
log.error("Parse bytes to object error", e);
throw new RuntimeException("Parse bytes to object error", e);
}
}
/**
* 对象转换
*/
public static <T> T convert(Object obj, Class<T> clazz) {
if (obj == null) {
return null;
}
try {
return OBJECT_MAPPER.convertValue(obj, clazz);
} catch (Exception e) {
log.error("Convert object error", e);
throw new RuntimeException("Convert object error", e);
}
}
/**
* 格式化JSON字符串
*/
public static String formatJson(String json) {
if (json == null || json.isEmpty()) {
return json;
}
try {
Object obj = OBJECT_MAPPER.readValue(json, Object.class);
return OBJECT_MAPPER.writerWithDefaultPrettyPrinter().writeValueAsString(obj);
} catch (Exception e) {
log.error("Format json error", e);
throw new RuntimeException("Format json error", e);
}
}
/**
* 判断字符串是否是合法的JSON
*/
public static boolean isValidJson(String json) {
if (json == null || json.isEmpty()) {
return false;
}
try {
OBJECT_MAPPER.readTree(json);
return true;
} catch (Exception e) {
return false;
}
}
}
- 使用示例
// 基本使用
public class JsonUtilTest {
@Test
public void testJsonUtil() {
// 对象转JSON
User user = new User(1L, "张三", 20);
String json = JsonUtil.toJson(user);
// JSON转对象
User parsedUser = JsonUtil.fromJson(json, User.class);
// 转换List
List<User> users = Arrays.asList(
new User(1L, "张三", 20),
new User(2L, "李四", 25)
);
String listJson = JsonUtil.toJson(users);
List<User> parsedUsers = JsonUtil.fromJson(listJson,
new TypeReference<List<User>>() {});
// 对象转换
UserDTO userDTO = JsonUtil.convert(user, UserDTO.class);
// 格式化JSON
String prettyJson = JsonUtil.formatJson(json);
// 验证JSON
boolean isValid = JsonUtil.isValidJson(json);
}
}
// 配合Spring使用
@Configuration
public class JacksonConfig {
@Bean
public ObjectMapper objectMapper() {
ObjectMapper objectMapper = new ObjectMapper();
objectMapper.registerModule(new JavaTimeModule());
objectMapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
return objectMapper;
}
}
- 扩展功能(根据需求添加)
public class JsonUtil {
// ... 前面的代码 ...
/**
* 合并两个JSON对象
*/
public static String mergeJson(String json1, String json2) {
try {
JsonNode node1 = OBJECT_MAPPER.readTree(json1);
JsonNode node2 = OBJECT_MAPPER.readTree(json2);
if (node1.isObject() && node2.isObject()) {
ObjectNode result = (ObjectNode) node1;
result.setAll((ObjectNode) node2);
return OBJECT_MAPPER.writeValueAsString(result);
}
throw new IllegalArgumentException("Both arguments must be JSON objects");
} catch (Exception e) {
log.error("Merge json error", e);
throw new RuntimeException("Merge json error", e);
}
}
/**
* 从JSON中获取指定路径的值
*/
public static JsonNode getPath(String json, String path) {
try {
JsonNode root = OBJECT_MAPPER.readTree(json);
return root.at(path);
} catch (Exception e) {
log.error("Get json path error", e);
throw new RuntimeException("Get json path error", e);
}
}
}
在中引入和配置 SpringDoc OpenAPI:
<!-- 或者使用 SpringDoc OpenAPI (推荐) -->
<dependency>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-ui</artifactId>
<version>1.6.15</version>
</dependency>
- apidoc配置类
@Configuration
public class openapiConfig {
@Bean
public OpenAPI springShopOpenAPI() {
return new OpenAPI()
.info(new Info()
.title("秒杀系统 API")
.description("秒杀系统接口文档")
.version("v1.0.0")
.contact(new Contact()
.name("作者名")
.email("[email protected]"))
.license(new License()
.name("Apache 2.0")
.url("http://springdoc.org")))
.externalDocs(new ExternalDocumentation()
.description("项目Wiki文档")
.url("https://wiki.example.com"));
}
// 配置分组
@Bean
public GroupedOpenApi publicApi() {
return GroupedOpenApi.builder()
.group("public")
.pathsToMatch("/api/public/**")
.build();
}
@Bean
public GroupedOpenApi adminApi() {
return GroupedOpenApi.builder()
.group("admin")
.pathsToMatch("/api/admin/**")
.addOpenApiMethodFilter(method -> method.isAnnotationPresent(Admin.class))
.build();
}
}
- Controller 示例
@Tag(name = "秒杀接口", description = "秒杀相关接口")
@RestController
@RequestMapping("/api/seckill")
@Validated
public class SeckillController {
@Operation(summary = "执行秒杀", description = "用户执行秒杀操作")
@ApiResponses(value = {
@ApiResponse(responseCode = "200", description = "秒杀成功"),
@ApiResponse(responseCode = "400", description = "参数错误"),
@ApiResponse(responseCode = "500", description = "系统错误")
})
@PostMapping("/doSeckill")
public ResponseEntity<SeckillResult> doSeckill(
@Parameter(description = "用户ID", required = true)
@RequestParam Long userId,
@Parameter(description = "商品ID", required = true)
@RequestParam Long productId) {
// 处理逻辑
return ResponseEntity.ok(new SeckillResult());
}
@Operation(summary = "查询秒杀结果")
@GetMapping("/result/{orderId}")
public ResponseEntity<OrderResult> queryResult(
@Parameter(description = "订单ID", required = true)
@PathVariable String orderId) {
// 处理逻辑
return ResponseEntity.ok(new OrderResult());
}
}
- Model 示例
@Schema(description = "秒杀请求参数")
@Data
public class SeckillRequest {
@Schema(description = "用户ID", example = "12345")
@NotNull(message = "用户ID不能为空")
private Long userId;
@Schema(description = "商品ID", example = "67890")
@NotNull(message = "商品ID不能为空")
private Long productId;
@Schema(description = "秒杀时间", example = "2023-12-25 10:00:00")
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private LocalDateTime seckillTime;
}
@Schema(description = "秒杀结果")
@Data
public class SeckillResult {
@Schema(description = "订单ID", example = "ORDER_123456")
private String orderId;
@Schema(description = "秒杀状态", example = "SUCCESS")
private SeckillStatus status;
@Schema(description = "错误信息")
private String errorMsg;
}
- 配置文件 (application.yml)
springdoc:
api-docs:
enabled: true
path: /api-docs
swagger-ui:
path: /swagger-ui.html
tags-sorter: alpha
operations-sorter: method
packages-to-scan: com.example.seckill.controller
paths-to-match: /api/**
- 全局异常处理与响应
@ControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(SeckillException.class)
@ResponseStatus(HttpStatus.BAD_REQUEST)
@Operation(summary = "秒杀异常处理")
public ResponseEntity<ErrorResponse> handleSeckillException(SeckillException ex) {
ErrorResponse error = new ErrorResponse(
HttpStatus.BAD_REQUEST.value(),
ex.getMessage()
);
return ResponseEntity.badRequest().body(error);
}
}
@Schema(description = "错误响应")
@Data
@AllArgsConstructor
public class ErrorResponse {
@Schema(description = "错误码", example = "400")
private int code;
@Schema(description = "错误信息")
private String message;
}
- 安全配置(如果使用Spring Security)
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
public void configure(WebSecurity web) {
web.ignoring().antMatchers(
"/swagger-ui/**",
"/swagger-resources/**",
"/api-docs/**"
);
}
}
- 访问方式
# Swagger UI
http://localhost:8080/swagger-ui.html
# OpenAPI JSON
http://localhost:8080/api-docs
- 常用注解说明
@Tag // 标记控制器类
@Operation // 标记接口方法
@Parameter // 描述参数
@Schema // 描述模型
@ApiResponse // 描述响应
@SecurityRequirement // 描述安全要求
- 测试支持
@SpringBootTest
@AutoConfigureMockMvc
class SeckillControllerTest {
@Autowired
private MockMvc mockMvc;
@Test
void shouldGenerateSwaggerDocs() throws Exception {
mockMvc.perform(get("/api-docs"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.info.title").value("秒杀系统 API"))
.andDo(document("swagger"));
}
}
使用 Swagger 的好处:
- 提供交互式API文档
- 支持API测试
- 文档随代码更新
- 支持多种开发语言的客户端生成
建议:
- 在开发环境启用Swagger
- 在生产环境禁用Swagger
- 详细描述API参数和响应
- 使用适当的安全配置