mobile wallpaper 1mobile wallpaper 2mobile wallpaper 3mobile wallpaper 4mobile wallpaper 5mobile wallpaper 6mobile wallpaper 7mobile wallpaper 8mobile wallpaper 9mobile wallpaper 10mobile wallpaper 11mobile wallpaper 12
2023 字
5 分钟
一个简单高效的秒杀方案的实现
2025-07-07

目录

一、业务需求

二、分析

sql表是否需要改动?

如何应对高并发?

如何实现一人一单和防止超卖?

中间件所引发的问题?

三、具体流程图

四、具体代码实现

(一)相关依赖以及yaml文件

(二)配置类

1.Redis配置类

2.RabbitTemplate配置类

3.拦截器和过滤器的配置类

4.雪花算法生成ID配置

(三)常量类

1.MQ相关常量

2.Redis相关常量

(四)工具类

1.缓存请求体工具类

2.线程池工具类

3.雪花算法的生成以及检验相关工具类

(五)实体类

(六)过滤器和拦截器

(七)核心代码

1.controller层

2.service层

3.Lua脚本

4.MQ

5.两个定时任务

五、关于InitSeckillShopsRedisDataTask中显式设置类加载器的作用


一、业务需求#

假设我们有一个电商平台,现计划开展一个秒杀活动。

核心要求是一人一单不能超卖

请你设计并实现该业务。


二、分析#

(一)sql表是否需要改动?#

考虑到电商平台已有用户表和商品表,但秒杀活动有其特殊性:

  • 秒杀商品的库存管理需要高性能和独立性,不宜直接操作海量数据的常规商品表。因此,需要**新增秒杀商品表 (shop_seckill)**来管理秒杀商品的专属信息。
  • 秒杀场景下,为了提升并发处理能力,避免下单时立即操作库存带来的性能瓶颈,应新增秒杀订单表 (order)。秒杀订单处理流程将优先保证快速响应,库存扣减采用异步方式。

最后, 为应对写入压力,对这两张表进行水平分表。采用对10取模的方式,将每张表拆分为10张物理表,以分散单表负载。

(二)如何应对高并发?#

对于高并发的接口,我们频繁对DB进行操作的话不仅对性能会有非常大的影响,甚至可能使其宕机,这是极有可能发生而且不可逆的现象。所以我们的目标是尽量使主线程不对DB进行操作

  1. 在秒杀活动开始前,通过定时任务将秒杀商品的核心数据预热加载到Redis缓存中,确保活动开始时可直接读取缓存数据,避免大量请求直接穿透到数据库。
  2. 然后因为我们不对DB进行操作,所以库存方面我们可以在redis中为每个秒杀商品维护一个库存键,只要库存大于0进行扣减,反之则拒绝请求。
  3. 用户下单成功后,主线程仅做必要校验和redis库存扣减,生成的订单数据不直接写入数据库,而是发送到消息队列进行异步处理。
  4. 在秒杀活动结束后,通过定时任务读取redis中记录的最终库存变化数据。该任务异步更新秒杀商品表中的实际库存。
  5. 在进入核心业务逻辑接口,设置拦截器对请求参数进行格式校验,快速过滤掉明显不合法或恶意请求,减轻后端压力。

(三)如何实现一人一单和防止超卖?#

针对一人一单,我们可以在中为每个秒杀商品维护一个Set结构的键,用于存储已成功下单的用户ID,在每次发起下单请求的时候进行校验即可。

针对超卖问题,由于超卖是因为高并发线程问题所引发的,所以我们应该确保获取库存以及删减库存的原子性操作,所以在这里我们要用到Lua脚本

为了减少频繁的网络请求,所以我们应该将对redis的所有操作都封装到脚本中,将多次请求缩减到一次。

(四)所引发的问题?#

对于MQ,最常见的问题就是消息发送失败以及重复消费,对此我们应该启用RabbitMQ的生产者确认机制,当消息发送失败或者消息未被确认的时候立即重新发送,连续三次失败后我们就应该回滚并返回错误结果。

然后是重复消费问题,我们应该在接收到消息之后进行幂等性检查,也就是在Redis中为每条消费的消息设置一个短暂的标记键,每次收到消息时进行校验,如果已经有数据则直接跳过该消息,反之则进行后续操作。最后在操作完成后删除标记键,如果操作过程中出现异常,则立刻进行数据回滚并返回下单失败的提示。

然后是redis,如果在秒杀过程中redis宕机,很容易想到用DB作为备用方案,但是这反而会导致同一时间大量的请求打到DB上反而压垮DB,更合理的策略是立即熔断返回秒杀活动结束或系统繁忙的错误提示,停止接受新的秒杀请求。

但此时肯定有部分订单是生成成功的,所以我们应该继续执行更新库存的,但是此时redis已经宕机了,无法读取到库存数据,那该怎么办呢。这时我们可以直接读取DB中秒杀商品表中对应商品的订单数量,然后通过计算进行更新即可。


三、具体流程图#

模拟实际情况的性能如下:


四、具体代码实现#

想直接了解具体实现请跳转至(七)核心代码

(一)相关依赖以及#

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.4.2</version>
<relativePath/>
</parent>
<groupId>com.yilena</groupId>
<artifactId>seckill</artifactId>
<version>1.0-SNAPSHOT</version>
<properties>
<maven.compiler.source>21</maven.compiler.source>
<maven.compiler.target>21</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!--mysql-->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.23</version>
</dependency>
<!--mybatis-->
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>3.0.4</version>
</dependency>
<!--lombok-->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.30</version>
<optional>true</optional>
</dependency>
<!--redis-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!--hutool工具包-->
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.8.24</version>
</dependency>
<!--rabbitMQ-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
<!--sharding-jdbc-->
<dependency>
<groupId>org.apache.shardingsphere</groupId>
<artifactId>shardingsphere-jdbc</artifactId>
<version>5.5.0</version>
<exclusions>
<exclusion>
<groupId>org.apache.shardingsphere</groupId>
<artifactId>shardingsphere-test-util</artifactId>
</exclusion>
</exclusions>
</dependency>
<!--druid-->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>1.2.18</version>
</dependency>
</dependencies>
<build>
<pluginManagement>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.8.1</version>
<configuration>
<source>21</source>
<target>21</target>
<encoding>UTF-8</encoding>
<annotationProcessorPaths>
<path>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.30</version>
</path>
</annotationProcessorPaths>
</configuration>
</plugin>
</plugins>
</pluginManagement>
</build>
</project>
server:
port: 8080
spring:
profiles:
include: dev
main:
allow-circular-references: true
# RabbitMQ 配置
rabbitmq:
host: ${yilena.mq.host}
port: 5672
username: ${yilena.mq.username}
password: ${yilena.mq.password}
virtual-host: /
connection-timeout: 5000
cache:
channel.size: 50
channel.checkout-timeout: 1000
publisher-returns: true
# Redis 配置
data:
redis:
host: ${yilena.redis.host}
port: ${yilena.redis.port}
database: ${yilena.redis.database}
password: ${yilena.redis.password}
# ShardingSphere 配置
datasource:
driver-class-name: org.apache.shardingsphere.driver.ShardingSphereDriver
url: jdbc:shardingsphere:classpath:sharding.yaml
# MyBatis 配置
mybatis:
configuration:
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
# 雪花算法配置
sf:
work-i-d: 3
sequence-length: 6
data-center-index: 6
work-i-d-index: 8
time-stamp-index: 12
# 日志配置
logging:
level:
org.apache.shardingsphere: DEBUG
com.alibaba.druid: DEBUG
org.springframework.jdbc: DEBUG
com.zaxxer.hikari: DEBUG
src/main/resources/sharding.yaml
dataSources:
ds_0:
dataSourceClassName: com.zaxxer.hikari.HikariDataSource
driverClassName: com.mysql.cj.jdbc.Driver
jdbcUrl: jdbc:mysql://${你自己的数据库地址}/seckill
username: ${用户名}
password: ${密码}
rules:
- !SHARDING
tables:
user:
actualDataNodes: ds_0.user
order:
actualDataNodes: ds_0.order_${0..9}
tableStrategy:
standard:
shardingColumn: user_id
shardingAlgorithmName: user_mod
shop_seckill:
actualDataNodes: ds_0.shop_seckill_${0..9}
tableStrategy:
standard:
shardingColumn: id
shardingAlgorithmName: id_mod
shardingAlgorithms:
user_mod:
type: INLINE
props:
algorithm-expression: order_${user_id % 10}
id_mod:
type: INLINE
props:
algorithm-expression: shop_seckill_${id % 10}
props:
sql-show: true
check-table-metadata-enabled: true
mode:
type: Standalone

(二)配置类#

1.Redis配置类#

package com.yilena.seckill.config;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.ClassPathResource;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.data.redis.core.script.RedisScript;
import org.springframework.data.redis.listener.RedisMessageListenerContainer;
import org.springframework.data.redis.serializer.StringRedisSerializer;
@Configuration
@Slf4j
public class RedisConfig {
@Bean
public RedisTemplate redisTemplate(RedisConnectionFactory redisConnectionFactory) {
log.info("开始创建redis模板对象...");
RedisTemplate redisTemplate = new RedisTemplate();
//设置redis的连接工厂对象
redisTemplate.setConnectionFactory(redisConnectionFactory);
//设置redis key的序列化器
redisTemplate.setKeySerializer(new StringRedisSerializer());
return redisTemplate;
}
@Bean(name = "seckillScript")
public RedisScript<Long> seckillScript() {
DefaultRedisScript<Long> script = new DefaultRedisScript<>();
script.setLocation(new ClassPathResource("scripts/seckill.lua")); // 确保路径正确
script.setResultType(Long.class);
return script;
}
}

2.RabbitTemplate配置类#

package com.yilena.seckill.config;
import com.yilena.seckill.constant.RabbitMqConstant;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.core.Binding;
import org.springframework.amqp.core.BindingBuilder;
import org.springframework.amqp.core.DirectExchange;
import org.springframework.amqp.core.Queue;
import org.springframework.amqp.rabbit.config.SimpleRabbitListenerContainerFactory;
import org.springframework.amqp.rabbit.connection.ConnectionFactory;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.amqp.support.converter.Jackson2JsonMessageConverter;
import org.springframework.amqp.support.converter.MessageConverter;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Slf4j
@Configuration
@RequiredArgsConstructor
public class RabbitMqConfig {
private final ConnectionFactory connectionFactory;
@Bean
public MessageConverter jsonMessageConverter() {
return new Jackson2JsonMessageConverter();
}
@Bean
public RabbitTemplate rabbitTemplate(ConnectionFactory connectionFactory) {
// 实例化
RabbitTemplate template = new RabbitTemplate(connectionFactory);
// 设置消息转换器为jsonMessageConverter,确保消息以JSON格式进行发送和接收
template.setMessageConverter(jsonMessageConverter());
return template;
}
// 创建监听容器工厂
@Bean
public SimpleRabbitListenerContainerFactory rabbitListenerContainerFactory(ConnectionFactory connectionFactory) {
SimpleRabbitListenerContainerFactory factory = new SimpleRabbitListenerContainerFactory();
// 设置连接工厂,用于后续创建的监听器容器连接到RabbitMQ
factory.setConnectionFactory(connectionFactory);
factory.setMessageConverter(jsonMessageConverter());
return factory;
}
// 创建交换机
@Bean
public DirectExchange directExchange() {
return new DirectExchange(RabbitMqConstant.SECKILL_EXCHANGE);
}
// 创建队列
@Bean
public Queue seckillQueue() {
return new Queue(RabbitMqConstant.SECKILL_QUEUE);
}
// 绑定队列和交换机
@Bean
public Binding binding(DirectExchange directExchange , Queue seckillQueue) {
return BindingBuilder.bind(seckillQueue)
.to(directExchange)
.with(RabbitMqConstant.SECKILL_BINDING_KEY);
}
}
package com.yilena.seckill.config;
import com.yilena.seckill.service.RetryService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.config.BeanPostProcessor;
import org.springframework.stereotype.Component;
/**
* 重试机制
*/
@Component
@Slf4j
@RequiredArgsConstructor
public class RabbitTemplateCallbackConfigurer implements BeanPostProcessor {
private final RetryService retryService;
// 在监听器初始化后才会触发
@Override
public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
// 检查是否是RabbitTemplate
if (bean instanceof RabbitTemplate) {
RabbitTemplate rabbitTemplate = (RabbitTemplate) bean;
// 设置确认回调
rabbitTemplate.setConfirmCallback((correlationData, ack, cause) -> {
// 当消息发送未被确认时,会调用此方法
if (!ack) {
log.error("消息发送失败: {}", correlationData != null ? correlationData.getId() : "unknown");
retryService.asyncRetry(correlationData);
}
});
}
// 返回处理后的bean实例
return bean;
}
}

3.和过滤器的配置类#

package com.yilena.seckill.config;
import com.yilena.seckill.filter.CachedBodyFilter;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.Ordered;
@Configuration
public class FilterConfig {
@Bean
public FilterRegistrationBean<CachedBodyFilter> requestCachingFilterRegistration() {
FilterRegistrationBean<CachedBodyFilter> registration = new FilterRegistrationBean<>();
registration.setFilter(new CachedBodyFilter());
// 设置拦截路径
registration.addUrlPatterns("/seckill/buy");
// 设置最高优先级,确保先于其他Filter执行
registration.setOrder(Ordered.HIGHEST_PRECEDENCE);
return registration;
}
}
package com.yilena.seckill.config;
import com.yilena.seckill.intercepter.SeckillInterceptor;
import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration
@RequiredArgsConstructor
public class WebMvcInterceptorConfig implements WebMvcConfigurer {
private final SeckillInterceptor seckillInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(seckillInterceptor)
.addPathPatterns("/**");
}
}

这里需要说明一下,过滤器的作用是缓存请求体,以便后续拦截器解析请求体中的ID数据从而拦截不合法请求。

4.雪花算法生成ID配置#

package com.yilena.seckill.config;
import com.yilena.seckill.utils.SnowFlake;
import com.yilena.seckill.utils.SnowFlakeProperties;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
@EnableConfigurationProperties(SnowFlakeProperties.class)
public class SnowFlakeConfig {
@Bean
public SnowFlake snowFlake(SnowFlakeProperties properties) {
return new SnowFlake(properties);
}
}

(三)常量类#

1.MQ相关常量#

package com.yilena.seckill.constant;
public class RabbitMqConstant {
public static final String SECKILL_EXCHANGE = "seckill.exchange";
public static final String SECKILL_QUEUE = "seckill.queue";
public static final String SECKILL_BINDING_KEY = "seckill.binding.key";
}

2.Redis相关常量#

package com.yilena.seckill.constant;
public class RedisKeyConstant {
public static final String SHOP_SECKILL_MSG_KEY = "shop:seckill:msg:";
public static final String SHOP_SECKILL_COUNT_KEY = "shop:seckill:count:";
public static final String SHOP_SECKILL_ORDER_KEY = "shop:seckill:order:";
public static final String SHOP_SECKILL_ORDER_MQ_KEY = "shop:seckill:order:mq:";
}

(四)工具类#

1.缓存请求体工具类#

package com.yilena.seckill.utils;
import jakarta.servlet.ReadListener;
import jakarta.servlet.ServletInputStream;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletRequestWrapper;
import java.io.BufferedReader;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
public class CachedBodyHttpServletRequest extends HttpServletRequestWrapper {
// 用于缓存请求体序列化的字节数组
private final byte[] cachedBody;
// 构造函数,将请求体序列化到cachedBody中
public CachedBodyHttpServletRequest(HttpServletRequest request) throws IOException {
super(request);
InputStream inputStream = request.getInputStream();
this.cachedBody = inputStream.readAllBytes();
}
@Override
public ServletInputStream getInputStream() {
return new CachedBodyServletInputStream(cachedBody);
}
// 重写getReader方法,返回可重复读取的BufferedReader
@Override
public BufferedReader getReader() {
ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(cachedBody);
return new BufferedReader(new InputStreamReader(byteArrayInputStream));
}
// 提供可重复读取的ServletInputStream
public static class CachedBodyServletInputStream extends ServletInputStream {
private final ByteArrayInputStream inputStream;
public CachedBodyServletInputStream(byte[] cachedBody) {
this.inputStream = new ByteArrayInputStream(cachedBody);
}
@Override
public int read() {
return inputStream.read();
}
@Override
public boolean isFinished() {
return inputStream.available() == 0;
}
@Override
public boolean isReady() {
return true;
}
@Override
public void setReadListener(ReadListener listener) {
throw new UnsupportedOperationException();
}
}
}

2.线程池工具类#

package com.yilena.seckill.utils;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.CompletableFuture;
public class ThreadPoolUtil {
public static <T> List<List<T>> slicingData(List<T> dataList, Integer splitSize) {
int dataSize = dataList.size();
int groupSize = dataSize / splitSize;
if (dataSize % splitSize != 0) {
groupSize = groupSize + 1;
}
List<List<T>> chunks = new ArrayList<>(groupSize);
for (int i = 0; i < groupSize; i++) {
int fromIndex = i * splitSize;
int toIndex = fromIndex + splitSize;
List<T> suppliesReadData;
if (i == groupSize - 1) {
suppliesReadData = dataList.subList(fromIndex, dataList.size());
} else {
suppliesReadData = dataList.subList(fromIndex, toIndex);
}
chunks.add(suppliesReadData);
}
return chunks;
}
public static void allFuturesWait(List<CompletableFuture<Void>> completableFutures) {
// 等待所有异步任务完成后,可以继续进行下一步操作
CompletableFuture<Void> allFutures = CompletableFuture.allOf(completableFutures.toArray(new CompletableFuture[0]));
allFutures.join();
}
}

3.雪花算法的生成以及检验相关工具类#

package com.yilena.seckill.utils;
import org.springframework.stereotype.Component;
@Component
public class SnowFlake {
private SnowFlakeProperties properties;
public SnowFlake() {
}
public SnowFlake(SnowFlakeProperties properties) {
this.properties = properties;
}
public synchronized long getID(){
//获得当前时间
long now = System.currentTimeMillis();
long lastTimeStamp = properties.getLastTimeStamp();
//当前系统时间小于上一次记录时间
if (now < lastTimeStamp){
throw new RuntimeException("时钟回拨异常");
}
//相同时间 要序列号进制增量
if (now == lastTimeStamp){
//防止溢出
long sequence = properties.getSequence();
sequence = (sequence + 1) & properties.getSequenceMask();
//更新sequence 的值
properties.setSequence(sequence);
if (sequence == 0L){
//溢出处理
try {
Thread.sleep(1L);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
//获取下一毫秒时间 (有锁)
now = System.currentTimeMillis();
}
}else {
//置0
properties.setSequence(0L);
}
//记录当前时间
properties.setLastTimeStamp(now);
return ((now - properties.getStartTimeStamp()) << properties.getTimeStampIndex()) |
(properties.getDataCenterID() << properties.getDataCenterIndex())|
(properties.getWorkID() << properties.getWorkIDIndex()) | properties.getSequence();
}
}
package com.yilena.seckill.utils;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.Setter;
import org.springframework.boot.context.properties.ConfigurationProperties;
@Data
@AllArgsConstructor
@ConfigurationProperties(prefix = "sf")
public class SnowFlakeProperties {
// 起始时间戳
private long startTimeStamp = 1577808000L;
// 机器ID
private long workID;
// 数据中心ID
private long dataCenterID = 0;
// 序列号
private long sequence = 0L;
// 数据中心位移位数
private long dataCenterIndex;
// 机器ID位移位数
private long workIDIndex;
// 时间戳位移位数
private long timeStampIndex;
// 记录上一次时间戳
private long lastTimeStamp = -1L;
// 序列号长度
private long sequenceLength;
// 序列号掩码(动态计算)
private long sequenceMask;
public SnowFlakeProperties() {
this.sequence = 0L;
}
// 当sequenceLength被设置时自动计算sequenceMask
public void setSequenceLength(long sequenceLength) {
this.sequenceLength = sequenceLength;
this.sequenceMask = ~(-1L << sequenceLength);
}
}
package com.yilena.seckill.utils;
public class SnowFlakeValidator {
public static boolean isSnowFlakeID(long id, SnowFlakeProperties props) {
// 验证序列号:必须在 0 ~ sequenceMask 之间
long sequenceMask = props.getSequenceMask();
long sequence = id & sequenceMask;
if (sequence < 0 || sequence > sequenceMask) {
return false;
}
// 验证数据中心ID
long dataCenterBits = props.getWorkIDIndex() - props.getDataCenterIndex();
long dataCenterMask = (1L << dataCenterBits) - 1;
long dataCenterID = (id >> props.getDataCenterIndex()) & dataCenterMask;
if (dataCenterID != props.getDataCenterID()) {
return false;
}
// 验证机器ID
long workIDBits = props.getTimeStampIndex() - props.getWorkIDIndex();
long workIDMask = (1L << workIDBits) - 1;
long workID = (id >> props.getWorkIDIndex()) & workIDMask;
if (workID != props.getWorkID()) {
return false;
}
// 验证时间戳
long timeStamp = (id >> props.getTimeStampIndex()) + props.getStartTimeStamp();
if (timeStamp < props.getStartTimeStamp()) {
return false;
}
return true;
}
}

(五)#

package com.yilena.seckill.entity.po;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.time.LocalDateTime;
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Order {
private Long id;
private Long userId;
private Long shopId;
private LocalDateTime createTime;
private Integer status;
}
package com.yilena.seckill.entity.po;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.RequiredArgsConstructor;
import java.time.LocalDateTime;
@Data
@NoArgsConstructor
@AllArgsConstructor
public class ShopSeckill {
private Long id;
private Integer count;
private LocalDateTime beginTime;
private LocalDateTime endTime;
}
package com.yilena.seckill.entity.po;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@NoArgsConstructor
@AllArgsConstructor
public class User {
private Long id;
}
package com.yilena.seckill.entity;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.io.Serializable;
@Data
@AllArgsConstructor
@NoArgsConstructor
@JsonInclude(JsonInclude.Include.NON_NULL)
public class Result implements Serializable {
@JsonProperty("code")
private Integer code;
@JsonProperty("msg")
private String msg;
@JsonProperty("data")
private Object data;
public static Result success() {
Result result = new Result();
result.code = 1;
result.msg = "success";
return result;
}
public static Result success(Object object) {
Result result = new Result();
result.data = object;
result.code = 1;
result.msg = "success";
return result;
}
public static Result error(String msg) {
Result result = new Result();
result.msg = msg;
result.code = 0;
return result;
}
}

(六)过滤器和拦截器#

package com.yilena.seckill.filter;
import com.yilena.seckill.utils.CachedBodyHttpServletRequest;
import jakarta.servlet.*;
import jakarta.servlet.http.HttpServletRequest;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
import java.io.IOException;
@Component
@Order(1) // 确保在其他过滤器之前执行
public class CachedBodyFilter implements Filter {
// 缓存秒杀请求的请求体
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
// 将通用的ServletRequest转换为HTTP请求对象,以便获取HTTP特定的功能和属性
HttpServletRequest httpRequest = (HttpServletRequest) request;
// 包装原始请求,以缓存请求体,这是为了在后续处理中能够多次读取请求内容
CachedBodyHttpServletRequest cachedRequest = new CachedBodyHttpServletRequest(httpRequest);
// 将包装后的请求对象传递给过滤链中的下一个过滤器
chain.doFilter(cachedRequest, response);
}
}
package com.yilena.seckill.intercepter;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.yilena.seckill.utils.SnowFlakeProperties;
import com.yilena.seckill.utils.SnowFlakeValidator;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.util.ContentCachingRequestWrapper;
import com.yilena.seckill.entity.po.Order;
import java.io.BufferedReader;
@Slf4j
@Component
@RequiredArgsConstructor
public class SeckillInterceptor implements HandlerInterceptor {
private final SnowFlakeProperties snowFlakeProperties;
private final ObjectMapper objectMapper;
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
String url = request.getRequestURL().toString();
// 匹配秒杀下单接口
if (url.contains("/seckill/buy")) {
// 从包装请求中读取请求体
StringBuilder body = new StringBuilder();
BufferedReader reader = request.getReader();
String line;
while ((line = reader.readLine()) != null) {
body.append(line);
}
try {
// 反序列化为Order对象,提取userId和shopId
Order order = objectMapper.readValue(body.toString(), Order.class);
Long userId = order.getUserId();
Long shopId = order.getShopId();
// 同步检测雪花ID合法性
if (userId == null
|| !SnowFlakeValidator.isSnowFlakeID(userId, snowFlakeProperties)
|| ! SnowFlakeValidator.isSnowFlakeID(shopId, snowFlakeProperties)) {
log.error("非法用户ID:{},拦截秒杀请求", userId);
response.sendError(HttpServletResponse.SC_BAD_REQUEST, "用户ID或商品ID非法");
return false;
}
} catch (Exception e) {
log.error("请求体解析或ID检测失败:{}", e.getMessage());
response.sendError(HttpServletResponse.SC_BAD_REQUEST, "请求格式错误");
return false;
}
}
return true;
}
}

(七)核心代码#

1.controller层#

package com.yilena.seckill.controller;
import com.yilena.seckill.entity.Result;
import com.yilena.seckill.entity.po.Order;
import com.yilena.seckill.service.SeckillService;
import com.yilena.seckill.utils.SnowFlake;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.*;
@Slf4j
@RestController
@RequestMapping("/seckill")
@RequiredArgsConstructor
public class SeckillController {
private final SeckillService seckillService;
@PostMapping( "/buy")
public Result buy(@RequestBody Order order){
log.info("用户id为{},下单了秒杀商品:{}", order.getUserId(), order.getShopId());
return seckillService.buy(order);
}
}

2.service层#

package com.yilena.seckill.service.serviceImpl;
import cn.hutool.json.JSONUtil;
import com.yilena.seckill.constant.RabbitMqConstant;
import com.yilena.seckill.constant.RedisKeyConstant;
import com.yilena.seckill.dao.SeckillDao;
import com.yilena.seckill.entity.Result;
import com.yilena.seckill.entity.po.Order;
import com.yilena.seckill.entity.po.ShopSeckill;
import com.yilena.seckill.service.SeckillService;
import com.yilena.seckill.utils.SnowFlake;
import jakarta.annotation.Resource;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.core.MessageDeliveryMode;
import org.springframework.amqp.rabbit.connection.CorrelationData;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.script.RedisScript;
import org.springframework.stereotype.Service;
import java.time.LocalDateTime;
import java.util.Collections;
@Slf4j
@Service
@RequiredArgsConstructor
public class SeckillServiceImpl implements SeckillService {
private final StringRedisTemplate stringRedisTemplate;
private final SnowFlake snowFlake;
private final RabbitTemplate rabbitTemplate;
@Resource(name = "seckillScript")
private RedisScript<Long> seckillScript;
@Override
public Result buy(Order order) {
// 获取订单信息
Long shopId = order.getShopId();
Long userId = order.getUserId();
//执行lua脚本,执行下单逻辑
Long result = stringRedisTemplate.execute(
seckillScript,
Collections.emptyList(),
userId.toString(), shopId.toString(),
String.valueOf(LocalDateTime.now()
.atZone(java.time.ZoneId.systemDefault())
.toInstant()
.toEpochMilli())
);
// 处理可能的null返回值
if (result == null) {
log.error("Lua脚本返回null, shopId: {}", shopId);
return Result.error("系统异常,请重试");
}
int resultCode = result.intValue();
// 扩展错误处理
switch (resultCode) {
case 500:
// 下单完成,生成订单ID
Long orderId = snowFlake.getID();
order.setId(orderId);
order.setCreateTime(LocalDateTime.now());
order.setStatus(0);
// 将订单信息发送到mq,异步处理
rabbitTemplate.convertAndSend(
RabbitMqConstant.SECKILL_EXCHANGE,
RabbitMqConstant.SECKILL_BINDING_KEY,
order,
message -> {
// 消息持久化, 防止消息丢失
message.getMessageProperties().setDeliveryMode(MessageDeliveryMode.PERSISTENT);
return message;
},
// 生成关联id,便于重试
new CorrelationData(order.getId().toString())
);
// 先返回订单信息交给前端进行响应,优化用户体验
return Result.success(order);
case 400:
return Result.error("当前商品已售空!");
case 401:
return Result.error("商品不存在");
case 402:
return Result.error("秒杀活动尚未开始!");
case 403:
return Result.error("相同商品仅限一单!");
case 404:
return Result.error("商品库存信息异常!");
default:
log.warn("未知返回码: {}", resultCode);
return Result.error("秒杀失败,请重试");
}
}
}

3.Lua脚本#

local userId = ARGV[1]
local shopId = ARGV[2]
local currentTime = tonumber(ARGV[3])
local countKey = "shop:seckill:count:" .. shopId
local orderKey = "shop:seckill:order:" .. shopId
local shopKey = "shop:seckill:msg:" .. shopId
-- 检查商品是否存在
if redis.call('exists', shopKey) == 0 then
return 401 -- 商品不存在
end
-- 解析商品信息
local beginTime = tonumber(redis.call('hget', shopKey, 'beginTime'))
local endTime = tonumber(redis.call('hget', shopKey, 'endTime'))
-- 校验活动时间
if currentTime < beginTime or currentTime > endTime then
return 402 -- 活动未开始/已结束
end
-- 订单重复性检查
if redis.call("SISMEMBER", orderKey, userId) == 1 then
return 403 -- 订单重复
end
-- 库存键存在性检查
if redis.call("EXISTS", countKey) == 0 then
return 404 -- 库存键不存在
end
local count = redis.call("GET", countKey)
if tonumber(count) > 0 then
redis.call("DECR", countKey)
redis.call("SADD", orderKey, userId)
return 500 -- 成功
else
return 400 -- 库存不足
end

注意这里Lua脚本存放的路径要与redis配置类里的一致!

4.MQ#

package com.yilena.seckill.mq;
import com.yilena.seckill.constant.RabbitMqConstant;
import com.yilena.seckill.constant.RedisKeyConstant;
import com.yilena.seckill.dao.OrderDao;
import com.yilena.seckill.dao.UserDao;
import com.yilena.seckill.entity.po.Order;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;
@Slf4j
@Component
@RequiredArgsConstructor
public class SeckillQueueListener {
private final OrderDao orderDao;
private final UserDao userDao;
private final StringRedisTemplate stringRedisTemplate;
@RabbitListener(queues = RabbitMqConstant.SECKILL_QUEUE)
public void handleSeckillQueue(Order order) {
log.info("秒杀商品信息队列接收到信息,开始处理id为 {} 的订单", order.getId());
// 幂等性检查,避免重复消费
String key = RedisKeyConstant.SHOP_SECKILL_ORDER_MQ_KEY + order.getId();
Boolean isNew = stringRedisTemplate.opsForValue().setIfAbsent(key, "processing");
// 如果返回false,则说明该订单已经被处理过,直接返回
if (Boolean.FALSE.equals(isNew)) {
log.warn("订单重复消费: {}", order.getId());
return;
}
try {
// 检验用户id是否存在
Integer isExist = userDao.isExist(order.getUserId());
// 用户不存在,回滚数据
if (isExist == 0) {
throw new RuntimeException("用户不存在");
}
// 处理订单
order.setStatus(1);
orderDao.insert(order);
} catch (Exception e) {
log.error("订单处理失败: {}", order.getId(), e);
// 回滚Redis库存
stringRedisTemplate.opsForValue().increment(
RedisKeyConstant.SHOP_SECKILL_COUNT_KEY + order.getShopId()
);
// 移除订单记录
stringRedisTemplate.opsForSet().remove(
RedisKeyConstant.SHOP_SECKILL_ORDER_KEY + order.getShopId(),
order.getUserId().toString()
);
} finally {
// 处理完成后删除标记
stringRedisTemplate.delete(key);
}
}
}

因为不会出现重复下单,所以如果isNew为false那只能是重复消费的情况。

5.两个定时任务#

package com.yilena.seckill.task;
import cn.hutool.json.JSONUtil;
import com.yilena.seckill.constant.RedisKeyConstant;
import com.yilena.seckill.dao.SeckillDao;
import com.yilena.seckill.entity.po.ShopSeckill;
import com.yilena.seckill.utils.ThreadPoolUtil;
import jakarta.annotation.PreDestroy;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
@Slf4j
@Component
@RequiredArgsConstructor
public class InitSeckillShopsRedisDataTask {
private final SeckillDao seckillDao;
private final StringRedisTemplate stringRedisTemplate;
private final ExecutorService executor = Executors.newFixedThreadPool(10);
// 销毁线程池
@PreDestroy
public void destroy() {
// 禁止新任务提交,等待已提交任务执行完毕
executor.shutdown();
try {
// 最多再等待60秒,如果超时直接强制关闭线程池
if (!executor.awaitTermination(60, TimeUnit.SECONDS)) {
executor.shutdownNow();
}
} catch (InterruptedException e) {
executor.shutdownNow();
Thread.currentThread().interrupt();
}
}
// 假设10点开启秒杀活动
@Scheduled (cron = "0 50 9 * * ?")
public void initSeckillShopsRedisData (){
// 显式设置类加载器,如果对这条代码有疑惑的话请看本博客目录第五个主标题
Thread.currentThread().setContextClassLoader(this.getClass().getClassLoader());
log.info("开始预热秒杀商品数据,执行时间:{}", LocalDateTime.now());
// 删除之前的缓存
deleteKeysByPattern(RedisKeyConstant.SHOP_SECKILL_COUNT_KEY + "*");
deleteKeysByPattern(RedisKeyConstant.SHOP_SECKILL_MSG_KEY + "*");
deleteKeysByPattern(RedisKeyConstant.SHOP_SECKILL_ORDER_KEY + "*");
// 因为秒杀商品一般数量不会太多,所以不需要怕oom问题
List<ShopSeckill> allSeckillShops = seckillDao.getAllSeckillShops();
// 进行分片
List<List<ShopSeckill>> splitData = ThreadPoolUtil.slicingData(allSeckillShops, 100);
// 开启线程并行进行数据预热,加快速度,防止秒杀开始前未预热完成
splitData.forEach(batch -> {
List<CompletableFuture<Void>> futures = new ArrayList<>();
batch.forEach(shopSeckill -> {
CompletableFuture<Void> future = CompletableFuture.runAsync(() -> {
try {
// 库存预热
stringRedisTemplate.opsForValue().set(RedisKeyConstant.SHOP_SECKILL_COUNT_KEY + shopSeckill.getId(), shopSeckill.getCount().toString());
// 信息预热
stringRedisTemplate.opsForHash().put(RedisKeyConstant.SHOP_SECKILL_MSG_KEY + shopSeckill.getId(),"id" ,shopSeckill.getId().toString());
stringRedisTemplate.opsForHash().put(RedisKeyConstant.SHOP_SECKILL_MSG_KEY + shopSeckill.getId(),"count" ,shopSeckill.getCount().toString());
stringRedisTemplate.opsForHash().put(RedisKeyConstant.SHOP_SECKILL_MSG_KEY + shopSeckill.getId(),"beginTime" ,String.valueOf(shopSeckill.getBeginTime()
.atZone(java.time.ZoneId.systemDefault())
.toInstant()
.toEpochMilli()));
stringRedisTemplate.opsForHash().put(RedisKeyConstant.SHOP_SECKILL_MSG_KEY + shopSeckill.getId(),"endTime" ,String.valueOf(shopSeckill.getEndTime()
.atZone(java.time.ZoneId.systemDefault())
.toInstant()
.toEpochMilli()));
}catch (Exception e){
log.error("预热秒杀商品数据失败,商品ID:{}", shopSeckill.getId(), e);
}
}, executor);
futures.add(future);
});
// 等待当前批次完成
ThreadPoolUtil.allFuturesWait(futures);
});
log.info("预热秒杀商品数据完成,结束时间:{}", LocalDateTime.now());
}
private void deleteKeysByPattern(String pattern) {
Set<String> keys = stringRedisTemplate.keys(pattern);
if (keys != null && !keys.isEmpty()) {
stringRedisTemplate.delete(keys);
}
}
}
package com.yilena.seckill.task;
import cn.hutool.json.JSONUtil;
import com.yilena.seckill.constant.RedisKeyConstant;
import com.yilena.seckill.dao.OrderDao;
import com.yilena.seckill.dao.SeckillDao;
import com.yilena.seckill.utils.ThreadPoolUtil;
import jakarta.annotation.PreDestroy;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
@Slf4j
@Component
@RequiredArgsConstructor
public class UpdateShopCountTask {
private final SeckillDao seckillDao;
private final StringRedisTemplate stringRedisTemplate;
private final OrderDao orderDao;
private final ExecutorService executor = Executors.newFixedThreadPool(10);
// 销毁线程池
@PreDestroy
public void destroy() {
// 禁止新任务提交,等待已提交任务执行完毕
executor.shutdown();
try {
// 最多再等待60秒,如果超时直接强制关闭线程池
if (!executor.awaitTermination(60, TimeUnit.SECONDS)) {
executor.shutdownNow();
}
} catch (InterruptedException e) {
executor.shutdownNow();
Thread.currentThread().interrupt();
}
}
// 假设十点十分结束秒杀活动
@Scheduled (cron = "0 10 10 * * ?")
public void updateShopCount () {
log.info("开始更新秒杀活动DB库存余额,执行时间:{}", LocalDateTime.now());
// 获取所有秒杀商品id
List<Long> shopIds = seckillDao.getAllShopIds();
// 进行分片, 提高效率
List<List<Long>> splitData = ThreadPoolUtil.slicingData(shopIds, 100);
// 开启线程并行进行数据更新
splitData.forEach(batch -> {
List<CompletableFuture<Void>> futures = new ArrayList<>();
batch.forEach(shopId -> {
CompletableFuture<Void> future = CompletableFuture.runAsync(() -> {
try {
// 检查redis中的库存余额
String countStr = stringRedisTemplate.opsForValue().get(RedisKeyConstant.SHOP_SECKILL_COUNT_KEY + shopId);
if (countStr != null) {
// 获取redis中的库存余额
Integer count = Integer.parseInt(countStr);
// 更新DB库存余额
seckillDao.updateShopCount(shopId, count);
}else{
// redis数据可能出了问题,所以需要通过计算DB中对应订单的数量来获取库存余额
Integer count = orderDao.getShopOrderCount(shopId);
seckillDao.updateShopCountByOrderCount(shopId, count);
}
}catch (Exception e){
log.error("更新秒杀活动DB库存余额失败,商品ID:{}", shopId , e);
}
}, executor);
futures.add(future);
});
// 等待当前批次完成
ThreadPoolUtil.allFuturesWait(futures);
});
log.info("更新秒杀活动DB库存余额完成,结束时间:{}", LocalDateTime.now());
}
}

至于,都比较简单所以没什么好提的,唯一需要注意的就是在更新库存的定时任务里,当redis数据丢失后执行的这条代码:orderDao.getShopOrderCount(shopId),其SQL语句要记得过滤掉订单创建时间在秒杀活动开始前的订单数据。


五、关于InitSeckillShopsRedisDataTask中显式设置类加载器的作用#

这是我在写代码时间Sharding-JDBC遇到的坑,具体作用可以看我这条博客,里面有详细解释。

Sharding-JDBC 定时任务 SQL 无响应的解决方案-CSDN博客


如果你有更好的方案,请在评论区告诉我!

码文不易,留个赞再走呗


原文链接: 一个简单高效的秒杀方案的实现 作者: Yilena

分享

如果这篇文章对你有帮助,欢迎分享给更多人!

一个简单高效的秒杀方案的实现
https://blog.csdn.net/2401_88959292/article/details/148370580?spm=1001.2014.3001.5501
作者
Yilena
发布于
2025-07-07
许可协议
CC BY 4.0

部分信息可能已经过时

相关文章 智能推荐
1
基于MySQL + Redis + JWT的双令牌认证方案的实现
业务拆解 本文针对单令牌认证机制存在的安全隐患与用户体验痛点,设计并实现了一套基于MySQL、Redis与JWT的双令牌(访问令牌+刷新令牌)认证方案。文章详细分析了双令牌机制的优势,通过分离认证与状态维护职责,结合Redis黑名单与分布式锁限流,有效提升了系统安全性。同时,提供了完整的Java代码实现,包括JWT工具类、Redis令牌管理服务、全局过滤器及业务层Controller逻辑,展示了如何优雅地处理令牌刷新、单点互斥登录及异常响应,为构建安全可靠的Web应用认证体系提供了实践参考。
2
自定义错误码 + 全局异常体系 + Result的优雅实现方案
业务拆解 本文详细介绍了一套适用于大型微服务项目的优雅错误处理方案。通过分析枚举类在规模化项目中的局限性,提出采用常量类结合接口抽象的方式设计静态与动态错误码。文章深入讲解了如何构建基础异常类及其具体实现,并展示了利用@RestControllerAdvice和@ExceptionHandler打造全局异常拦截器的完整代码。此外,还提供了标准化的Result响应封装及反馈类设计,帮助开发者实现前后端交互中错误码、异常与响应结果的统一规范与高效管理。
3
点餐场景下:分析实现十万级用户日志的店铺推荐方案
业务拆解 本文针对点餐小程序十万级用户进店日志场景,设计并实现了一套高效的个性化店铺推荐方案。通过冷热数据分离策略,利用Elasticsearch存储近30天热数据,结合MySQL与Redis优化查询性能。文章详细阐述了基于访问频次、分区及分类偏好的加权评分排序算法,并规划了每日定时执行的离线分析任务以平衡系统开销。最后,提供了完整的实体类定义、ES数据迁移定时任务及多线程并发日志分析的Java代码实现,为海量日志分析与推荐系统落地提供了实践参考。
4
如何应对海量Key带来的redis内存占用问题?
业务拆解 本文针对海量Key导致的Redis内存占用过高问题,深入分析了内存碎片化与元数据开销的根源,并提出了五种切实可行的解决方案。从基础的合并小Key(利用Hash结构)与临时添加TTL救急,到架构层面的Redis集群模式与Redis on Flash(内存+SSD)降本方案,再到结合MySQL的冷热数据分离策略(全量/冷数据存DB,热数据存Redis)。文章通过详实的流程图与优劣对比,帮助开发者在不同预算与业务场景下,科学应对Redis内存瓶颈。
5
一文讲透 RAG:从定义、架构、底层原理到主流方案对比
技术笔记 本文深度剖析了检索增强生成(RAG)技术的全貌,从大模型面临的知识静态、幻觉及私有数据缺失等痛点出发,阐明了RAG作为工程化解法的核心价值。文章详细拆解了RAG的标准架构,包括离线索引(文档解析、Chunking、Embedding)与在线问答(检索、重排、上下文组装、生成)的关键模块,并揭示了其基于参数化与非参数化知识结合的底层原理。此外,针对召回不准、上下文污染等常见问题提供了优化思路,对比了Naive RAG、Modular RAG、GraphRAG及Agentic RAG等主流方案的优劣与适用场景,并特别强调了企业级应用中权限控制与知识治理的重要性,为构建生产级RAG系统提供了全面指南。

目录

封面
Sample Song
Sample Artist
封面
Sample Song
Sample Artist
0:00 / 0:00