GoodsKill项目分析及亮点解析

商品秒杀(GoodsKill)

本文对github商品秒杀项目进行项目拆解分析和代码详解,从而学习微服务的知识》。。(如有侵权,请联系我删除)

项目地址:goodsKill

1. 项目模块总览

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
goodsKill
|--goodskill-admin ||SpringBoot Admin监控服务端,支持Spring Cloud微服务发现
|--goodskill-ai ||AI机器人聊天服务
|--goodskill-gateway ||微服务API网关,统一服务鉴权,支持动态路由加载
|--goodskill-order-provider ||订单服务提供者
|--goodskill-seckill-provider ||秒杀服务提供者
|--goodskill-spring-boot-starter ||项目配置自动装配
|--goodskill-common ||公共服务(目前包含minio上传下载功能)
|--goodskill-web ||提供秒杀模拟接口访问
|--goodskill-job ||elastic-job定时任务
|--goodskill-seata ||集成nacos+dubbo+shardingjdbc+seata的分布式事务解决方案示例
|--goodskill-auth ||auth登录以及授权模块
| |--auth-service ||基于Sa-Token框架的用户登录授权服务
|--oauth2-auth-server ||oauth2.0登录授权服务端,自定义的登录授权服务
|--oauth2-resource-server ||oauth2.0资源服务端,自定义的登录授权服务

2. goodskill-order-provider (订单服务提供者)

分为两个模块:

  • goodskill-order-api
  • goodskill-order-service

2.1 goodskill-order-api

OrderService接口及其实现类

  • OrderService接口
  • OrderServiceFallback实现类

基于 Spring Cloud OpenFeign 实现的 订单微服务 (goodskill-order) 远程调用接口,并提供了 熔断降级处理
它的作用是让其他微服务(比如 goodskill-seckill-provider)可以通过 FeignClient 远程调用订单服务,并在服务不可用时提供降级策略。

OrderService接口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/*
指定调用的微服务名称:goodskill-order
设置熔断降级实现类:OrderServiceFallback
*/
@FeignClient(value = "goodskill-order", fallback = OrderServiceFallback.class)
public interface OrderService {

//调用 goodskill-order 服务的 /deleteRecord 接口,删除秒杀订单
@DeleteMapping("/deleteRecord")
Boolean deleteRecord(@RequestParam("seckillId") long seckillId);

//调用 goodskill-order 服务的 /saveRecord 接口,保存订单记录。
@PostMapping("/saveRecord")
Boolean saveRecord(@RequestBody OrderDTO orderDTO);

//调用 goodskill-order 服务的 /count 接口,查询某个 seckillId 的 订单数量。
@GetMapping("/count")
long count(@RequestParam("seckillId") long seckillId);
}
  • 指定调用的微服务名称:goodskill-order
  • 设置熔断降级实现类:OrderServiceFallback
  • 这意味着当 goodskill-order 服务 不可用 时,调用 OrderServiceFallback 里的方法返回默认值,而不是让请求直接失败

接口中:定义远程调用的 API

  • 调用 goodskill-order 服务的 /deleteRecord 接口,删除秒杀订单
  • 调用 goodskill-order 服务的 /saveRecord 接口,保存订单记录。
  • 调用 goodskill-order 服务的 /count 接口,查询某个 seckillId 的 订单数量。

OrderServiceFallback(熔断降级实现类)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
/*
熔断降级实现类:OrderServiceFallback
*/
@Component
class OrderServiceFallback implements OrderService {
//降级策略:当 deleteRecord() 调用失败时,返回 false,表示删除失败。
public Boolean deleteRecord(long seckillId) {
return false;
}

//降级策略:当 saveRecord() 调用失败时,返回 false,表示订单保存失败。
public Boolean saveRecord(OrderDTO orderDTO) {
return false;
}

//降级策略:count() 方法在降级时 抛出异常,表示此接口不提供备用方案。
public long count(long seckillId) {
throw new NoFallbackAvailableException("Boom!", new RuntimeException());
}
}

降级策略:

  • 当 deleteRecord() 调用失败时,返回 false,表示删除失败。
  • count() 方法在降级时 抛出异常,表示此接口不提供备用方案。

2.2 goodskill-order-service

2.2.1 OrderApplication启动类

订单微服务(goodskill-order-provider)的启动类 OrderApplication,使用的是 Spring Boot + MongoDB + 消息驱动模型(类似 Stream 消费)。

  • 实现:监听名为 seckillMongoSave 的消息队列(或通道),接收秒杀请求信息 SeckillMockSaveVo,然后封装为 OrderDTO 对象并存入 MongoDB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
@SpringBootApplication(scanBasePackages = {"com.goodskill.order", "com.goodskill.core"})
@EnableMongoRepositories(basePackages = "com.goodskill.order.repository")
@Slf4j
public class OrderApplication {
@Autowired
private OrderServiceImpl mongoService;

public static void main(String[] args) {
new SpringApplicationBuilder(OrderApplication.class)
.web(WebApplicationType.SERVLET)
.registerShutdownHook(true)
.run(args);
}

/*
定义 Bean:监听消息通道 seckillMongoSave
*/
@Bean
public Consumer<SeckillMockSaveVo> seckillMongoSave() {
return it -> {
log.info("接收到消息:{}", it);
OrderDTO orderDTO = new OrderDTO();
orderDTO.setSeckillId(BigInteger.valueOf(it.getSeckillId()));
orderDTO.setUserPhone(it.getUserPhone());
orderDTO.setCreateTime(LocalDateTime.now());
mongoService.saveRecord(orderDTO);
};
}
}
  • 启动订单服务(基于 Spring Boot)
  • 启用 MongoDB 的仓库支持
  • 监听名为 seckillMongoSave 的消息通道
  • 每当有用户秒杀请求(如压测模拟数据)通过消息中间件发来时:
    • 接收消息
    • 构建订单对象 OrderDTO
    • 调用 OrderServiceImpl 将其保存至 MongoDB

2.2.2 订单微服务的接口和实现类

  • OrderController
  • OrderRepository
  • OrderServiceImpl

OrderController

OrderController 是订单模块(goodskill-order-provider)的一个 REST 控制器类,用于提供对订单的 基础增删查接口,主要是供内部服务或其他微服务调用,比如秒杀服务、前端管理系统等。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
/*
提供了 订单记录的新增、删除、统计查询的 HTTP 接口
*/
@Slf4j
@RestController
public class OrderController {
@Autowired
private OrderServiceImpl orderService;

@DeleteMapping("/deleteRecord")
public Boolean deleteRecord(long seckillId) {
return orderService.deleteRecord(seckillId);
}

@PostMapping("/saveRecord")
public Boolean saveRecord(OrderDTO orderDTO) {
return orderService.saveRecord(orderDTO);
}

@GetMapping("/count")
public Long count(long seckillId) {
return orderService.count(seckillId);
}
}
方法名 功能说明 请求方式 路径
deleteRecord 删除某个秒杀ID的订单记录 DELETE /deleteRecord
saveRecord 保存订单记录 POST /saveRecord
count 查询某活动的订单总数 GET /count

OrderRepository接口

OrderRepository 接口是订单服务中用于 操作 MongoDB 的数据访问层(DAO),它继承自 Spring Data MongoDB 提供的 MongoRepository,让你几乎不用写 SQL/查询语句,就可以实现对 Order 实体的 CRUD 操作。

1
2
3
4
5
6
7
8
public interface OrderRepository extends MongoRepository<Order, String> {

//根据 seckillId 删除所有对应的订单记录
void deleteBySeckillId(BigInteger seckillId);

//统计某个秒杀活动对应的订单数量
long countBySeckillId(BigInteger seckillId);
}

这个接口是 操作 MongoDB 的核心 DAO 接口,功能如下:

方法名 作用
deleteBySeckillId() 删除某个活动下的订单记录
countBySeckillId() 统计某个活动下的订单数量
(继承自 MongoRepository) 提供常用增删改查操作

OrderServiceImpl

OrderServiceImpl 是订单模块(goodskill-order-provider)的 业务逻辑实现类,用于处理和封装订单的核心操作:保存订单、删除订单、统计订单数量。它作为 OrderController 的 Service 层实现类,并依赖 MongoDB 进行持久化存储。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
@Slf4j
@Service
public class OrderServiceImpl {
@Autowired
private OrderRepository orderRepository;

//删除某场秒杀活动下的所有订单记录
public Boolean deleteRecord(long seckillId) {
orderRepository.deleteBySeckillId(BigInteger.valueOf(seckillId));
return true;
}

//用于秒杀成功时落库,或压测模拟生成订单
public Boolean saveRecord(OrderDTO orderDTO) {
Order order = Order.builder()
.id(UUID.randomUUID().toString())
.seckillId(orderDTO.getSeckillId())
.userPhone(orderDTO.getUserPhone())
.build();
orderRepository.insert(order);
log.info("保存成功,{}", order);
return true;
}

//统计某个 seckillId 对应的订单数
public Long count(long seckillId) {
return orderRepository.countBySeckillId(BigInteger.valueOf(seckillId));
}
}
  • OrderServiceImpl 是订单微服务的核心业务类,负责订单数据的 创建、清理、统计
  • 用于操作订单数据的 服务实现类,调用底层 OrderRepository 完成 CRUD,并对外提供统一的业务逻辑封装
方法名 作用 调用底层 DAO 方法
deleteRecord 删除指定秒杀活动的所有订单记录 deleteBySeckillId(…)
saveRecord 保存订单数据到 MongoDB insert(order)
count 统计某秒杀活动对应的订单数量 countBySeckillId(…)

2.2.3 application.yml配置文件

配置文件为 goodskill-order 微服务准备了 开发环境下的运行基础设施支持

  • 服务注册/配置:Nacos
  • 消息监听:RabbitMQ + Stream + Function
  • 数据存储:MongoDB(含自动索引)
  • 日志调试:Mongo 操作可见
  • 项目解耦和多环境支持:配置中心导入 + 动态端口 + graceful shutdown

3. goodskill-seckill-provider (秒杀服务提供者)

分为两个模块:

  • goodskill-api
  • goodskill-service

3.1 goodskill-api

3.1.1 DTO和VO

DTO数据传输对象

  • ExposerDTO: 暴露秒杀地址 的数据传输对象(DTO),它的作用是在秒杀接口开放时,向前端返回当前秒杀的状态和必要参数(如 MD5 加密串)
  • GoodsDTO: 商品信息的 数据传输对象(DTO),用于封装秒杀商品或普通商品的基本信息,便于服务之间传输或展示在前端页面
  • SeckillExecutionDTO: 用于表示 秒杀执行结果 的数据传输对象(DTO),主要是用来封装一个用户参与秒杀后的结果详情,比如成功/失败状态、相关描述、下单数据、以及二维码路径等
  • SeckillInfoDTO: 封装秒杀活动基本信息的一个数据传输对象(DTO),用于传递秒杀活动的详细信息,包括时间、商品、库存和价格等
  • SeckillMockCanalResponseDTO: 用于表示秒杀活动状态变更响应结果的一个数据传输对象(DTO),常用于消息传递、活动更新反馈
  • SeckillMockRequestDTO: 用于模拟秒杀请求的请求数据传输对象(DTO),主要用于秒杀接口的压测或定时任务模拟用户请求行为
  • SeckillMockResponseDTO: 用于返回秒杀模拟任务执行结果的响应数据传输对象(DTO),包含秒杀活动 ID、执行备注、状态以及任务 ID
  • SeckillResponseDTO: 用于封装 秒杀操作返回的二进制数据(byte[]),通常用于返回一些 结构化或文件类的数据内容,而不是简单的 JSON 文本
  • SuccessKilledDTO: 用于封装秒杀成功记录的 数据传输对象(DTO),它记录了某个用户在参与某次秒杀活动时的详细信息与状态

VO

  • GoodsVO: 用于表示 商品信息的值对象,用于传输与商品相关的信息,如商品ID、价格、描述等
  • SeckillVO: 用于表示 秒杀活动相关的数据。VO 类主要用于数据传输,通常用于前后端数据交互或传递业务数据

3.1.2 Service接口

  • GoodsEsService
  • GoodsService
  • SeckillService

GoodsEsService

GoodsEsService是一个 Feign 客户端接口,它用于和 goodskill-seckill 微服务进行通信,为 goodskill-seckill 服务提供了保存商品、批量保存商品、删除商品以及商品名称搜索的功能

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
/*
用于和 `goodskill-seckill` 微服务进行通信
*/
@FeignClient(value = "goodskill-seckill", contextId = "goods")
public interface GoodsEsService {

//保存商品信息:通过 save 方法,将单个商品信息保存到 goodskill-seckill 服务
@PutMapping("/save")
void save(@RequestBody GoodsDTO goodsDto);

//批量保存商品信息:通过 saveBatch 方法,将多个商品信息批量保存到 goodskill-seckill 服务
@PutMapping("/saveBatch")
void saveBatch(@RequestBody List<GoodsDTO> list);

//删除商品信息:通过 delete 方法,根据传入的 GoodsDTO 对象删除商品信息
@DeleteMapping("/delete")
void delete(@RequestBody GoodsDTO goodsDto);

//根据商品名称进行分页查询:通过 searchWithNameByPage 方法,根据传入的关键词查询商品信息,并分页返回结果
@GetMapping("/searchWithNameByPage")
List<GoodsDTO> searchWithNameByPage(@RequestParam("input") String input);
}

GoodsEsService 接口定义了与 goodskill-seckill 微服务进行商品信息交互的操作。包括以下功能:

  1. 保存商品信息:通过 save 方法,将单个商品信息保存到 goodskill-seckill 服务。
  2. 批量保存商品信息:通过 saveBatch 方法,将多个商品信息批量保存到 goodskill-seckill 服务。
  3. 删除商品信息:通过 delete 方法,根据传入的 GoodsDTO 对象删除商品信息。
  4. 根据商品名称进行分页查询:通过 searchWithNameByPage 方法,根据传入的关键词查询商品信息,并分页返回结果。

GoodsService

用于处理商品相关业务逻辑的接口,提供了多个方法来管理商品信息

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public interface GoodsService {

//上传商品照片
void uploadGoodsPhoto(long goodsId, String fileUrl);

//添加一个新的商品
void addGoods(GoodsVO goods);

//查询多个商品
List<GoodsVO> findMany();

//根据商品ID查询商品
GoodsVO findById(Serializable goodsId);
}

GoodsService 接口主要负责商品的管理操作,包括:

  • 上传商品照片:通过 uploadGoodsPhoto 方法上传商品照片,并将照片关联到商品。
  • 添加商品:通过 addGoods 方法向系统中添加新的商品信息。
  • 查询商品列表:通过 findMany 方法查询多个商品信息,通常用于获取商品的集合。
  • 根据商品ID查询商品信息:通过 findById 方法查询指定商品的详细信息。

SeckillService

用于处理秒杀相关操作的服务接口,提供了多个与秒杀活动相关的功能。该接口通过 Feign Client 与名为 goodskill-seckill 的微服务进行通信

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
/**
* 秒杀服务
*/
@FeignClient(value = "goodskill-seckill", contextId = "seckill")
public interface SeckillService {

/**
* 获取秒杀活动列表
*
* @param pageNum 页码
* @param pageSize 每页数量
* @param goodsName 商品名称,模糊匹配
* @return
*/
@GetMapping("/getSeckillList")
PageDTO<SeckillVO> getSeckillList(@RequestParam("pageNum") int pageNum,
@RequestParam("pageSize") int pageSize,
@RequestParam(value = "goodsName", required = false) String goodsName);


/**
* 暴露秒杀活动url
*
* @param seckillId 秒杀活动id
* @return 活动信息
*/
@PostMapping("/exportSeckillUrl")
ExposerDTO exportSeckillUrl(@RequestParam("seckillId") long seckillId);


/**
* 根据秒杀id删除成功记录
*
* @param seckillId 秒杀活动id
* @return 删除数量
*/
@DeleteMapping("/deleteSuccessKillRecord")
void deleteSuccessKillRecord(@RequestParam("seckillId") long seckillId);

/**
* 执行秒杀,通过同步来控制并发
*
* @param requestDto 秒杀请求
* @param strategyNumber 秒杀策略编码
*/
@PostMapping("/execute")
void execute(@RequestBody SeckillMockRequestDTO requestDto,
@RequestParam("strategyNumber") int strategyNumber);


/**
* 获取成功秒杀记录数
*
* @param seckillId 秒杀活动id
*/
@GetMapping("/getSuccessKillCount")
long getSuccessKillCount(@RequestParam("seckillId") Long seckillId);


/**
* 准备秒杀商品数量
*
* @param seckillId 秒杀商品id
* @param seckillCount 秒杀数量
* @param taskId 任务id
*/
@PostMapping("/prepareSeckill")
void prepareSeckill(@RequestParam("seckillId") Long seckillId,
@RequestParam("seckillCount") int seckillCount, @RequestParam("taskId") String taskId);


@GetMapping("/getById")
SeckillVO findById(@RequestParam("seckillId") Serializable seckillId);

@PostMapping("/saveOrUpdate")
boolean saveOrUpdateSeckill(@RequestBody SeckillVO seckill);

@DeleteMapping("/removeById")
boolean removeBySeckillId(@RequestParam("seckillId") Serializable seckillId);

@PostMapping("/save")
boolean save(@RequestBody SeckillVO seckill);

/**
* 减商品库存
*
* @param successKilled
* @return 1代表成功,小于1为失败
*/
@PostMapping("/reduceNumber")
int reduceNumber(@RequestBody SuccessKilledDTO successKilled);

@PostMapping("/reduceNumberInner")
int reduceNumberInner(@RequestBody SuccessKilledDTO successKilled);

/**
* 获取二维码
*
* @param fileName 二维码图片名称
* @return SeckillResponseDto
*/
@GetMapping("/getQrcode")
SeckillResponseDTO getQrcode(@RequestParam("fileName") String fileName) throws IOException;

/**
* 根据秒杀id获取秒杀活动信息
*
* @param seckillId
* @return
*/
@GetMapping("/getInfoById")
SeckillInfoDTO getInfoById(@RequestParam("seckillId") Serializable seckillId);


/**
* 结束秒杀操作
*
* @param seckillId 秒杀ID
* @return boolean 结束秒杀是否成功
*/
boolean endSeckill(@RequestParam("seckillId") Long seckillId);
}

SeckillService 接口是用于管理秒杀活动的一个服务接口,涵盖了秒杀活动的以下几个方面:

  • 秒杀活动管理:包括查询、添加、更新、删除秒杀活动
  • 秒杀商品管理:准备秒杀商品数量、减少商品库存等。
  • 秒杀操作:执行秒杀、获取成功秒杀记录、获取二维码等。
  • 秒杀统计:获取秒杀活动的成功秒杀记录数等。

3.2 goodskill-service

3.2.1 common包

  • CommonConstant 类: 是一个常量类,它的作用是统一管理项目中使用的 公共常量,主要用于消息队列(如 Spring Cloud Stream)中绑定通道(binding name)的定义
  • GoodsKillStrategyEnum 是一个 枚举类,用于表示秒杀系统中可选的多种策略实现方案,方便系统在运行时根据配置或参数选择不同的秒杀处理策略。秒杀策略枚举,通过枚举避免暴露具体方法
  • ResponseInfo 是一个 响应状态枚举类(Enum),用于统一定义后端接口的响应状态码和对应的信息,例如成功、失败、超时、服务器内部错误等
  • SuccessKilledServiceSuccessKilled 实体提供 基础的增删改查(CRUD)服务,通过继承 IService<T> 来实现。

RedisService

RedisService 类是一个用于操作秒杀相关 Redis 缓存的工具类,它封装了对 Redis 的读写逻辑,主要目的是提升查询效率、减轻数据库压力、标识秒杀状态等

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
@Component
@Slf4j
public class RedisService {
@Autowired
private SeckillMapper seckillMapper;
@Resource
private RedisTemplate<String, Object> redisTemplate;
@Resource
private StringRedisTemplate stringRedisTemplate;

//查询秒杀活动缓存(如果没有则查数据库并回填到 Redis)
public Seckill getSeckill(long seckillId) {
String key = "seckill:" + seckillId;
Seckill seckill = (Seckill) redisTemplate.opsForValue().get(key);
if (seckill != null) {
return seckill;
} else {
seckill = seckillMapper.selectById(seckillId);
if (seckill == null) {
throw new RuntimeException("秒杀活动不存在!");
}
putSeckill(seckill);
return seckill;
}
}

//将 Seckill 对象存入 Redis,过期时间是60秒
public void putSeckill(Seckill seckill) {
String key = "seckill:" + seckill.getSeckillId();
int timeout = 60;
redisTemplate.opsForValue().set(key, seckill, timeout, TimeUnit.SECONDS);
}

//删除 Redis 中的秒杀缓存
public void removeSeckill(long seckillId) {
String key = "seckill:" + seckillId;
redisTemplate.delete(key);
redisTemplate.delete(String.valueOf(seckillId));
}

//设置一个秒杀活动结束的标识位,返回值表示是否成功设置(第一次设置成功返回 true,之后返回 false)
//使用场景:防止多线程/多节点重复触发“秒杀结束”逻辑
public Boolean setSeckillEndFlag(long seckillId, String taskId) {
return stringRedisTemplate.opsForValue().setIfAbsent("goodskill:seckill:end:notice" + seckillId + ":" + taskId, "1");
}

//删除对应的“秒杀已结束”标志位
public Boolean clearSeckillEndFlag(long seckillId, String taskId) {
return stringRedisTemplate.delete("goodskill:seckill:end:notice" + seckillId + ":" + taskId);
}
}

主要职责

  • 查询秒杀活动缓存(如果没有则查数据库并回填到 Redis);
  • 写入/删除 Redis 缓存中的秒杀活动数据;
  • 管理秒杀是否结束的标识位(布尔值标识);
  • 封装 Redis 访问逻辑,避免业务代码中重复操作 Redis。

技术点总结

  • 使用了 Redis 的过期机制和 setIfAbsent 机制,确保高并发下的操作一致性
  • 缓存穿透控制:缓存未命中时自动加载数据库并回填,防止大量请求直击数据库。

3.2.2 config包

StateMachineConfig

用于控制“秒杀活动状态机”逻辑的核心配置类,它基于 Spring State Machine 状态机框架,通过配置状态、事件、状态转换规则、监听器、持久化机制来驱动一个流程(比如:秒杀活动的生命周期)按照预期的步骤运行。

目前秒杀活动状态的控制基于Spring Statemachine状态机实现,使用状态机的优点:

  • 统一控制活动状态,便于状态的集中维护;
  • 防止业务状态被随意更改,保证状态的可控更新;

  • StateMachineConfig 配置了 秒杀活动的完整生命周期状态机,包括状态、事件、状态转换逻辑、监听器和 Redis 持久化支持,是活动流程控制的核心配置。

ThreadPoolConfig

线程池配置类,用于在 Spring 容器中注册一个 ThreadPoolExecutor 类型的 Bean,供项目中的异步任务或并发操作复用。
提供一个可复用的线程池 taskExecutor,用来统一管理项目中的并发任务,比如执行秒杀异步逻辑、发送MQ、处理回调、记录日志等。

1
2
3
4
5
6
7
8
9
10
11
/**
* 线程池配置
*/
@Configuration
public class ThreadPoolConfig {
@Bean
public ThreadPoolExecutor taskExecutor() {
return new ThreadPoolExecutor(2, 10, 1, TimeUnit.MINUTES,
new LinkedBlockingDeque<>(100), new ThreadPoolExecutor.CallerRunsPolicy());
}
}
参数 含义
corePoolSize = 2 核心线程数:即使空闲也会保留 2 个线程
maximumPoolSize = 10 最大线程数上限为 10,超过就进入队列或拒绝
keepAliveTime = 1 分钟 非核心线程闲置 1 分钟后会被销毁
LinkedBlockingDeque<>(100) 队列最大缓存任务数是 100
CallerRunsPolicy 拒绝策略:当线程和队列都满了,任务由提交任务的线程自己执行(不会丢任务)

3.2.3 Controller层

SeckillGraphqlController

基于 GraphQL 的控制器:为前端 GraphQL 查询提供数据解析能力,通过注解驱动的方式,将不同的 GraphQL 查询或字段解析,映射到后端的服务接口上,方便地查询秒杀活动及其关联信息(如商品信息、成功秒杀记录等)。
SeckillGraphqlController 是一个 GraphQL 控制器类,用于处理前端 GraphQL 查询请求,负责从后端服务中查询秒杀活动、关联商品信息、成功秒杀记录等,并将它们绑定到对应 GraphQL 的字段和查询结构中。

GoodsController

GoodsController 类是一个基于 Spring Boot 的 RESTful 控制器类,主要用于商品管理模块中的订单统计功能,通过 HTTP 接口对外提供服务。

提供一个用于查询某个秒杀活动下订单数量的接口,便于统计参与某商品秒杀的用户数量或下单数量。

1
2
3
4
5
6
7
8
9
10
11
12
13
@Tag(name = "商品管理")
@RestController
@RequestMapping("/seckill/goods")
public class GoodsController {
@Resource
private OrderService orderService;

//查询订单数量
@GetMapping("/orders/count")
public Long countOrders(@RequestParam("seckillId") long seckillId) {
return orderService.count(seckillId);
}
}

SeckillController

秒杀管理控制器类: 用于处理与 商品秒杀活动相关的页面跳转、数据接口和业务操作。它结合了传统 MVC 的页面跳转和 REST 接口,支持页面渲染、数据分页、创建/编辑/删除秒杀活动、二维码支付、上传图片、商品搜索等功能。

SeckillController 方法功能表:

请求方式 请求路径 方法名 描述
GET /seckill/list list 分页展示秒杀活动列表,支持商品名称搜索
GET /seckill/{seckillId}/detail detail 查看指定秒杀活动的详情
POST /seckill/{seckillId}/exposer exposer 获取秒杀地址(接口暴露)
POST /seckill/create addSeckill 添加一个新的秒杀活动
GET /seckill/new toAddSeckillPage 跳转到新建秒杀活动的页面
GET /seckill/{seckillId}/delete delete 删除某个秒杀活动
GET /seckill/{seckillId}/edit edit 跳转到编辑页面,加载当前活动信息
POST /seckill/{seckillId}/update update 更新秒杀活动信息
GET /seckill/pay/Qrcode/{QRfilePath} payTransaction 跳转到支付宝二维码支付页面
POST /seckill/upload/{seckillId}/create uploadPhoto 上传秒杀商品图片,更新商品图地址
GET /seckill/goods/search/{goodsName} searchGoods 通过商品名模糊搜索(基于ES),返回高亮搜索结果

3.2.4 entity包

  • Goods 类是一个实体类,表示系统中的商品对象,在秒杀系统中用于存储和管理商品的相关信息
  • Seckill 类是一个实体类,表示系统中的秒杀库存活动对象,主要用于描述一个特定商品的秒杀活动的基本信息,包括时间、库存、价格等内容
  • SuccessKilled 类是一个实体类,表示秒杀成功记录的对象,用于存储用户参与秒杀活动并成功购买商品的记录

3.2.5 es包(Elasticsearch)

  • Goods 类是一个 Elasticsearch 实体类,用于表示商品信息,并且该类通过 @Document 注解与 Elasticsearch 中的 goods 索引进行映射。

GoodsRepository 接口是一个用于操作 Elasticsearch 数据库的 Spring Data Elasticsearch 仓库接口。它继承自 ElasticsearchRepository,这是 Spring Data Elasticsearch 提供的一个基础接口,提供了常用的 CRUD(创建、读取、更新、删除)操作

1
2
3
4
5
public interface GoodsRepository extends ElasticsearchRepository<Goods, String> {

//根据 ID 删除 Goods 实体
void deleteByGoodsId(Integer goodsId);
}

3.2.6 handler包

AbstractPreRequestHandler 抽象类

抽象基类,提供预处理器统一接口和执行顺序控制
用于请求前的统一处理流程(如校验、限流、日志、鉴权等)

1
2
3
4
5
public abstract class AbstractPreRequestHandler implements PreRequestHandler, Ordered {

@Override
public abstract void handle(SeckillWebMockRequestDTO request);
}

DatabasePreRequestHandler

DatabasePreRequestHandler 是一个用于 数据库初始化/重置的请求预处理器,在某些业务场景中(如模拟秒杀环境或初始化测试数据),该处理器会:

  • 重置指定秒杀活动的库存数量
  • 清空该秒杀活动下的已成功秒杀记录
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
@Component
public class DatabasePreRequestHandler extends AbstractPreRequestHandler {
@Resource
private SeckillMapper seckillMapper;
@Resource
private SuccessKilledMapper successKilledMapper;
// 更新库存 将传入的秒杀商品 ID 的库存数量设置为指定数量
// 删除记录 清除该商品 ID 所有的成功秒杀记录(例如用于测试或重置)
@Override
public void handle(SeckillWebMockRequestDTO request) {
Seckill entity = new Seckill();
entity.setSeckillId(request.getSeckillId());
entity.setNumber(request.getSeckillCount());
seckillMapper.updateById(entity);

// 清理已成功秒杀记录
SuccessKilled example = new SuccessKilled();
example.setSeckillId(request.getSeckillId());
successKilledMapper.delete(new QueryWrapper<>(example));
}

@Override
public int getOrder() {
return 0;
}
}

MongoPreRequestHandler

MongoPreRequestHandler 类的作用是:在执行秒杀压测或测试之前,清空 MongoDB 中该秒杀活动的相关订单记录,以保证测试数据干净、不会受到历史数据影响。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@Component
@Slf4j
public class MongoPreRequestHandler extends AbstractPreRequestHandler {
@Resource
private OrderService orderService;

//从 MongoDB 中删除该活动的所有订单数据
@Override
public void handle(SeckillWebMockRequestDTO request) {
orderService.deleteRecord(request.getSeckillId());
}

//责任链中执行顺序排在后面(值越大越后执行)
@Override
public int getOrder() {
return 3;
}
}

PreRequestHandler 接口

定义一个统一的“秒杀请求预处理”操作规范

1
2
3
4
5
6
7
/**
* 请求处理类
*/
public interface PreRequestHandler {

void handle(SeckillWebMockRequestDTO request);
}

PreRequestPipeline

统一调度和执行所有预处理器(PreRequestHandler)的逻辑,按照优先级顺序依次执行每个预处理步骤。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@Component
@Slf4j
@Setter
public class PreRequestPipeline {
@Resource
private List<AbstractPreRequestHandler> prePreRequestHandlers;

public void handle(SeckillWebMockRequestDTO request) {
prePreRequestHandlers.stream().sorted(Comparator.comparing(Ordered::getOrder))
.forEach(it -> {
try {
it.handle(request);
} catch (Exception e) {
log.warn("pre request handler error", e);
}
});
}
}
  • 入参是秒杀请求的数据对象 SeckillWebMockRequestDTO,包含诸如秒杀 ID、秒杀库存等信息。
  • 会遍历所有注册的 AbstractPreRequestHandler 实现类,按照 getOrder() 指定的顺序依次调用 handle(request) 方法。
  • 如果某个处理器出错,捕获异常并记录日志,继续执行后面的处理器,不中断整个流程

RedisPreRequestHandler

RedisPreRequestHandler 类是一个预处理器组件,其作用是在执行秒杀前,清理和初始化 Redis 中与秒杀活动相关的缓存数据,以确保 Redis 缓存环境处于干净且正确的状态。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@Component
public class RedisPreRequestHandler extends AbstractPreRequestHandler {
@Resource
private RedisService redisService;

@Override
public int getOrder() {
return 1;
}

@Override
public void handle(SeckillWebMockRequestDTO request) {
long seckillId = request.getSeckillId();
redisService.removeSeckill(seckillId);
Seckill seckill = redisService.getSeckill(seckillId);
seckill.setStatus(SeckillStatusConstant.IN_PROGRESS);
redisService.putSeckill(seckill);
redisService.clearSeckillEndFlag(seckillId, request.getTaskId());
}
}

StateMachinePreRequestHandler

这个 StateMachinePreRequestHandler 类是一个预处理器组件,用于在秒杀活动开始前,通过状态机服务来初始化和管理秒杀活动的状态流转,是预处理责任链中的第三步

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
@Component
public class StateMachinePreRequestHandler extends AbstractPreRequestHandler {
@Resource
private StateMachineService stateMachineService;

@Override
public void handle(SeckillWebMockRequestDTO request) {
// 状态机初始化
stateMachineService.initStateMachine(request.getSeckillId());
// 初始化库存数量
// 使用状态机控制活动状态
if (!stateMachineService.feedMachine(Events.ACTIVITY_RESET, request.getSeckillId())) {
throw new RuntimeException("活动尚未结束,请等待活动结束后再次操作");
}
stateMachineService.feedMachine(Events.ACTIVITY_START, request.getSeckillId());
}

@Override
public int getOrder() {
return 2;
}
}

Handler的作用

秒杀系统的特点是高并发、数据敏感、状态复杂,所以在每次活动开始前,需要完成一系列的准备工作
这些 Handler 就像“启动前检查清单”,每个负责一个环节,从数据、缓存、状态、历史记录等多个维度做好准备,确保秒杀系统在高并发下也能稳定运行。
假设你运行一个新的秒杀模拟任务,流程如下:

顺序 处理器 功能
0 DatabasePreRequestHandler 重置数据库库存,清理订单记录
1 RedisPreRequestHandler 清除缓存、设置为活动进行中
2 StateMachinePreRequestHandler 初始化状态机、检测是否可重启、启动活动状态
3 MongoPreRequestHandler 清除 Mongo 中的订单记录

3.2.7 impl实现类

GoodsServiceImpl

GoodsServiceImpl 是一个商品服务的实现类,属于秒杀系统中的商品模块,负责商品的基本信息管理,包括数据库操作、ES(Elasticsearch)索引同步、图片上传等

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
@DubboService
@Slf4j
public class GoodsServiceImpl extends ServiceImpl<GoodsMapper, Goods> implements GoodsService {
@Resource
private GoodsEsService goodsEsService;
@Resource
private GoodsMapper baseMapper;

//上传并更新商品图片链接
@Override
public void uploadGoodsPhoto(long goodsId, String fileUrl) {
Goods goods = new Goods();
goods.setGoodsId((int) goodsId);
goods.setPhotoUrl(fileUrl);
log.info(goods.toString());
baseMapper.updateById(goods);
}

//添加新商品(含ES同步)
@Override
@Transactional(rollbackFor = Exception.class)
public void addGoods(GoodsVO goods) {
baseMapper.insert(BeanUtil.copyProperties(goods, Goods.class));
GoodsDTO goodsDto = new GoodsDTO();
goodsDto.setId(IdWorker.getId());
BeanUtils.copyProperties(goods, goodsDto);
try {
goodsEsService.save(goodsDto);
} catch (Exception e) {
log.warn("es服务不可用,请检查!", e);
}
}

//根据商品 ID 查询商品信息
@Override
public GoodsVO findById(Serializable id) {
Goods byId = super.getById(id);
return BeanUtil.copyProperties(byId, GoodsVO.class);
}

//查询所有商品
@Override
public List<GoodsVO> findMany() {
return BeanUtil.copyToList(super.list(), GoodsVO.class);
}
}
方法名 方法作用 说明
uploadGoodsPhoto(long goodsId, String fileUrl) 上传并更新商品图片链接 将上传好的图片 URL 写入数据库中对应的商品记录里。
addGoods(GoodsVO goods) 添加新商品(含ES同步) - 将商品信息插入数据库;
- 将商品信息构造成 GoodsDTO 对象同步到 Elasticsearch;
- 若 ES 出错则记录警告日志,不中断事务。
findById(Serializable id) 根据商品 ID 查询商品信息 从数据库查询商品记录并转换为前端用的 GoodsVO 对象返回。
findMany() 查询所有商品 查询所有商品记录并封装成 VO 列表。

SeckillServiceImpl

秒杀服务的实现类,继承了 ServiceImpl<SeckillMapper, Seckill>,并实现了 SeckillService 接口。这个类集成了 Dubbo、Redis、MongoDB、线程池、状态机、预处理流水线等组件,功能非常丰富,核心作用是负责“秒杀活动”的业务逻辑处理,包括活动准备、秒杀执行、二维码生成、秒杀记录管理等

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
@Slf4j
@RestController
@DubboService
public class SeckillServiceImpl extends ServiceImpl<SeckillMapper, Seckill> implements SeckillService {
//分页获取秒杀活动列表,支持商品名模糊查询,查询结果缓存进 Redis,避免频繁 DB 查询
@Override
public PageDTO<SeckillVO> getSeckillList(int pageNum, int pageSize, String goodsName) {
String key = "seckill:list:" + pageNum + ":" + pageSize + ":" + goodsName;
ValueOperations valueOperations = redisTemplate.opsForValue();
Object pageCache = valueOperations.get(key);
PageDTO<SeckillVO> page;
if (Objects.isNull(pageCache)) {
QueryWrapper<Seckill> queryWrapper = new QueryWrapper<>();
if (StringUtils.isNotBlank(goodsName)) {
queryWrapper.lambda().like(Seckill::getName, goodsName);
}
Page<Seckill> seckillPage = baseMapper.selectPage(new Page(pageNum, pageSize), queryWrapper);
List<SeckillVO> collect = seckillPage.getRecords().stream().map(it -> {
SeckillVO seckillVO = new SeckillVO();
BeanUtils.copyProperties(it, seckillVO);
GoodsVO byId = goodsService.findById(it.getGoodsId());
if (Objects.nonNull(byId)) {
String photoUrl = byId.getPhotoUrl();
seckillVO.setPhotoUrl(photoUrl);
}
return seckillVO;
}).collect(Collectors.toList());
page = new PageDTO<>();
page.setCurrent(pageNum);
page.setSize(pageSize);
page.setTotal(seckillPage.getTotal());
page.setRecords(collect);
valueOperations.set(key, page, 5, TimeUnit.MINUTES);
} else {
page = (PageDTO<SeckillVO>) pageCache;
}
return page;
}

//暴露秒杀地址,用于验证活动是否在有效期内,并生成一个带 MD5 的加密链接。
@Override
public ExposerDTO exportSeckillUrl(long seckillId) {
//从redis中获取缓存秒杀信息
Seckill seckill = redisService.getSeckill(seckillId);
Date startTime = seckill.getStartTime();
Date endTime = seckill.getEndTime();
Date nowTime = new Date();
if (nowTime.getTime() < startTime.getTime() || nowTime.getTime() > endTime.getTime()) {
return new ExposerDTO(false, seckillId, nowTime.getTime(), startTime.getTime(), endTime.getTime());
}
String md5 = MD5Util.getMD5(seckillId);
return new ExposerDTO(true, md5, seckillId);
}

//删除某个秒杀活动的所有成功记录(操作数据库)
@Override
public void deleteSuccessKillRecord(long seckillId) {
SuccessKilled example = new SuccessKilled();
example.setSeckillId(seckillId);
successKilledMapper.delete(new QueryWrapper<>(example));
}

//根据策略编号选择对应的秒杀策略执行器,执行秒杀逻辑(策略模式)
@Override
public void execute(SeckillMockRequestDTO requestDto, int strategyNumber) {
goodskillStrategies.stream()
.filter(n -> n.getClass().getName().startsWith(Objects.requireNonNull(GoodsKillStrategyEnum.stateOf(strategyNumber)).getClassName()))
.findFirst().ifPresent(n -> n.execute(requestDto));
}

//获取秒杀成功的记录数,优先查数据库,没有再查 MongoDB。
@Override
public long getSuccessKillCount(Long seckillId) {
SuccessKilled example = new SuccessKilled();
example.setSeckillId(seckillId);
long count = successKilledMapper.selectCount(new QueryWrapper<>(example));
if (count == 0) {
try {
count = orderService.count(seckillId);
} catch (Exception e) {
log.error("mongo服务不可用,请检查!", e);
throw e;
}
}
return count;
}

//准备秒杀活动的资源(库存、状态、缓存等),调用预处理流水线中的多个 Handler 执行。
@Override
public void prepareSeckill(Long seckillId, int seckillCount, String taskId) {
SeckillWebMockRequestDTO request = new SeckillWebMockRequestDTO();
request.setSeckillId(seckillId);
request.setSeckillCount(seckillCount);
request.setTaskId(taskId);
preRequestPipeline.handle(request);
}

//减库存的外层方法,调用 reduceNumberInner。捕获异常避免中断流程。
@Override
public int reduceNumber(SuccessKilledDTO successKilled) {
int count = 0;
try {
count = seckillService.reduceNumberInner(successKilled);
} catch (Exception e) {
log.warn(e.getMessage());
}
return count;
}

//真正减库存:插入秒杀记录,然后更新库存(带条件限制)。更新失败会抛出“活动已结束”异常。
@Transactional(rollbackFor = Exception.class)
@Override
public int reduceNumberInner(SuccessKilledDTO successKilled) {
SuccessKilled entity = BeanUtil.copyProperties(successKilled, SuccessKilled.class);
successKilledMapper.insert(entity);
Seckill wrapper = new Seckill();
wrapper.setSeckillId(entity.getSeckillId());
UpdateWrapper<Seckill> updateWrapper = new UpdateWrapper(wrapper);
updateWrapper.gt("end_time", entity.getCreateTime());
updateWrapper.lt("start_time", entity.getCreateTime());
updateWrapper.gt("number", 0);
updateWrapper.setSql("number = number - 1");
int update = baseMapper.update(null, updateWrapper);
if (update <= 0) {
throw new SeckillCloseException("seckill is closed");
} else {
return update;
}
}

//根据文件名读取本地二维码图片并返回字节流,通常用于订单支付
@Override
public SeckillResponseDTO getQrcode(String fileName) throws IOException {
SeckillResponseDTO seckillResponseDto = new SeckillResponseDTO();
if (qrcodeImagePath == null) {
qrcodeImagePath = System.getProperty("user.dir");
}
FileInputStream inputStream = new FileInputStream(qrcodeImagePath + "/" + fileName + ".png");
int b;
// 二维码一般不超过10KB
byte[] data = new byte[1024 * 10];
int i = 0;
while ((b = inputStream.read()) != -1) {
data[i] = (byte) b;
i++;
}
seckillResponseDto.setData(data);
return seckillResponseDto;
}

//查询秒杀活动的详细信息,包括绑定的商品信息。
@Override
public SeckillInfoDTO getInfoById(Serializable seckillId) {
SeckillInfoDTO seckillInfoDTO = new SeckillInfoDTO();
SeckillVO seckill = seckillService.findById(seckillId);
GoodsVO goods = goodsService.findById(seckill.getGoodsId());
BeanUtils.copyProperties(seckill, seckillInfoDTO);
seckillInfoDTO.setGoodsName(goods.getName());
return seckillInfoDTO;
}

//通过状态机结束秒杀活动(触发状态变迁)。
@Override
public boolean endSeckill(Long seckillId) {
return stateMachineService.feedMachine(Events.ACTIVITY_END, seckillId);
}

//删除某个秒杀活动。
@Override
public boolean removeBySeckillId(Serializable id) {
return super.removeById(id);
}

//新建秒杀活动记录。
@Override
public boolean save(SeckillVO seckill) {
return baseMapper.insert(BeanUtil.copyProperties(seckill, Seckill.class)) > 0;
}

//根据 ID 获取秒杀活动 VO 对象(封装展示数据)。
@Override
public SeckillVO findById(Serializable id) {
return BeanUtil.copyProperties(super.getById(id), SeckillVO.class);
}

//更新或新增秒杀活动数据(MyBatis Plus 封装方法)
@Override
@Transactional(rollbackFor = Exception.class)
public boolean saveOrUpdateSeckill(SeckillVO seckill) {
return saveOrUpdate(BeanUtil.copyProperties(seckill, Seckill.class));
}
}
方法名 作用说明
getSeckillList 分页获取秒杀活动列表,支持商品名模糊查询,查询结果缓存进 Redis,避免频繁 DB 查询。
exportSeckillUrl 暴露秒杀地址,用于验证活动是否在有效期内,并生成一个带 MD5 的加密链接。
deleteSuccessKillRecord 删除某个秒杀活动的所有成功记录(操作数据库)。
execute 根据策略编号选择对应的秒杀策略执行器,执行秒杀逻辑(策略模式)。
getSuccessKillCount 获取秒杀成功的记录数,优先查数据库,没有再查 MongoDB。
prepareSeckill 准备秒杀活动的资源(库存、状态、缓存等),调用预处理流水线中的多个 Handler 执行。
reduceNumber 减库存的外层方法,调用 reduceNumberInner。捕获异常避免中断流程。
reduceNumberInner 真正减库存:插入秒杀记录,然后更新库存(带条件限制)。更新失败会抛出“活动已结束”异常。
getQrcode 根据文件名读取本地二维码图片并返回字节流,通常用于订单支付。
getInfoById 查询秒杀活动的详细信息,包括绑定的商品信息。
endSeckill 通过状态机结束秒杀活动(触发状态变迁)。
removeBySeckillId 删除某个秒杀活动。
save 新建秒杀活动记录。
findById 根据 ID 获取秒杀活动 VO 对象(封装展示数据)。
saveOrUpdateSeckill 更新或新增秒杀活动数据(MyBatis Plus 封装方法)。

GoodsEsServiceImpl

是一个基于Elasticsearch 的服务实现类,负责对 商品信息进行 Elasticsearch 的增删查操作,提供了与搜索相关的功能,适用于电商类系统中的商品搜索模块。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
@RestController
@Service
public class GoodsEsServiceImpl implements GoodsEsService {
private final BeanCopier beanCopier = BeanCopier.create(GoodsDTO.class, Goods.class, false);
@Autowired
private ElasticsearchOperations elasticsearchOperations;
@Autowired
private GoodsRepository goodsRepository;

//将单个商品保存到 Elasticsearch
@Override
public void save(GoodsDTO goodsDto) {
Goods goods = new Goods();
beanCopier.copy(goodsDto, goods, null);
goodsRepository.save(goods);
}

//批量保存商品信息到 Elasticsearch
@Override
public void saveBatch(List<GoodsDTO> list) {
List<Goods> collect = list.stream().map(dto -> {
Goods goods = new Goods();
beanCopier.copy(dto, goods, null);
return goods;
}).collect(Collectors.toList());
goodsRepository.saveAll(collect);
}

//删除某个商品的 ES 索引文档
@Override
public void delete(GoodsDTO goodsDto) {
goodsRepository.deleteByGoodsId(goodsDto.getGoodsId());
}

//按商品名模糊匹配搜索商品(最多返回3条),并高亮搜索关键字
@Override
public List<GoodsDTO> searchWithNameByPage(String input) {
Criteria criteria = new Criteria("name").matches(input);
Query query = new CriteriaQuery(criteria);

HighlightFieldParameters parameters = HighlightFieldParameters.builder()
.withPostTags(new String[]{"</font>"})
.withPreTags(new String[]{"<font color='red'>"})
.build();
HighlightField highlightField = new HighlightField("name", parameters);
Highlight highlight = new Highlight(HighlightParameters.builder().build(), List.of(highlightField));
query.setHighlightQuery(new HighlightQuery(highlight, null));
query.setPageable(PageRequest.of(0, 3));
return elasticsearchOperations.search(query, Goods.class)
.getSearchHits().stream().map(s -> {
Goods goods = s.getContent();
GoodsDTO goodsDto = new GoodsDTO();
goodsDto.setName(s.getHighlightField("name").getFirst());
goodsDto.setRawName(goods.getName());
return goodsDto;
}).collect(Collectors.toList());
}
}
  • 将商品信息 GoodsDTO 同步保存到 Elasticsearch。
  • 支持批量保存、删除。
  • 提供商品名称的搜索功能,带 关键字高亮
  • 作为系统中搜索服务的实现类,适用于用户输入商品名称关键字时的联想搜索/模糊搜索。

SuccessKilledServiceImpl

实现了 SuccessKilledService 接口,并继承了 MyBatis-Plus 提供的 ServiceImpl<SuccessKilledMapper, SuccessKilled>,因此 自动继承了大部分 CRUD 方法

1
2
3
4
@Service
public class SuccessKilledServiceImpl extends ServiceImpl<SuccessKilledMapper, SuccessKilled> implements SuccessKilledService {

}

3.2.8 inner包

接口 SeckillExecutor

SeckillExecutor 是一个定义了标准秒杀处理方法的接口,为整个秒杀系统中不同的执行策略提供统一的扩展入口

1
2
3
4
5
6
7
public interface SeckillExecutor {

/**
* 秒杀内部处理方法
*/
void dealSeckill(long seckillId, String userPhone, String note, String taskId);
}

实现类 SeckillProcedureExecutor

是 SeckillExecutor 接口的一个具体实现,用于处理用户发起的秒杀请求

这是一个基于数据库存储过程/乐观锁逻辑的秒杀执行器实现类,主要职责是:

  • 尝试为用户执行一次秒杀操作
  • 在库存售罄时,触发状态机变更、通知下游系统(如通过 StreamBridge 发消息)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
@Slf4j
@Service
public class SeckillProcedureExecutor implements SeckillExecutor {
/**
* 处理用户秒杀请求
*
* @param seckillId 秒杀活动id
* @param userPhone 秒杀用户手机号
* @param note 秒杀备注信息
* @param taskId 秒杀任务id
*/
@Override
public void dealSeckill(long seckillId, String userPhone, String note, String taskId) {
try {
InetAddress localHost = InetAddress.getLocalHost();
SuccessKilledDTO successKilled = new SuccessKilledDTO();
successKilled.setSeckillId(seckillId);
successKilled.setUserPhone(userPhone);
successKilled.setCreateTime(new Date());
successKilled.setServerIp(localHost.getHostAddress() + ":" + localHost.getHostName());
if (seckillService.reduceNumber(successKilled) < 1) {
Seckill seckill = seckillMapper.selectById(seckillId);
log.debug("#dealSeckill 当前库存:{},秒杀活动id:{},商品id:{}", seckill.getNumber(), seckill.getSeckillId(), seckill.getGoodsId());
if (stateMachineService.checkState(seckillId, States.IN_PROGRESS)) {
stateMachineService.feedMachine(Events.ACTIVITY_CALCULATE, seckillId);
// 高并发时可能多次发送完成通知,使用锁控制
Boolean endFlag = redisService.setSeckillEndFlag(seckillId, taskId);
if (endFlag) {
streamBridge.send(DEFAULT_BINDING_NAME, MessageBuilder.withPayload(
SeckillMockResponseDTO.builder().seckillId(seckillId).note(note).status(true).taskId(taskId).build())
.build());
log.info("#dealSeckill 商品已售罄,最新秒杀信息:{}", seckill);
}
}
if (seckill.getNumber() <= 0) {
log.debug("#dealSeckill 库存不足,无法继续秒杀!");
}
}
} catch (UnknownHostException e) {
log.error(e.getMessage(), e);
}
}
}

3.2.9 mapper层

这三个 Mapper 接口是基于 MyBatis-Plus 的数据访问接口,它们分别对应数据库中的三个核心表:商品表、秒杀活动表、秒杀成功记录表

  • GoodsMapper:操作商品表 goods 的数据库接口
  • SeckillMapper:操作秒杀活动表 seckill 的数据库接口
    • 扩展了两个用于库存扣减的自定义 SQL 方法:
      • 普通的 SQL 扣减库存语句
      • 加入了 乐观锁控制的扣减方式(通过传入 number 保证版本一致性)
  • SuccessKilledMapper:操作秒杀成功记录表 success_killed 的数据库接口

3.2.10 mock.strategy

GoodsKillStrategy

接口 GoodsKillStrategy 是一个 秒杀策略接口,用于支持策略模式,目的是根据不同的策略,执行不同的“秒杀处理逻辑”。

  • 定义了一个统一的秒杀处理方法 execute(…)
1
2
3
4
5
6
7
8
9
10
11
12
/**
* 秒杀策略接口
*/
public interface GoodsKillStrategy {

/**
* 执行秒杀
*
* @param requestDto 秒杀请求
*/
void execute(SeckillMockRequestDTO requestDto);
}

策略1:AtomicUpdateStrategy原子更新策略

实现了秒杀策略接口 GoodsKillStrategy 的一种具体策略实现,属于原子更新策略(Atomic Update)

1
2
3
4
5
6
7
8
9
10
11
@Component
@Slf4j
public class AtomicUpdateStrategy implements GoodsKillStrategy {
@Autowired
private SeckillExecutor seckillExecutor;

@Override
public void execute(SeckillMockRequestDTO requestDto) {
seckillExecutor.dealSeckill(requestDto.getSeckillId(), requestDto.getPhoneNumber(), ATOMIC_UPDATE.getName(), requestDto.getTaskId());
}
}

AtomicUpdateStrategy 更强调:

  • 使用 数据库乐观锁机制 或 原子性更新语句(如 UPDATE … SET number = number - 1 WHERE number > 0)
  • 来保障高并发环境下的数据一致性与库存安全

策略2:AtomicWithCanalStrategy

AtomicWithCanalStrategy 是一个秒杀策略的具体实现类,结合了数据库原子性更新操作与**Canal监听 binlog(数据库变更日志)**的架构思路,来完成高并发秒杀的处理。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
@Component
@Slf4j
public class AtomicWithCanalStrategy implements GoodsKillStrategy {

@Resource
private SeckillMapper seckillMapper;
@Resource
private SuccessKilledMapper successKilledMapper;
@Resource
private StateMachineService stateMachineService;

private final ConcurrentHashMap<Long, Object> seckillIdList = new ConcurrentHashMap<>();

@Override
public void execute(SeckillMockRequestDTO requestDto) {
Long seckillId = requestDto.getSeckillId();
String userId = requestDto.getPhoneNumber();
// 此处已优化,锁单个商品而不是整个方法
Object seckillMonitor = seckillIdList.get(seckillId);
if (seckillMonitor == null) {
Object value = new Object();
seckillIdList.put(seckillId, value);
}
synchronized (seckillIdList.get(seckillId)) {
Seckill seckill = seckillMapper.selectById(seckillId);
if (seckill.getNumber() > 0) {
seckillMapper.reduceNumber(seckillId, new Date());
SuccessKilled record = new SuccessKilled();
record.setSeckillId(seckillId);
record.setUserPhone(String.valueOf(userId));
record.setStatus(1);
record.setCreateTime(new Date());
successKilledMapper.insert(record);
} else {
stateMachineService.feedMachine(Events.ACTIVITY_CALCULATE, seckillId);
if (log.isDebugEnabled()) {
log.debug("#execute 库存不足,无法继续秒杀!");
}
}
}
}
}

在秒杀时:

  1. 利用 数据库层的原子更新操作(SQL语句直接减少库存) 来防止超卖。
  2. 利用 局部同步锁(按商品 ID 加锁) 来控制并发。
  3. 将用户秒杀成功信息插入数据库(后续可通过 Canal 监听该表的 binlog 进行异步处理,比如发放优惠券、通知用户等)。
  4. 在库存耗尽时,通知状态机进入“结束计算”状态。

策略3:RedisMongoReactiveStrategy

RedisMongoReactiveStrategy 类的策略是处理秒杀请求的异步逻辑。它主要通过以下几个步骤:

  • 异步执行秒杀请求。
  • 使用 Redis 来判断秒杀商品库存,并进行增量操作。
  • 若秒杀成功,发送消息到消息中间件,进行 MongoDB 保存操作。
  • 若秒杀失败(库存不足),通过状态机检查活动状态,并发送秒杀结束的消息。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63

@Component
@Slf4j
public class RedisMongoReactiveStrategy implements GoodsKillStrategy {
@Resource
private RedisService redisService;
@Resource
private RedisTemplate<String, Object> redisTemplate;
@Resource(name = "taskExecutor")
private ThreadPoolExecutor taskExecutor;
@Resource
private StreamBridge streamBridge;
@Resource
private StateMachineService stateMachineService;
//当收到秒杀请求时,它会将请求交给线程池 taskExecutor 异步执行。
@Override
public void execute(SeckillMockRequestDTO requestDto) {
taskExecutor.execute(() -> {
try {
doExecute(requestDto);
} catch (Exception e) {
log.error("秒杀失败", e);
}
});
}

//执行秒杀逻辑的主要方法
private void doExecute(SeckillMockRequestDTO requestDto) {
long seckillId = requestDto.getSeckillId();
Seckill seckill = redisService.getSeckill(seckillId);
if (redisTemplate.opsForValue().increment(String.valueOf(seckillId)) <= seckill.getNumber()) {
taskExecutor.execute(() ->
streamBridge.send(DEFAULT_BINDING_NAME_MONGO_SAVE, MessageBuilder.withPayload(
SeckillMockSaveVo
.builder()
.seckillId(seckillId)
.userPhone(requestDto.getPhoneNumber())
.note(REDIS_MONGO_REACTIVE.getName())
.build())
.build())
);
} else {
synchronized (this) {
seckill = redisService.getSeckill(seckillId);
if (stateMachineService.checkState(seckillId, States.IN_PROGRESS)) {
stateMachineService.feedMachine(Events.ACTIVITY_CALCULATE, seckillId);
log.info("秒杀商品暂无库存,发送活动结束消息!");
streamBridge.send(DEFAULT_BINDING_NAME, MessageBuilder.withPayload(
SeckillMockResponseDTO
.builder()
.status(true)
.seckillId(seckillId)
.note(REDIS_MONGO_REACTIVE.getName())
.taskId(requestDto.getTaskId())
.build())
.build());
seckill.setStatus(SeckillStatusConstant.END);
redisService.putSeckill(seckill);
}
}
}
}
}

策略4:RedissonStrategy

通过分布式锁 Redisson 保证在高并发下对同一个商品的秒杀操作是线程安全的,避免超卖问题

  • RedissonStrategy 是一种 基于 Redisson 分布式锁的秒杀策略,它通过给每个秒杀商品加锁来确保同一时间只能有一个线程处理某个商品的秒杀逻辑,从而实现并发控制和库存一致性。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@Component
@Slf4j
public class RedissonStrategy implements GoodsKillStrategy {
@Autowired
private RedissonClient redissonClient;
@Autowired
private SeckillExecutor seckillExecutor;

/*
为当前商品获取一个分布式锁(锁名为商品 ID 的字符串)。
使用的是 可重入锁 RLock,多个节点下的并发用户都会竞争这个锁,确保同一时间只有一个线程能执行下面的逻辑。
*/
@Override
public void execute(SeckillMockRequestDTO requestDto) {
RLock lock = redissonClient.getLock(requestDto.getSeckillId() + "");
lock.lock();
try {
seckillExecutor.dealSeckill(requestDto.getSeckillId(), requestDto.getPhoneNumber(), REDISSION_LOCK.getName(), requestDto.getTaskId());
} finally {
lock.unlock();
}
}
}

策略5:SentinelLimitStrategy

SentinelLimitStrategy 是一个基于 Sentinel 的限流策略,用于在高并发场景下保护系统资源,防止秒杀接口被过载调用导致系统崩溃。它是 GoodsKillStrategy 接口的一个实现,核心是利用 Sentinel 做流量控制(限流)

  • 通过 Sentinel 实现限流保护机制,限制每秒进入秒杀系统的请求数量;
  • 一旦请求超过阈值,Sentinel 会触发 blockHandler 方法,进行降级处理,而不会继续执行秒杀逻辑;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@Component
@Slf4j
public class SentinelLimitStrategy implements GoodsKillStrategy {
@Autowired
private SeckillExecutor seckillExecutor;

@Override
@SentinelResource(value = "limit", blockHandler = "exceptionHandler")
public void execute(SeckillMockRequestDTO requestDto) {
seckillExecutor.dealSeckill(requestDto.getSeckillId(), requestDto.getPhoneNumber(), ATOMIC_UPDATE.getName(), requestDto.getTaskId());
}

// 相当于一种“熔断降级处理”,系统过载时拒绝请求,而不是让服务宕机
public void exceptionHandler(long seckillId, String userPhone, String note, BlockException ex) {
log.error("Oops, error occurred at {}:{}:{}", seckillId, userPhone, note, ex);
}
}
  • 通过 Sentinel 的流控规则对指定方法做限流
  • Sentinel 的限流是基于滑动窗口算法实现,性能开销低

策略6:SynchronizedLockStrategy

采用的是最传统的 Java 本地锁 synchronized 关键字来控制并发操作,防止秒杀超卖。

它的核心思想是:对每个商品 ID 建立一个监视对象(Object),然后用 synchronized 关键字对这个商品加锁实现单商品的并发控制

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55

@Component
@Slf4j
public class SynchronizedLockStrategy implements GoodsKillStrategy {
@Autowired
private SeckillMapper seckillMapper;
@Autowired
private SuccessKilledMapper successKilledMapper;
@Autowired
private StreamBridge streamBridge;
@Resource
private StateMachineService stateMachineService;

private final ConcurrentHashMap<Long, Object> seckillIdList = new ConcurrentHashMap<>();

@Override
public void execute(SeckillMockRequestDTO requestDto) {
Long seckillId = requestDto.getSeckillId();
String userId = requestDto.getPhoneNumber();
// 此处已优化,锁单个商品而不是整个方法
Object seckillMonitor = seckillIdList.get(seckillId);
if (seckillMonitor == null) {
Object value = new Object();
seckillIdList.put(seckillId, value);
}
synchronized (seckillIdList.get(seckillId)) {
Seckill seckill = seckillMapper.selectById(seckillId);
if (seckill.getNumber() > 0) {
seckillMapper.reduceNumber(seckillId, new Date());
SuccessKilled record = new SuccessKilled();
record.setSeckillId(seckillId);
record.setUserPhone(String.valueOf(userId));
record.setStatus(1);
record.setCreateTime(new Date());
successKilledMapper.insert(record);
} else {
if (stateMachineService.checkState(seckillId, States.IN_PROGRESS)) {
stateMachineService.feedMachine(Events.ACTIVITY_CALCULATE, seckillId);
streamBridge.send(DEFAULT_BINDING_NAME, MessageBuilder.withPayload(
SeckillMockResponseDTO
.builder()
.seckillId(seckillId)
.status(true)
.note(SYCHRONIZED.getName())
.taskId(requestDto.getTaskId())
.build())
.build());
}
if (log.isDebugEnabled()) {
log.debug("库存不足,无法继续秒杀!");
}
}
}
}
}
  • SynchronizedLockStrategy 是一种基于 Java 本地锁、按商品粒度加锁的秒杀策略,适用于单机项目,简单易实现,但不适合多节点部署。

策略7:ZookeeperLockStrategy

基于 Zookeeper 分布式锁的秒杀策略。它通过 Apache Curator 提供的 InterProcessMutex(分布式可重入锁)来控制对秒杀商品的并发访问,确保在分布式部署环境下的数据一致性和线程安全。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
@Component
@Slf4j
public class ZookeeperLockStrategy implements GoodsKillStrategy {
@Resource
private ZookeeperLockUtil zookeeperLockUtil;
@Resource
private SeckillExecutor seckillExecutor;

@Override
@SneakyThrows
public void execute(SeckillMockRequestDTO requestDto) {
long seckillId = requestDto.getSeckillId();
InterProcessMutex lock = zookeeperLockUtil.lock(seckillId);
if (lock != null) {
try {
seckillExecutor.dealSeckill(seckillId, requestDto.getPhoneNumber(), ZOOKEEPER_LOCK.getName(), requestDto.getTaskId());
} finally {
lock.release();
}
}
}
}
  • 提供的一种 可重入、阻塞式分布式锁实现
  • 保证在多个服务节点中,某一时刻只有一个节点可以获得锁来操作该商品。
  • 最后释放锁,无论执行成功与否,必须释放锁防止死锁

3.2.11 mq

SeckillKafkaConsumer

基于 Kafka 消息队列的消费者组件,用于异步处理秒杀请求。它监听名为 “goodskill-kafka” 的 Kafka 主题,接收到秒杀消息后,调用核心业务处理类 SeckillExecutor 执行具体的秒杀操作。
SeckillKafkaConsumer 是一个 Kafka 消费者类,主要用于接收异步秒杀请求消息,解包后调用实际的秒杀处理逻辑,实现“削峰填谷”,有效缓解高并发带来的系统压力。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@Slf4j
@Component
public class SeckillKafkaConsumer {
@Autowired
private SeckillExecutor seckillExecutor;

@KafkaListener(topics = "goodskill-kafka")
public void onMessage(Object data) {
if (data instanceof ConsumerRecord record) {
String userPhone = record.key().toString();
String value = (String) record.value();
SeckillMockRequestDTO seckillMockRequestDTO;
if (StringUtils.hasText(value)) {
seckillMockRequestDTO = JSON.parseObject(value, SeckillMockRequestDTO.class);
seckillExecutor.dealSeckill(seckillMockRequestDTO.getSeckillId(), userPhone, "秒杀场景四(kafka消息队列实现)", seckillMockRequestDTO.getTaskId());
}
} else {
log.warn("未处理数据:{}", data.toString());
}
}
}

StreamMessageConsumer

StreamMessageConsumer 是一个 基于 Spring Cloud Stream 的消息消费者配置类,用于处理来自消息中间件(如 RabbitMQ、Kafka、RocketMQ 等)的秒杀请求消息。它将接收到的 SeckillMockRequestDTO 类型消息交由 SeckillExecutor 执行,从而完成异步、解耦的秒杀操作。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
@Configuration
public class StreamMessageConsumer {

private static final Logger log = LoggerFactory.getLogger(StreamMessageConsumer.class);

private SeckillExecutor seckillExecutor;

@Autowired
public void setSeckillExecutor(SeckillExecutor seckillExecutor) {
this.seckillExecutor = seckillExecutor;
}

@Bean
public Consumer<SeckillMockRequestDTO> seckill() {
return (payload) -> {
log.info("收到秒杀请求:{}", payload);
String userPhone = payload.getPhoneNumber();
Long seckillId = payload.getSeckillId();
seckillExecutor.dealSeckill(seckillId, userPhone, SeckillSolutionEnum.RABBIT_MQ.name(), payload.getTaskId());
};
}
}

3.2.12 pojo

  1. ResponseDTO<T>
    通用响应封装类,用于在系统中统一返回结果格式,便于前后端交互时的数据结构一致性和处理统一性。你可以理解为这是一个前后端通信的“标准返回模板”。
    ResponseDTO 用于包装接口的返回数据,支持携带:
  • 状态码(code)
  • 提示信息(msg)
  • 总记录数(count)
  • 实际返回的数据数组(data)

它可以统一表示成功或失败的响应结果

  1. PageVO<T>
    PageVO 是一个通用分页结果对象,用于统一封装分页接口的返回结果,包括:
  • 总记录数 total
  • 当前页的数据项 items
  1. UserVO用户视图对象
    UserVO 是用来表示用户信息的展示模型,通常是接口返回用户信息时使用的结构,是从数据库实体 User(或类似表)转换而来

3.2.13 util

StateMachineService

通过 Spring StateMachine 管理每一个秒杀活动的状态生命周期
用来协调商品在不同状态下的行为(比如进行中、已结束等),并且通过 Redis 实现状态持久化和跨服务共享。

该类是一个 状态机管理服务组件,为每个秒杀活动(以 seckillId 区分)维护一个独立的状态机实例。提供以下功能:

方法名 作用
feedMachine() 输入一个事件,驱动状态变化, 给状态机发送事件,驱动状态流转
checkState() 检查指定活动是否处于某个状态
initStateMachine() 初始化某个活动的状态机,并持久化
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
@Slf4j
@Component
public class StateMachineService {
public static final String STATEMACHINE_REDIS_KEY_PREFIX = "seckillId:";

private final Map<String, StateMachine<States, Events>> stateMachineMap = new ConcurrentHashMap<>();
@Resource
private StateMachineFactory<States, Events> stateMachineFactory;
@Resource
private StateMachinePersister<States, Events, String> stateMachinePersister;

/**
* 向状态机中输入事件,用于处理活动事件的方法
*
* @param e 待处理的活动事件
*/
@SneakyThrows
public boolean feedMachine(Events e, long seckillId) {
StateMachine<States, Events> stateMachine = stateMachineMap.get(String.valueOf(seckillId));
stateMachinePersister.restore(stateMachine, STATEMACHINE_REDIS_KEY_PREFIX + seckillId);
StateMachineEventResult<States, Events> eventResult =
stateMachine.sendEvent(Mono.just(MessageBuilder.withPayload(e).build())).blockLast();
log.info("eventResult:{}", eventResult);
stateMachinePersister.persist(stateMachine, STATEMACHINE_REDIS_KEY_PREFIX + seckillId);
return eventResult != null && StateMachineEventResult.ResultType.ACCEPTED.equals(eventResult.getResultType());
}


/**
* 检查秒杀活动的状态是否符合要求
*
* @param seckillId 秒杀活动的ID
* @param state 指定的状态
* @return 如果当前状态与state相同则返回true,否则返回false
*/
@SneakyThrows
public boolean checkState(long seckillId, States state) {
StateMachine<States, Events> stateMachine = stateMachineMap.get(String.valueOf(seckillId));
stateMachinePersister.restore(stateMachine, STATEMACHINE_REDIS_KEY_PREFIX + seckillId);
return stateMachine.getState().getId().equals(state);
}

@SneakyThrows
public StateMachine<States, Events> initStateMachine(long seckillId) {
StateMachine<States, Events> stateMachine = stateMachineFactory.getStateMachine();
stateMachinePersister.persist(stateMachine, STATEMACHINE_REDIS_KEY_PREFIX + seckillId);
return stateMachineMap.putIfAbsent(String.valueOf(seckillId), stateMachine);
}
}

StateMachineService 是一个基于 Spring StateMachine 实现的活动状态管理中心,每个商品秒杀活动都有自己独立的状态流转过程(开始、进行中、结束),这个类负责事件驱动、状态判断和持久化操作,确保系统在并发下仍能可靠地进行秒杀流程控制。

UploadFileUtil

封装了一个 MinIO 客户端的文件上传工具类,提供了将文件(通常是前端上传的图片、视频、文档等)上传至 MinIO 对象存储服务的功能,并返回访问地址。

  • UploadFileUtil 是一个上传文件到 MinIO 并返回文件 URL 的工具类

ZookeeperLockUtil

提供基于 ZooKeeper 的分布式锁功能,使用 Curator 框架实现分布式锁的获取和释放,以保证在分布式环境下,秒杀活动等并发场景中的资源(如库存)能够被有效控制和协调

  • 用于在并发场景中控制对某个资源(如秒杀商品库存)的访问,确保只有一个线程/进程能在同一时刻对其进行操作
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
@Slf4j
@Component
public class ZookeeperLockUtil {

private CuratorFramework client;
@Value("${zookeeper_ip}")
private String zookeeperIp;

/**
* 采用curator提供的互斥锁获取锁方法
* 采用线程本地变量的方法解决并发问题
*
* @param seckillId 秒杀id
* @return true 加锁成功
*/
public InterProcessMutex lock(long seckillId) {
try {
String rootLockPath = "/goodskill";
InterProcessMutex interProcessMutex = new InterProcessMutex(client, rootLockPath + "/" + seckillId);
boolean acquire = interProcessMutex.acquire(1000L, TimeUnit.MILLISECONDS);
if (acquire) {
log.info("成功获取到zk锁,秒杀id{}", seckillId);
} else {
log.info("未获取到zk锁,秒杀id{}", seckillId);
}
return interProcessMutex;
} catch (Exception e) {
log.warn("获取zk锁异常:{}", e.getMessage());
return null;
}
}

@PostConstruct
private void initClient() {
// 初始化Curator客户端
RetryPolicy retryPolicy = new ExponentialBackoffRetry(1000, 3);
this.client = CuratorFrameworkFactory.newClient(zookeeperIp, retryPolicy);
client.start();
}

@PreDestroy
private void stopLocal() {
client.close();
log.info("zkClient已关闭");
}

private ZookeeperLockUtil() {
}
}

3.2.14 SeckillApplication秒杀服务启动类

这个类是 Spring Boot 应用的启动类,通过以下方式来配置和启动应用:

  1. 启动 Spring Boot 应用。
  2. 启用自动配置、事务管理、MyBatis Mapper 扫描、服务发现(如 Eureka)、Feign 客户端等功能。
  3. 定义了 REST API 控制器,能够处理 HTTP 请求并返回响应。
  4. 使用 Lombok 自动生成日志功能
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@SpringBootApplication(scanBasePackages = {
"com.goodskill.service",
"com.goodskill.core",
"com.goodskill.order.api",
})
@EnableTransactionManagement
@MapperScan("com.goodskill.service.mapper")
@EnableDiscoveryClient
@EnableFeignClients({"com.goodskill.order.api"})
@RestController
@Slf4j
public class SeckillApplication {

public static void main(String[] args) {
SpringApplication application = new SpringApplication(SeckillApplication.class);
application.setBannerMode(Banner.Mode.CONSOLE);
application.run(args);
}
}

3.2.15 application.yml

包含以下配置:

  • 服务端口和应用信息。
  • 数据库连接配置,如 MySQL 和 Redis。
  • Spring Cloud 和消息流配置,包括 Nacos、Kafka、RabbitMQ。
  • 分布式事务 配置(Seata)和 Dubbo 配置。
  • Flyway 和 Feign 配置。

4. goodskill-web (提供秒杀模拟接口访问)

4.1 config包

  • GlobalExceptionHandler

GlobalExceptionHandler

GlobalExceptionHandler 类是一个用于全局异常处理的类,基于 Spring Boot 中的 @RestControllerAdvice 和 @ExceptionHandler 注解。这个类捕获并处理应用中的各种异常,并返回格式化的响应信息。它的作用是集中处理应用中的异常,提供统一的错误信息响应格式,以便前端或客户端更好地处理错误。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
/**
* 全局异常处理类
*/
@Slf4j
@RestControllerAdvice
public class GlobalExceptionHandler {

//用来处理所有未被其他 @ExceptionHandler 捕获的通用异常(继承自 Exception 类的异常)
@ExceptionHandler(Exception.class)
@ResponseStatus(HttpStatus.OK)
public ApiResult exception(Exception e) {
log.error(e.getMessage(), e);
return ApiResult.error(ResultCode.C500.getCode(), e.getMessage());
}

// 用来处理参数绑定异常,通常发生在请求的参数不符合要求时(例如请求参数格式错误)。
// 这种异常一般是在 Spring MVC 中发生的,当请求体数据无法绑定到 Java Bean 时,会抛出 BindException。
@ExceptionHandler(BindException.class)
@ResponseStatus(HttpStatus.OK)
public ApiResult exception(BindException e) {
String errorMsg = e.getBindingResult().getFieldErrors().stream()
.map(errorInfo -> errorInfo.getField() + errorInfo.getDefaultMessage()).collect(Collectors.joining(","));
log.warn(e.getMessage(), e);
return ApiResult.error(ResultCode.C500.getCode(), "参数校验失败:" + errorMsg);
}

//处理参数校验失败的异常,通常发生在使用 @Valid 或 @Validated 注解进行参数校验时
// 如果校验失败,Spring 会抛出 MethodArgumentNotValidException。
@ExceptionHandler(MethodArgumentNotValidException.class)
@ResponseStatus(HttpStatus.OK)
public ApiResult exception(MethodArgumentNotValidException e) {
String errorMsg = e.getBindingResult().getFieldErrors().stream()
.map(errorInfo -> errorInfo.getField() + errorInfo.getDefaultMessage()).collect(Collectors.joining(","));
log.warn(e.getMessage(), e);
return ApiResult.error(ResultCode.C500.getCode(), "参数校验失败:" + errorMsg);
}
}

4.2 controller

  • SeckillMockController

SeckillMockController

模拟秒杀场景的控制器,主要用于模拟不同类型的秒杀策略,并通过不同的方式来控制并发、库存、任务分配等。

主要功能和作用

  1. 模拟不同的秒杀场景:

    • 类中的方法模拟了多种秒杀场景,涵盖了同步锁、分布式锁、消息队列、数据库原子更新等技术手段。
    • 每个场景对应一个秒杀策略,模拟秒杀的并发处理和库存更新。
  2. 场景详细解释:

    • 场景一 (sychronized):使用同步锁来控制并发,模拟单机环境下的秒杀。
    • 场景二 (redisson):使用 Redis 分布式锁来控制并发,适用于分布式环境。
    • 场景三 (activemq):使用 ActiveMQ 消息队列进行异步秒杀,模拟高并发情况下的异步处理。
    • 场景四 (kafka):使用 Kafka 消息队列,提升性能并处理更高并发。
    • 场景五 (procedure):使用数据库原子性更新来处理秒杀,使用 SQL 存储过程进行库存更新。
    • 场景六 (activemq/reply):异步秒杀并在 30 秒内返回秒杀执行结果。
    • 场景七 (zookeeperLock):使用 Zookeeper 分布式锁来控制并发。
    • 场景八 (redisReactiveMongo):使用 Redis 缓存执行减库存操作,最后通过 MQ 完成数据落地。
    • 场景九 (rabbitmq):使用 RabbitMQ 消息队列来进行异步秒杀处理。
    • 场景十 (limit):使用 Sentinel 限流策略与数据库原子更新结合,防止过多请求对系统造成过载。
    • 场景十一 (atomicWithCanal):使用数据库原子更新和 Canal 监听数据库变更,处理秒杀结果。
  3. 执行秒杀操作:

    • 每个秒杀场景都通过 processSeckill 方法来执行。此方法负责准备秒杀的库存、调整线程池参数、记录任务开始时间等。
    • 该方法可以通过不同的秒杀策略(如同步锁、分布式锁、消息队列等)来执行秒杀,并提供自定义的执行任务(如发送消息队列、执行秒杀服务等)。
  4. 异步和并发处理:

    • 该类通过线程池 (taskExecutor) 来模拟高并发环境下的秒杀处理,确保大量请求可以同时进行。
    • 使用不同的并发控制方法(如同步锁、Redis 锁、消息队列等)来保证秒杀过程中的数据一致性和库存正确性。
  5. 自定义任务和日志记录:

    • 通过 Runnable 来定义任务并提交给线程池,模拟每个用户参与秒杀的操作。
    • 每个秒杀请求都会通过日志记录相关信息,便于调试和监控。
  6. 任务统计与时间记录:

    • 类中还包括了一些用于任务时间统计的工具(如 TaskTimeCaculateUtil),可以记录和返回每个秒杀任务的执行时间。
  7. 扩展性与容错处理:

    • 该控制器支持不同的秒杀策略和多种分布式环境的配置(如 Redis、Zookeeper、Kafka、RabbitMQ 等),使得秒杀系统可以根据实际需求进行调整和优化。
    • 错误处理也经过了设计,能够在各种场景下提供反馈。

4.3 stream.consumer

  • SeckillMockCanalResponseListener
  • SeckillMockResponseListener

SeckillMockCanalResponseListener

该类主要用于在秒杀活动结束时,监听 Canal 推送的数据库变更消息,记录秒杀活动的结束时间、ID 和成功的交易笔数。它实现了一个基于 Canal 的数据库变更监听机制,帮助系统在秒杀活动结束后执行后续处理和统计操作。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
/**
* canal binlog秒杀结果消息消费者
*/
@Slf4j
@Configuration
public class SeckillMockCanalResponseListener {
@DubboReference
private SeckillService seckillService;

@Bean
public Consumer<SeckillMockCanalResponseDTO> seckillCanalResult() {
return responseDto -> {
long seckillId = responseDto.getSeckillId();
String note = responseDto.getNote();

if (Boolean.TRUE.equals(responseDto.getStatus())) {
log.info("binlog监控到秒杀活动结束,{}时间:{},秒杀id:{}", note, new Date(), seckillId);
long successKillCount = seckillService.getSuccessKillCount(seckillId);
log.info("最终成功交易笔数:{}", successKillCount);
}
};
}
}
  • seckillCanalResult:创建了一个 Consumer<SeckillMockCanalResponseDTO>,它定义了如何处理 Canal 监听到的 SeckillMockCanalResponseDTO 数据。
    • 如果 status 为 true(表示秒杀活动已结束),则执行以下操作:
      • 打印秒杀结束的日志,包括活动的结束时间、秒杀 ID 和描述信息。
      • 调用 seckillService.getSuccessKillCount(seckillId) 获取成功秒杀的总数,并打印出来。

SeckillMockResponseListener

核心功能是监听秒杀活动的结束事件,确保在活动结束后正确计算并记录成功的交易笔数,处理异步操作带来的延迟,并输出相关的统计信息。它还确保在场景八(Redis + MongoDB 异步操作场景)下,执行补偿等待操作,最终结束秒杀活动

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
@Slf4j
@Configuration
public class SeckillMockResponseListener {
@DubboReference
private SeckillService seckillService;

@Bean
public Consumer<SeckillMockResponseDTO> seckillResult() {
return responseDto -> {
long seckillId = responseDto.getSeckillId();
String note = responseDto.getNote();

if (Boolean.TRUE.equals(responseDto.getStatus())) {
log.info("秒杀活动结束,{}时间:{},秒杀id:{}", note, new Date(), seckillId);
long successKillCount = seckillService.getSuccessKillCount(seckillId);
long temp = 0;
while (successKillCount != temp) {
// 计算总数可能有延迟,等待统计数据稳定后得到最终结果
log.info("最终成功交易笔数统计中。。。");
successKillCount = temp;
try {
Thread.sleep(1500L);
} catch (InterruptedException e) {
log.warn(e.getMessage(), e);
}
temp = seckillService.getSuccessKillCount(seckillId);
}
TaskTimeCaculateUtil.stop(responseDto.getTaskId());
log.info("最终成功交易笔数:{}", successKillCount);
log.info("历史任务耗时统计:{}", TaskTimeCaculateUtil.prettyPrint(responseDto.getTaskId()));
// 场景八由于是异步插入,补偿查询最新插入记录数,延迟10s
try {
if (SeckillSolutionEnum.REDIS_MONGO_REACTIVE.getName().endsWith(note)) {
Thread.sleep(10000);
log.info("mongo最终成功交易笔数:{}", successKillCount);
}
} catch (InterruptedException e) {
log.error(e.getMessage(), e);
Thread.currentThread().interrupt();
}
seckillService.endSeckill(seckillId);
}
};
}
}
  • seckillResult():创建了一个 Consumer,该方法定义了如何处理收到的秒杀活动结束消息:
    • 它首先获取秒杀活动的 ID 和状态(通过 responseDto)。
    • 如果秒杀活动已经结束(status == true),则开始统计成功交易笔数。
    • 如果统计的成功交易笔数发生变化,则会继续等待,直到数据稳定。
    • 完成统计后,停止任务时间统计,输出日志,记录活动的最终成功交易笔数和任务的耗时。
    • 如果是特定场景(如 Redis + MongoDB 异步处理),还会进行补偿操作,等待数据插入完成后再输出最终结果。
    • 最后,调用 seckillService.endSeckill(seckillId) 来结束秒杀活动。

4.4 util

  • TaskTimeCaculateUtil

TaskTimeCaculateUtil

TaskTimeCaculateUtil 类通过封装 StopWatch 来提供简单的任务计时、停止和统计功能。它可以用来监控任务执行的时长、输出执行时间报告

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
@Slf4j
public class TaskTimeCaculateUtil {

/**
* 计时器
*/
private static StopWatch stopWatch = new StopWatch();

/**
* 任务计时器映射, taskId:stopWatch
*/
private static final Map<String, StopWatch> taskWatchMap = new ConcurrentHashMap<>();


/**
* 开启一个计时任务,最大允许同时开启十个任务
*
* @param taskName 任务名
* @param taskId 任务id
*/
public synchronized static void startTask(String taskName, String taskId) {
if (stopWatch.getTaskCount() > 10 || stopWatch.isRunning()) {
stopWatch = new StopWatch();
}
try {
stopWatch.start(taskName);
taskWatchMap.put(taskId, stopWatch);
} catch (IllegalStateException e) {
log.warn("Last task have not finish yet!", e);
}
}

/**
* 停止上一个计时任务
*
* @param taskId 任务id
*/
public synchronized static void stop(String taskId) {
try {
taskWatchMap.get(taskId).stop();
} catch (IllegalStateException e) {
log.warn("Oops, stop error occurs!", e);
}
}

/**
* 输出历史任务的耗时情况
*
* @param taskId 任务id
* @return 任务的耗时情况
*/
public static String prettyPrint(String taskId) {
return taskWatchMap.get(taskId).prettyPrint();
}
}

关键方法:

  1. startTask(String taskName, String taskId):
    • 启动一个新的计时任务。最多允许同时有 10 个任务在计时(通过检查 stopWatch.getTaskCount())。如果任务数超过限制,创建一个新的计时器实例。
    • 使用 taskId 将任务与计时器实例关联。
  2. stop(String taskId):
    • 停止指定任务的计时。通过 taskId 获取对应的 StopWatch 对象并停止它。
    • 如果停止时出现异常(如没有找到对应的任务或任务没有启动),会记录警告日志。
  3. prettyPrint(String taskId):
    • 获取并返回某个任务的详细耗时信息(如开始时间、结束时间、总耗时等),输出的格式通过 StopWatch 的 prettyPrint 方法来实现。

4.5 SampleWebApplication启动类

该启动类的作用是:

  • 启动一个 Spring Boot 应用。
  • 启用服务发现功能,允许应用与服务注册中心进行交互。
  • 启用 Feign 客户端支持,方便远程调用其他服务。
  • 启用异步功能,支持方法的异步执行。
  • 显式地指定 Web 应用类型为 Servlet,使用传统的 Servlet 容器启动。

5. goodskill-spring-boot-starter (项目配置自动装配)

goodskill-spring-boot-starter 模块的作用

  • 它是一个为 GoodSkill 系统封装的 Spring Boot 自动配置模块,提供了 OAuth2 登录支持、MinIO 对象存储支持、以及其他通用能力的开箱即用集成,简化业务模块的集成和配置。

在大型项目中,多个微服务可能都要使用:

  • 第三方登录(比如 Gitee OAuth2)
  • MinIO 对象存储服务
  • 通用的服务注册与配置逻辑
  • 自定义工具类、AOP 组件等

如果每个模块都手动去写一遍配置,那很麻烦而且容易出错。
于是就有了这个 starter 模块,统一封装好这些配置和工具类
主工程只要在 application.yml 里写好配置,就能自动装配并使用这些功能,实现“开箱即用

6. goodskill-seata (集成nacos+dubbo+shardingjdbc+seata的分布式事务解决方案示例)

6.1 feign包

AuthFeignClient接口

AuthFeignClient 接口是一个 Feign 客户端,用于在 Spring Cloud 微服务架构中,通过声明式方式远程调用 goodskill-auth 服务的接口

1
2
3
4
5
6
7
8
9
@FeignClient(name = "goodskill-auth")
public interface AuthFeignClient {

/**
* 为用户增加角色
*/
@PostMapping("/user/role/add")
Result<String> addRole(@RequestParam("userId") int userId, @RequestParam("roleId") int roleId);
}

AuthFeignClient 的作用是:

  • 在当前服务中,以 Java 接口的方式,调用 goodskill-auth 服务的 “为用户添加角色” 接口,实现跨服务远程调用,而不需要手动写 HTTP 请求逻辑。

6.2 GoodskillSeataApplication

这是一个 Spring Boot 应用的启动类,主要功能是:

✅ 在一个服务中同时调用多个远程服务(Dubbo + Feign)
通过 Seata 实现分布式事务管理
✅ 测试在异常情况下,多个子服务之间的数据回滚效果,确保最终一致性

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
@SpringBootApplication(scanBasePackages = {"com.goodskill.seata", "com.goodskill.core"})
@RestController
@EnableFeignClients(basePackages = "com.goodskill.seata.feign")
public class GoodskillSeataApplication {
@DubboReference
private SeckillService seckillService;
@DubboReference
private GoodsService goodsService;
@Resource
private AuthFeignClient authFeignClient;

/**
* 测试seata分布式事务
*/
@GetMapping("/test")
@GlobalTransactional
public void testGlobalTransaction() {
GoodsVO goods = new GoodsVO();
goods.setName("test");
goodsService.addGoods(goods);
SeckillVO seckill = seckillService.findById(1001L);
seckill.setNumber(seckill.getNumber() - 1);
seckillService.saveOrUpdateSeckill(seckill);
authFeignClient.addRole(21, 7);
// 测试异常情况
throw new RuntimeException();
}

public static void main(String[] args) {
SpringApplication.run(GoodskillSeataApplication.class, args);
}
}

因为加了 @GlobalTransactional,只要任何一个子操作失败(比如最后抛出异常),就会触发 Seata 的事务回滚机制,让:

  • 添加商品
  • 更新秒杀信息
  • 添加用户角色
    全部回滚,确保数据一致性。

7. goodskill-admin (SpringBoot Admin监控服务端,支持Spring Cloud微服务发现)

GoodskillAdminApplication
后台管理模块的启动类,它的作用是启动整个 goodskill-admin 微服务。
用于构建一个用于监控其他 Spring Boot 微服务的后台管理界面

8. goodskill-job (elastic-job定时任务)

goodskill.job.simple包

  • MyJob
  • MyJob2

MyJob

分片定时任务实现类

作用

  • 定时同步商品数据到 Elasticsearch 索引中,通过 ElasticJob 的分片机制对任务进行分布式拆分和执行。

  • 通过 Dubbo 调用远程的商品服务,获取商品列表。

  • 将商品数据转换为 ES 所需的 VO 对象后,调用 goodsEsService.saveBatch() 批量更新索引

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
@Component
@Slf4j
public class MyJob implements SimpleJob {
@DubboReference
private GoodsService goodsService;
@Resource
private GoodsEsService goodsEsService;

@Override
public void execute(ShardingContext context) {
switch (context.getShardingItem()) {
case 0:
try {
log.info("分片0 商品es索引开始更新。。。");
List list = goodsService.findMany().parallelStream().map(g -> {
GoodsVO goods = new GoodsVO();
BeanUtils.copyProperties(g, goods);
return goods;
}).collect(Collectors.toList());
goodsEsService.saveBatch(list);
log.info("分片0 商品es索引更新成功,条数:{}", list.size());
} catch (Exception e) {
log.warn("分片0 商品es索引定时任务更新失败!", e);
}
break;
case 1:
try {
log.info("分片1 商品es索引开始更新。。。");
} catch (Exception e) {
log.warn("分片1 商品es索引定时任务更新失败!", e);
}
break;
case 2:
try {
log.info("分片2 商品es索引开始更新。。。");
} catch (Exception e) {
log.warn("分片2 商品es索引定时任务更新失败!", e);
}
break;
// case n: ...
default:
break;
}
}
}

ElasticJob 会根据分片数多线程调用该方法,每个分片负责处理不同的数据

  • 分片0的执行逻辑
    • 获取商品列表:调用远程 goodsService.findMany() 获取所有商品数据。
    • 转换成 VO 对象:使用 BeanUtils.copyProperties() 把原始商品实体转为 GoodsVO(适用于 ES 存储结构)。
    • 并行处理数据:parallelStream() 多线程加速属性拷贝(视数据量而定是否有效)。
    • 批量存储到 ES:通过 goodsEsService.saveBatch(list) 一次性把所有商品数据同步到 Elasticsearch 索引中。
    • 记录日志:记录更新成功的商品条数。

MyJob2

这个类也是一个定时任务的实现类,不过:

作用
作为第二个定时任务示例,仅打印日志,演示分片功能如何生效。

ElasticJobApplication

ElasticJob 服务启动类

  • 启动整个任务服务程序,注册两个 ElasticJob 定时任务

application.yml

ElasticJob 配置文件

  • 配置了 ElasticJob、Dubbo、Nacos、服务端口等信息,是整个任务系统的配置中心。
  • 每 6 秒执行一次 MyJob
  • 定义并配置两个定时任务(simpleJob 和 simpleJob2)的执行方式,包括调度时间、分片策略、执行类等。
  • 注册了两个 ElasticJob 分片任务,每个任务的执行类分别是 MyJob 和 MyJob2,都每 6 秒执行一次,分片数量为 1,执行时使用轮询策略,并通过 Zookeeper 注册中心进行调度管理。

goodskill-job 执行流程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
          +--------------------------+
| ElasticJobApplication |
| ⬇ 启动 SpringBoot |
+------------+-------------+
|
Registers ElasticJob Jobs
|
+-------------+--------------+
| |
+------------------+ +------------------+
| MyJob | | MyJob2 |
| ES 索引更新任务 | | 日志演示任务 |
+------------------+ +------------------+
| |
+----v----+ +-----v----+
| Dubbo | | 日志打印 |
| Goods | +----------+
| Services|
+---------+

9. goodskill-common (公共服务)

  • goodskill.common.api
  • goodskill.common.service
  • goodskill.core

9.1 goodskill-common-api 模块

  • DictDTO: 业务字典表,用于在系统中传递“业务字典”相关的数据
  • DictSimpleDTO: 简化版字典数据模型

DictionaryService

字典通用服务接口

  • 为系统提供统一的业务字典查询能力,支持通过字典编码、键(key)、值(value)等方式获取简化的字典数据,用于前端展示或业务逻辑处理。
方法 适用场景
getSimpleDictByCode 前端加载下拉框/单选框选项
getSimpleDictByDictCodeAndDictKey 通过 code+key 显示具体字典项,比如展示“男”而不是“1”
getSimpleDictByDictCodeAndDictValue 通过 code+value 反查 key,适用于数据入库或比对

GlobalDictionaryService

全局字典通用服务接口

  • 它的核心目标是为系统提供无需指定系统编码(systemCode)的字典查询能力,适用于通用性更强、不区分子系统的字典查询场景

9.2 goodskill-common-service

OssFileController

是一个基于 Spring Boot 实现的 文件上传和下载控制器类,它的主要作用是:

  • 提供统一的接口,用于接收前端上传的文件,并将文件上传到 MinIO,同时保存文件信息(如 MD5、文件名映射)到 MongoDB 中;同时提供文件下载接口,支持返回文件的二进制流或生成临时下载 URL。

FileNameMapping

作为文件名映射信息的实体类用于存储上传文件的元数据信息并持久化到 MongoDB 的 oss_file_name_mapping 集合中。

FileNameMappingRepository

用于操作 MongoDB 的 数据访问层接口(Repository),继承自 Spring Data 提供的 MongoRepository,用于管理 FileNameMapping 实体的数据库操作

方法签名 查询条件 返回内容 作用说明
findByOriginalFileName(String originalFileName) 根据原始文件名查询 Optional<FileNameMapping> 判断用户上传的文件是否已存在
findByUniqueFileName(String uniqueFileName) 根据唯一文件名查询 Optional<FileNameMapping> 根据 MinIO 上的文件名找记录
findFirstByFileMd5(String fileMd5) 根据文件 MD5 查询一条记录 Optional<FileNameMapping> 判断是否上传过相同内容的文件(实现文件去重逻辑)

CommonServiceApplication

用于启动整个 Common Service 模块的服务

9.3 goodskill-core

分为三个子模块:

  • core-jwt
  • core-orm
  • core-util

9.3.1 core-jwt

JwtUtils

JWT 工具类

  • 用于在项目中 创建、验证和解析 JWT(JSON Web Token)令牌,常用于用户身份认证和授权场景。

JwtUtils 封装了 JWT 的常用操作,包括:

  • 创建 Token(createToken)
  • 验证 Token 合法性(verifyToken)
  • 解析 Token 中的内容(parseToken)
  • 生成用于签名的密钥(generateKey)
    适合在登录成功后生成 Token,下次访问接口时用这个 Token 验证用户身份。

9.3.2 core-orm

FieldMetaObjectHandler

是 MyBatis-Plus 提供的一个 字段自动填充处理器,用于在插入或更新数据库记录时,自动填充公共字段(如:createTime, updateTime, createUser, updateUser 等),避免手动赋值,简化开发。

  • FieldMetaObjectHandler 是为了解决你在新增或更新数据时,不用每次都手动设置 创建人/创建时间/更新人/更新时间,MyBatis-Plus 帮你自动搞定。

MybatisPlusConfig

是一个 MyBatis-Plus 的配置类,主要作用是配置分页插件,以支持数据库的分页查询功能

  • 向 Spring 容器中注入 MybatisPlusInterceptor,并注册分页拦截器 PaginationInnerInterceptor,用于开启 MyBatis-Plus 的分页功能。

9.3.3 core-util

constant包

  • SeckillStatusConstant:定义秒杀活动状态的常量

entity包

  • BaseColEntity 是一个 通用的基础类,主要作用是为系统中绝大部分实体类提供公共字段,比如:createTime、updateTime、createUser 和 updateUser, 用于封装实体类中常见的审计字段(记录创建、更新人和时间),方便代码复用,减少冗余。

enums包

  • ActivityEvent: 枚举各个支付事件
  • CommonConstants: 公共常量类,包含用户账户状态
  • Events: 枚举各个支付事件(和ActivityEvent一样)
  • SeckillSolutionEnum: 枚举各个秒杀场景
  • SeckillStatEnum: 枚举秒杀状态
  • States: 秒杀活动状态枚举
  • UserAccountEnum: 枚举用户账户登录状态

exception包

  • CommonException: 自定义普通异常
  • SeckillException: 自定义秒杀异常
  • SeckillCloseException: 自定义秒杀关闭异常

feign包

  • FeignHeaderInterceptor 的作用是 拦截并传递 HTTP 请求中的头部信息,使得在使用 Feign 进行远程调用时,能够将当前请求的头部信息自动添加到 Feign 请求的头部。

info包

  • ResultCode: 枚举系统响应码
Result

通用的响应封装类,用于统一封装 API 响应结果

  • 统一封装 API 响应结果,使得所有接口的返回结构保持一致。
  • 提供了通用的返回码(如成功、失败)和响应信息字段,便于客户端处理。
ApiResult

API 响应结果的封装类,用于统一管理接口返回的数据格式

  • ApiResult 类主要用于封装 API 的响应内容。通过该类,开发者可以统一定义接口返回的格式,使得接口的响应结构在系统中保持一致。

pojo包

  • UserBO 类是一个用于表示 用户基本信息的业务对象(BO, Business Object)。它封装了与用户相关的所有基础信息,通常用于在系统中传递或持久化用户数据。
  • AuthResponseDTO 类是一个 用户鉴权服务的响应数据传输对象。它用于在用户认证成功后向客户端返回认证信息。
  • SeckillWebMockRequestDTO 类是一个 模拟秒杀请求的 DTO,用于模拟秒杀活动中的请求数据。该类可能用于测试、模拟或调试秒杀系统的功能

util包

  • HeaderUtil: 从当前请求的 Header 中提取 userId。
  • MD5Util: 生成和获取md5加密
  • UserInfoUtil: 用户信息工具类,主要用于从当前请求中提取用户信息。

10. goodskill-gateway (微服务API网关,统一服务鉴权,支持动态路由加载)

10.1 config包

  • GatewayConfiguration
  • SaTokenConfiguration
  • StpInterfaceImpl

GatewayConfiguration

配置 Spring Cloud Gateway 网关路由

  • 为微服务架构中的 Spring Cloud Gateway 网关服务 提供 路由转发能力和远程调用的兼容支持。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
@Configuration
public class GatewayConfiguration {

// 定义网关路由规则
@Bean
public RouteLocator customizedLocator(RouteLocatorBuilder builder) {
return builder.routes()
.route("goodskill-order", r -> r.path("/api/order/**")
.filters(f -> f.stripPrefix(2))
.uri("lb://goodskill-order"))
.route("goodskill-seata", r -> r.path("/api/seata/**")
.filters(f -> f.stripPrefix(2))
.uri("lb://goodskill-seata"))
.route("goodskill-seckill", r -> r.path("/api/seckill/**")
.filters(f -> f.stripPrefix(2))
.uri("lb://goodskill-seckill"))
.route("goodskill-auth", r -> r.path("/api/auth/**")
.filters(f -> f.stripPrefix(2))
.uri("lb://goodskill-auth"))
.route("goodskill-web", r -> r.path("/api/web/**")
.filters(f -> f.stripPrefix(2))
.uri("lb://goodskill-web"))
.build();
}

// 修复使用feign client远程调用服务时报HttpMessageConverters缺失问题
@Bean
@ConditionalOnMissingBean
public HttpMessageConverters messageConverters(ObjectProvider<HttpMessageConverter<?>> converters) {
return new HttpMessageConverters(converters.orderedStream().collect(Collectors.toList()));
}
}
方法 功能
customizedLocator 定义网关路由规则,将 /api/… 路径转发给对应微服务,支持路径前缀剥离、服务名路由等
messageConverters 防止 Feign 使用中 HTTP 消息转换器缺失的问题,保障远程调用的数据转换正常

SaTokenConfiguration

Sa-Token 权限认证框架的 全局配置类,其主要作用是在网关层统一处理登录校验、权限控制、放行白名单、异常处理等逻辑

  • 配置 Sa-Token 的全局过滤器,对所有请求进行统一的权限认证处理
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
/**
* [Sa-Token 权限认证] 全局配置类
*/
@Configuration
@Slf4j
public class SaTokenConfiguration {
@Resource
private IgnoreProperties ignoreProperties;

/**
* 注册 [Sa-Token全局过滤器],优先级高于GlobalFilter,即在GlobalFilter之前执行
*/
@Bean
public SaReactorFilter getSaReactorFilter() {
return new SaReactorFilter()
// 指定 [拦截路由]
.addInclude("/**") /* 拦截所有path */
// 指定 [放行路由]
.addExclude("/favicon.ico", "/actuator/**")
// 指定[认证函数]: 每次请求执行
.setAuth(obj -> {
// 打印路径
ServerHttpRequest request = SaReactorSyncHolder.getContext().getRequest();
RequestPath path = request.getPath();
log.info("---------- sa全局认证, 请求url:{}", path);
SaRouter.notMatch(ignoreProperties.getWhiteUrl())
.check(r -> {
StpUtil.checkLogin();
request.mutate().header(USER_ID_HEADER, StpUtil.getLoginId() + "");
List<String> userWhiteUrlList = ignoreProperties.getLoginUserWhiteUrls();
// 登录用户白名单接口放行
if (!ifMatchUserLoginWhiteUrl(userWhiteUrlList, path)) {
// StpUtil.checkPermission(path.value());
}
});
})
// 指定[异常处理函数]:每次[认证函数]发生异常时执行此函数
.setError(e -> {
log.error("---------- sa全局异常", e);
return SaResult.error(e.getMessage());
});
}


/**
* 判断当前url是否命中白名单
*/
private static boolean ifMatchUserLoginWhiteUrl(List<String> userWhite, RequestPath path) {
boolean found = false;
for (String pattern : userWhite) {
boolean match = SaPathMatcherHolder.getPathMatcher().match(pattern, path.value());
if (match) {
found = true;
break;
}
}
return found;
}
}
功能模块 说明
登录校验 非白名单接口必须登录才能访问
权限控制 登录后可拓展控制用户访问哪些接口(预留权限校验代码)
白名单机制 支持配置全局白名单 + 登录用户白名单
异常统一处理 所有异常都被统一捕获返回格式化的错误响应
兼容性好 基于 Sa-Token + Spring Cloud Gateway,适用于微服务架构

StpInterfaceImpl

Sa-Token 框架中用于 自定义权限和角色加载逻辑的实现类,它通过远程调用用户权限服务(authFeignClient)来动态获取用户的权限和角色。

  • 根据用户的 loginId 加载其权限码集合和角色集合,用于 StpUtil.checkPermission()、StpUtil.checkRole() 这样的权限判断方法
  • 结合 Sa-Token 的权限控制机制,实现在登录后动态获取用户权限和角色信息
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// sa-token自定义权限加载接口实现类
@Component
public class StpInterfaceImpl implements StpInterface {
@Resource
private AuthFeignClient authFeignClient;


// 返回一个账号所拥有的权限码集合
@Override
public List<String> getPermissionList(Object loginId, String loginType) {
return CompletableFuture.supplyAsync(() -> authFeignClient.listUserPermission(String.valueOf(loginId), loginType)).join();
}


// 返回一个账号所拥有的角色标识集合 (权限与角色可分开校验)
@Override
public List<String> getRoleList(Object loginId, String loginType) {
return CompletableFuture.supplyAsync(() -> authFeignClient.listUserRole(String.valueOf(loginId), loginType)).join();
}
}
方法名 返回值 作用说明
getPermissionList(Object loginId, String loginType) List<String> 根据 loginId 获取该用户的权限列表(如:user:add, order:view 等),供权限注解或方法调用使用
getRoleList(Object loginId, String loginType) List<String> 获取该用户所属的角色列表(如:admin, user, super_admin),供角色注解或方法调用使用

10.2 feign包

AuthFeignClient接口

Feign 客户端接口,用于在微服务架构中 远程调用 goodskill-auth 服务的接口,专门用于获取用户的权限信息和角色信息

10.3 properties

IgnoreProperties

配置 接口白名单 的 Spring 配置类,作用是:

  • 从配置文件中读取不需要权限校验的 URL 列表(白名单),并提供给权限框架(如 Sa-Token)使用。
  • 用于加载并管理 白名单 URL 列表(不拦截、无需登录即可访问)

10.4 service

  • DynamicRouteConfigWatcher
  • DynamicRouteRefresh

DynamicRouteConfigWatcher

动态网关路由配置监听器和初始化器,它的核心作用是:

  • 从 Nacos 中读取、监听并应用动态路由配置,用于 Spring Cloud Gateway 的动态路由管理。
  1. 初始化网关路由配置(从 Nacos 获取)
  2. 监听 Nacos 中路由配置变化并自动更新网关路由
  3. 启动时加载当前配置
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
/**
* 动态路由配置监听以及初始化
*/
@Slf4j
@Component
public class DynamicRouteConfigWatcher implements CommandLineRunner {

@Autowired
private DynamicRouteRefresh dynamicRouteService;

private ConfigService configService;
@Value("${spring.cloud.nacos.server-addr}")
private String NACOS_SERVER_ADDR;

@Value("${spring.cloud.nacos.namespace:}")
private String NACOS_NAMESPACE;

@Value("${spring.cloud.nacos.group:DEFAULT_GROUP}")
private String NACOS_ROUTE_GROUP;

@Value("${spring.cloud.nacos.username}")
private String NACOS_USERNAME;

@Value("${spring.cloud.nacos.password}")
private String NACOS_PASSWORD;

@Value("${nacos.router.data.id}")
private String NACOS_ROUTE_DATA_ID;

// 初始化时调用:用于初始化 ConfigService 并注册路由配置监听器
@PostConstruct
public void init() {
log.info("gateway route init...");
try {
configService = initConfigService();
if (configService == null) {
log.warn("initConfigService fail");
return;
}
} catch (Exception e) {
log.error("初始化网关路由时发生错误", e);
}
dynamicRouteByNacosListener(NACOS_ROUTE_DATA_ID, NACOS_ROUTE_GROUP);
}

// 监听指定的 Nacos 路由配置,如果路由有变更,则触发更新
public void dynamicRouteByNacosListener(String dataId, String group) {
try {
configService.addListener(dataId, group, new Listener() {
@Override
public void receiveConfigInfo(String configInfo) {
log.info("进行网关更新:\n\r{}", configInfo);
List<RouteDefinition> definitionList = JSON.parseArray(configInfo, RouteDefinition.class);
log.info("update route : {}", definitionList.toString());
dynamicRouteService.updateList(definitionList);
}

@Override
public Executor getExecutor() {
log.info("getExecutor\n\r");
return null;
}
});
} catch (NacosException e) {
log.error("从nacos接收动态路由配置出错!!!", e);
}
}

// 初始化 Nacos 配置客户端,设置必要连接参数,如地址、账号、密码等。
private ConfigService initConfigService() {
try {
Properties properties = new Properties();
properties.setProperty("serverAddr", NACOS_SERVER_ADDR);
properties.setProperty("username", NACOS_USERNAME);
properties.setProperty("password", NACOS_PASSWORD);
properties.setProperty("namespace", NACOS_NAMESPACE);
return configService = NacosFactory.createConfigService(properties);
} catch (Exception e) {
log.error("初始化网关路由时发生错误", e);
return null;
}
}

// 实现 CommandLineRunner 接口,在项目启动时执行:拉取一次当前 Nacos 中的路由配置并加载到网关中
@Override
public void run(String... args) throws Exception {
String configInfo = configService.getConfig(NACOS_ROUTE_DATA_ID, NACOS_ROUTE_GROUP, 3000);
log.info("获取网关当前配置:{}", configInfo);
List<RouteDefinition> definitionList = JSON.parseArray(configInfo, RouteDefinition.class);
if (!CollectionUtils.isEmpty(definitionList)) {
definitionList.forEach(definition -> {
log.info("add route : {}", definition.toString());
dynamicRouteService.save(definition);
});
}
}
}
方法名 / 生命周期 作用
@PostConstruct init() 初始化时调用:用于初始化 ConfigService 并注册路由配置监听器(动态监听 Nacos 配置变化)。
dynamicRouteByNacosListener(String dataId, String group) 监听指定的 Nacos 路由配置,如果路由有变更,则触发更新。
ConfigService initConfigService() 初始化 Nacos 配置客户端,设置必要连接参数,如地址、账号、密码等。
run(String… args) 实现 CommandLineRunner 接口,在项目启动时执行:拉取一次当前 Nacos 中的路由配置并加载到网关中。

DynamicRouteRefresh

实现 动态网关路由刷新功能的核心类

  • 实现对网关路由的 新增、更新、删除 操作,并通过事件机制(RefreshRoutesEvent)动态刷新路由。
  1. 使用 ApplicationEventPublisher 推送事件刷新网关路由。
  2. 操作内存中的 RouteDefinitionRepository(路由定义仓库)。
  3. 提供供外部调用的 API 方法来动态管理网关路由。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
/**
* 1)实现一个Spring提供的事件推送接口ApplicationEventPublisherAware
* 2)提供动态路由的基础方法,可通过获取bean操作该类的方法。该类提供新增路由、更新路由、删除路由,然后实现发布的功能。
*/
@Slf4j
@Service
public class DynamicRouteRefresh implements ApplicationEventPublisherAware {
@Autowired
private InMemoryRouteDefinitionRepository memoryRouteDefinitionRepository;

@Autowired
private ApplicationEventPublisher publisher;

// 设置事件发布器
@Override
public void setApplicationEventPublisher(ApplicationEventPublisher applicationEventPublisher) {
this.publisher = applicationEventPublisher;
}

// 删除指定 id 的路由定义,并发布刷新事件
public String delete(String id) {
try {
log.info("gateway delete route id {}", id);
memoryRouteDefinitionRepository.delete(Mono.just(id)).doOnError(error -> log.error("删除失败,id:{}", id))
.doAfterTerminate(() -> this.publisher.publishEvent(new RefreshRoutesEvent(this))).subscribe();
return "delete success";
} catch (Exception e) {
return "delete fail";
}
}

// 批量更新路由列表
public String updateList(List<RouteDefinition> definitions) {
log.info("gateway update route {}", definitions);
// 删除缓存routerDefinition
memoryRouteDefinitionRepository.getRouteDefinitions()
.filter(it -> !it.getId().startsWith("ReactiveCompositeDiscoveryClient_"))
.doOnNext(routeDefinition -> {
log.info("delete routeDefinition:{}", routeDefinition);
delete(routeDefinition.getId());
}).doAfterTerminate(() -> definitions.forEach(this::save)).subscribe();
return "success";
}

// 保存一个新的路由定义
public String save(RouteDefinition definition) {
log.info("gateway add route {}", definition);
try {
memoryRouteDefinitionRepository.save(Mono.just(definition))
.doAfterTerminate(() -> this.publisher.publishEvent(new RefreshRoutesEvent(this))).subscribe();
return "success";
} catch (Exception e) {
return "update route fail";
}
}
}
方法名 作用 说明
setApplicationEventPublisher(…) 设置事件发布器 实现 ApplicationEventPublisherAware 接口后,Spring 容器启动时会注入 publisher。
delete() 删除指定 id 的路由定义,并发布刷新事件 删除路由后立即触发 RefreshRoutesEvent,使网关路由立即更新。
updateList() 批量更新路由列表 会先删除旧路由(非注册中心自动生成的),再逐个新增,并发布刷新事件。
save() 保存一个新的路由定义 添加到内存中并触发刷新事件,使其立即生效。

10.5 GatewayBootApplication

这是整个网关服务的入口类,启动时会初始化 Spring Boot 应用环境,并开启 Feign 客户端支持,注册微服务间远程调用能力。

11. goodskill-auth (auth登录以及授权模块)

  • auth-service
    • 基于Sa-Token框架的用户登录授权服务
  • oauth2-auth-client
    • oauth2.0登录客户端
  • oauth2-auth-server
    • oauth2.0登录授权服务端,自定义的登录授权服务
  • oauth2-resource-server
    • oauth2.0资源服务端,自定义的登录授权服务

11.1 auth-service (基于Sa-Token框架的用户登录授权服务)

11.1.1 config包

  • EncryptConfig: 加密配置
  • StpInterfaceImpl

StpInterfaceImpl

是一个 Sa-Token 框架的自定义权限加载实现类,用于在用户登录后,动态加载其拥有的 角色 和 权限码

  • 它实现了 StpInterface 接口,是 权限控制核心的一部分。
  • “当你需要判断某个用户的角色或权限时,我来告诉你他有哪些。”
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
/**
* sa-token自定义权限加载接口实现类
*/
@Component
public class StpInterfaceImpl implements StpInterface {
@Resource
private UserRoleService userRoleService;
@Resource
private RolePermissionService rolePermissionService;
@Resource
private RoleService roleService;
@Resource
private PermissionService permissionService;

// 返回一个账号所拥有的权限码集合
@Override
public List<String> getPermissionList(Object loginId, String loginType) {
// 1. 声明权限码集合
List<String> permissionList = new ArrayList<>();

// 2. 遍历角色列表,查询拥有的权限码
for (String roleCode : getRoleList(loginId, loginType)) {
SaSession roleSession = SaSessionCustomUtil.getSessionById("role-" + roleCode);
List<String> list = roleSession.get("Permission_List", () -> {
List<Integer> roleIdList = roleService.list(Wrappers.<Role>lambdaQuery().eq(Role::getRoleCode, roleCode))
.stream().map(Role::getRoleId).collect(Collectors.toList());
List<Integer> permissionIdList = rolePermissionService.list(Wrappers.<RolePermission>lambdaQuery().in(RolePermission::getRoleId, roleIdList))
.stream().map(RolePermission::getPermissionId).distinct().collect(Collectors.toList());
List<Permission> permissions = permissionService.list(Wrappers.<Permission>lambdaQuery().in(Permission::getPermissionId, permissionIdList));
return permissions.stream().map(Permission::getPermissionCode).collect(Collectors.toList());
});
permissionList.addAll(list);
}
// 3. 返回权限码集合
return permissionList;
}

// 返回一个账号所拥有的角色标识集合 (权限与角色可分开校验)
@Override
public List<String> getRoleList(Object loginId, String loginType) {
SaSession session = StpUtil.getSessionByLoginId(loginId);
return session.get("Role_List", () -> {
// 从数据库查询这个账号id拥有的角色列表
List<UserRole> userRoleList = userRoleService.list(Wrappers.<UserRole>lambdaQuery().eq(UserRole::getUserId, loginId));
List<Integer> roleIds = userRoleList.stream().map(UserRole::getRoleId).collect(Collectors.toList());
return roleService.list(Wrappers.<Role>lambdaQuery().in(Role::getRoleId, roleIds)).stream().map(Role::getRoleCode)
.collect(Collectors.toList());
});
}
}
  • 获取指定用户的权限码集合(权限字符串列表)。
  • 获取指定用户的所有角色代码集合

11.1.2 controller

  • LoginController
  • UserController

LoginController

用于处理登录相关操作的控制器类,主要用于提供接口来测试登录状态、执行登录、退出登录、注册等功能

接口路径 方法 功能简介
/isLogin GET 检查是否已登录
/tokenInfo GET 获取当前登录 token 信息
/logout POST 用户登出
/register POST 用户注册
/login POST 用户登录并返回 token

UserController

用于用户管理的控制器类,提供了和用户角色、权限相关的接口,核心围绕 角色分配、用户信息获取、权限/角色查询 展开

  • 提供对用户信息、角色和权限的管理能力,依赖 UserService 和 StpInterface 实现用户信息的查询和权限体系的接入。

11.1.3 entity

实体类名 作用说明 备注
Permission 权限表实体类,表示系统中的具体权限项 用于控制按钮/菜单权限等,配合角色使用
Role 角色表实体类,定义系统中不同的角色 和权限、用户关联,用于授权管理
RolePermission 角色与权限的中间表实体类,用于维护角色拥有的权限 多对多关联表
User 用户表实体类,保存用户基本信息 核心用户实体类
UserAuthAccount 用户第三方授权账号表,保存绑定的第三方登录信息 用于实现微信、QQ等第三方登录
UserRole 用户与角色的中间表实体类,用于维护用户拥有的角色 多对多关联表

11.1.4 pojo

  • UserDTO: 用户DTO
  • UserInfoVO: 用户信息视图对象

11.1.5 mapper

这些mapper都继承了mybatis-plus中的BaseMapper接口,实现了基本的增删改查

  • PermissionMapper: 权限表mapper接口
  • RoleMapper: 角色表mapper接口
  • RolePermissionMapper: 角色权限表mapper接口
  • UserAuthAccountMapper: 用户授权账号表 Mapper 接口
  • UserMapper: 用户表mapper接口
  • UserRoleMapper: 用户角色表mapper接口

11.1.6 service接口和实现类

service接口

这些service接口继承了mybatis-plus中的IService接口,实现了基本的增删改查

  • PermissionService
  • RolePermissionService
  • RoleService
  • UserAccountService
  • UserAuthAccountService: 用户授权账号表服务类
  • UserRoleService
  • UserService

ServiceImpl服务实现类

这些实现类同样继承了mybatis-plus中的ServiceImpl

  • PermissionServiceImpl
  • RolePermissionServiceImpl
  • RoleServiceImpl
  • UserAccountServiceImpl
  • UserRoleServiceImpl

UserAuthAccountServiceImpl

是一个 用户第三方授权账号服务的实现类
该类的主要职责是处理用户与第三方账号(如微信、QQ、GitHub等)的关联逻辑,主要包括:

  • 根据第三方账号查找对应的用户信息。
  • 判断某个第三方账号是否已经绑定过系统用户。

UserServiceImpl

用户管理的核心服务类,主要负责处理用户的注册、查询、权限角色管理、登录状态更新等逻辑。
它继承了 ServiceImpl<UserMapper, User>,实现了 UserService 接口,借助 MyBatis-Plus 的通用 Service 功能,并集成了用户、角色、权限等多个模块的业务逻辑。

方法名 方法作用描述
register 用户注册,保存加密密码和用户名到数据库
findRoles 根据账号查找用户的所有角色
findPermissions 根据账号查找用户的所有权限(通过角色权限中间表)
findByUserAccount 根据账号查找对应的用户实体
getUserInfoById 根据用户 ID 查询用户信息,并隐藏密码
page 获取用户分页信息(使用 MyBatis-Plus 提供的分页)
removeById 根据用户 ID 删除用户
updateLastLoginTime 更新用户最近一次登录时间
checkPassword 校验输入密码是否与加密密码匹配
addRole 为指定用户添加一个角色(校验角色是否存在)

11.1.7 AuthApplication

认证服务的启动类, 作用是启动整个认证模块(auth 模块)的应用

11.2 oauth2-auth-client (oauth2.0登录客户端)

  • OAuth2LoginController 是一个用于 展示 OAuth2 登录成功后用户信息的控制器,它的作用是处理用户登录成功后的页面请求,并将登录用户的信息展示在页面上
  • OAuth2AuthClientApplication: OAuth2.0授权客户端服务的启动类

11.3 oauth2-auth-service (oauth2.0登录授权服务端,自定义的登录授权服务)

SecurityConfig

SecurityConfig 类是一个 Spring Security 配置类,用于配置 OAuth2 认证与授权服务器的安全策略,以及处理 资源服务器 和 默认的用户认证

具体来说,它负责:

  1. 配置 授权服务器的安全策略。
  2. 配置 资源服务器(JWT 验证)。
  3. 配置 用户认证与登录。
  4. 设置 客户端和 公钥私钥对。

SimpleController

是一个 Spring Boot 控制器,用于处理一个简单的 GET 请求。其主要功能是通过 Spring Security 的 @AuthenticationPrincipal 注解获取当前认证用户的信息,并返回该用户的名称

UserInfoController

是一个 Spring Boot REST 控制器,它定义了两个端点:/user 和 /whoami,并提供了一些用户信息的功能

OAuth2AuthorizationServerApplication

作用是启动一个 OAuth2 授权服务器

11.4 oauth2-resource-server (oauth2.0资源服务端,自定义的登录授权服务)

  • UserInfoController: 提供关于用户的两类信息:一是模拟的用户数据,二是当前已认证用户的数据。
    • /user 接口提供模拟的用户数据,返回一个静态的 JSON 响应。
    • /whoami 接口用于获取当前登录用户的信息,返回认证用户的相关信息。
  • OAuth2ResourceServerApplication: OAuth2.0资源服务器启动类

12. goodskill-ai (AI机器人聊天服务)

12.1 controller

  • AssistantController
  • ChatRequest

AssistantController

主要用于处理与用户的对话(聊天)请求。具体来说,它的作用是为用户提供一个基于 Web 的聊天接口,允许用户与一个助手进行互动。类中定义的请求接口会通过 UserSupportAssistant 来处理用户的输入并返回聊天信息。

  • 作为一个实时的用户助手聊天接口,处理用户消息并实时推送聊天回复。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@RequestMapping("/api/assistant")
@RestController
public class AssistantController {

private final UserSupportAssistant agent;

public AssistantController(UserSupportAssistant agent) {
this.agent = agent;
}

@RequestMapping(path="/chat", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
public Flux<String> chat(@RequestParam String chatId, @RequestParam String userMessage) {
return agent.chat(chatId, userMessage);
}
}
  • 接口功能:/api/assistant/chat 提供了一个与用户聊天的接口。用户通过传递 chatId 和 userMessage 进行交互,助手会实时返回聊天消息。
  • 流式响应:通过 Flux<String> 和 SSE,实现了一个实时对话的机制。客户端可以逐步接收来自服务器的消息,而不需要多次请求。

ChatRequest

ChatRequest 类的作用是表示一个聊天请求,通常用于向聊天模型(如 OpenAI 的 GPT 模型)发送用户消息,并请求模型生成相应的回复。这个类包含了请求模型所需的关键信息,比如模型的选择、消息的内容、是否启用流式响应等。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
public class ChatRequest {
private String model;
private List<Message> messages;
private boolean stream;

// Getters and Setters
public String getModel() {
return model;
}

public void setModel(String model) {
this.model = model;
}

public List<Message> getMessages() {
return messages;
}

public void setMessages(List<Message> messages) {
this.messages = messages;
}

public boolean isStream() {
return stream;
}

public void setStream(boolean stream) {
this.stream = stream;
}

public static class Message {
private String role;
private String content;

// Getters and Setters
public String getRole() {
return role;
}

public void setRole(String role) {
this.role = role;
}

public String getContent() {
return content;
}

public void setContent(String content) {
this.content = content;
}
}
}
  • 请求模型生成聊天内容:ChatRequest 类表示向聊天模型发送的请求,包括模型类型、消息列表和流式响应的设置。
  • 构建对话上下文:通过 messages 列表,可以传递多轮对话的上下文,确保模型根据历史对话生成合适的回应。
  • 流式聊天:通过 stream 设置,可以实现实时的对话流式返回,使得消息能够逐步推送给客户端,支持更流畅的聊天体验。

12.2 service

  • SeckillMockFeignClient
  • LoggingAdvisor
  • SeckillTools
  • UserSupportAssistant

SeckillMockFeignClient

是一个 Feign客户端接口,用于模拟秒杀场景并与 goodskill-web 服务进行通信。

  • 该类包含多个模拟秒杀场景的接口,每个方法对应一个特定的秒杀实现方案,如同步锁、分布式锁、消息队列、数据库操作等。这些接口主要用于模拟不同技术栈下的秒杀场景,并返回相应的秒杀操作结果。

LoggingAdvisor

实现了 RequestResponseAdvisor 接口的类,用于对请求进行日志记录。这通常用于拦截和处理请求数据,在请求被进一步处理之前或之后执行一些操作,比如日志记录、监控等。

  • 主要作用是拦截请求并输出请求的日志信息,帮助开发人员了解和跟踪请求的细节。这对于调试和监控系统的运行状态非常有用。

SeckillTools

主要用于封装和暴露与“秒杀”功能相关的工具方法,以便以函数式编程的方式调用

这是一个功能组件类,用于:

  • 开启模拟的秒杀活动(使用某种策略如数据库原子更新)

  • 获取某个秒杀活动对应任务的耗时统计信息

  • 使用 @Bean + Function<…> 的方式暴露方法,适用于函数式编程接口,像 Spring Cloud Function 这类平台可以自动扫描这些函数用于服务注册或事件驱动编程。

  • 使用了 CompletableFuture.supplyAsync() 以异步方式调用远程服务,避免阻塞。

UserSupportAssistant

UserSupportAssistant 是一个 服务类(@Service),其作用是通过与 ChatClient 的交互,充当用户支持聊天代理,提供与秒杀活动相关的功能,如查询商品详情、开启和重置秒杀活动等

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44

@Service
public class UserSupportAssistant {

private final ChatClient chatClient;

public UserSupportAssistant(ChatClient.Builder modelBuilder, VectorStore vectorStore, ChatMemory chatMemory) {
// @formatter:off
this.chatClient = modelBuilder
.defaultSystem("""
您是用户聊天支持代理。请以友好、乐于助人且愉快的方式来回复。
您正在通过在线聊天系统与用户互动。
您能够支持已有秒杀商品详情查询、开启秒杀活动、重置秒杀活动等操作,其余功能将在后续版本中添加,如果用户问的问题不支持请告知详情。
在提供有关秒杀活动查询、开启秒杀活动、重置秒杀活动等操作之前,您必须始终从用户处获取以下信息:秒杀活动id、秒杀商品数量、请求次数。
如果需要提前结束秒杀活动,您必须在继续之前征得用户同意。
使用提供的功能获取秒杀活动以及商品详细信息、开启秒杀活动和重置秒杀。
用户开始秒杀活动后,等待10秒,然后调用 获取任务耗时统计信息 函数,最后将函数结果返回给用户。
如果需要,您可以调用相应函数辅助完成。
请讲中文。
今天的日期是 {current_date}.
""")
.defaultAdvisors(
// Chat Memory
new PromptChatMemoryAdvisor(chatMemory),
// new VectorStoreChatMemoryAdvisor(vectorStore)),
// RAG
new QuestionAnswerAdvisor(vectorStore, SearchRequest.builder().topK(4).similarityThresholdAll().build()),
// new QuestionAnswerAdvisor(vectorStore, SearchRequest.defaults()
// .withFilterExpression("'documentType' == 'terms-of-service' && region in ['EU', 'US']")),
new LoggingAdvisor())
// FUNCTION CALLING
.defaultFunctions("startSeckill", "getTaskTimeInfo")
.build();
// @formatter:on
}

public Flux<String> chat(String chatId, String userMessageContent) {
return this.chatClient.prompt().system(s -> s.param("current_date", LocalDate.now().toString()))
.user(userMessageContent)
.advisors(a -> a.param(CHAT_MEMORY_CONVERSATION_ID_KEY, chatId).param(CHAT_MEMORY_RETRIEVE_SIZE_KEY, 100))
.stream()
.content();
}
}

功能

  • 它是一个智能聊天助手,处理用户的秒杀活动查询、启动和重置请求。
  • 通过 ChatClient,该类能够与用户进行互动,自动收集信息(如秒杀活动ID、商品数量、请求次数),并在后台执行相关功能。
  • 同时支持通过“聊天”方式与用户进行交流,提供友好、乐于助人的服务。

架构

  • 依赖于 ChatClient、VectorStore 和 ChatMemory 组件,其中 ChatClient 是与聊天系统交互的核心。
  • 该类定义了一个以聊天为基础的交互式功能服务,帮助用户执行和查询秒杀活动相关操作。

12.3 AiBotApplication

用于启动一个AI机器人应用的启动类

  • AiBotApplication 是AI机器人应用的核心启动类,负责配置和初始化各个组件,并处理应用启动过程中的关键配置。

13. 从前往后分析

13.1 goodskill-order-provider

A. OrderService

package com.goodskill.order.api

基于 Spring Cloud OpenFeign 实现的 订单微服务 (goodskill-order) 远程调用接口,并提供了 熔断降级处理。

亮点:

  1. 基于 Feign 的声明式服务调用
    • 使用 Spring Cloud OpenFeign 实现声明式 HTTP 客户端,简化了微服务之间的远程调用提高了代码可读性和开发效率,屏蔽了底层 HTTP 通信细节,符合微服务架构中服务间通信的简洁性要求。
  2. 服务熔断与降级机制
    • 集成了服务熔断和降级功能,通过 fallback 指定备用方案,确保服务调用失败时系统仍能平稳运行。在高并发或服务不可用场景下,避免了请求堆积和系统崩溃,提升了系统的容错能力和稳定性,特别适合秒杀这种高负载场景。
  3. 灵活的降级策略设计
    • 降级策略根据业务需求差异化处理,既有默认返回值,也有异常抛出,体现了设计的灵活性。根据接口的重要性和业务逻辑,合理选择降级方式

13.2 goodskill-seckill-provider

A. RedisService

package com.goodskill.service.common

RedisService 类是一个用于操作秒杀相关 Redis 缓存的工具类,它封装了对 Redis 的读写逻辑,主要目的是提升查询效率、减轻数据库压力、标识秒杀状态等

亮点:

  1. 使用了 Redis 的过期机制和 setIfAbsent 机制,确保高并发下的操作一致性。
  2. 缓存穿透控制:缓存未命中时自动加载数据库并回填,防止大量请求直击数据库。
  3. 使用 Redis 的 setIfAbsent 操作实现分布式环境下秒杀结束标志的原子性设置,防止重复触发逻辑。

B. StateMachineConfig

package com.goodskill.service.config

StateMachineConfig 的主要作用是为秒杀系统定义和管理一个状态机,用于控制秒杀活动的生命周期。状态机通过预定义的状态和事件驱动状态转换,确保活动状态的有序性和一致性。具体作用包括:

  1. 状态定义: 定义了秒杀活动的各种状态(如 INIT、IN_PROGRESS、CALCULATING、END、INTERRUPTTED)。
  2. 状态转换: 通过事件(如 ACTIVITY_START、ACTIVITY_END)驱动状态之间的跳转,例如从 INIT 到 IN_PROGRESS。
  3. 状态持久化: 使用 Redis 存储状态机的上下文,支持分布式环境下的状态恢复和一致性。
  4. 状态监控: 通过监听器记录状态变化和未接受事件,便于调试和日志追踪。

亮点:

  1. 使用 Spring Statemachine 框架实现状态机,清晰定义了秒杀活动的状态和转换规则。状态机将复杂的业务流程抽象为状态和事件,降低了代码复杂度,提高了流程的可控性和可维护性
  2. 集成 Redis 实现状态机的持久化,确保分布式环境下状态的一致性和可恢复性。在微服务架构中,节点可能故障或重启,通过 Redis 持久化状态机上下文,可以保证状态不会丢失,支持高可用性和分布式一致性。

C. PreRequestHandler

package com.goodskill.service.handler

包含以下这些类:

  • AbstractPreRequestHandler
  • DatabasePreRequestHandler
  • MongoPreRequestHandler
  • PreRequestHandler
  • PreRequestPipeline
  • RedisPreRequestHandler
  • StateMachinePreRequestHandler

秒杀系统的特点是高并发、数据敏感、状态复杂,所以在每次活动开始前,需要完成一系列的准备工作, 这些 Handler 就像“启动前检查清单”,每个负责一个环节,从数据、缓存、状态、历史记录等多个维度做好准备,确保秒杀系统在高并发下也能稳定运行。

亮点:

  1. 使用责任链模式(Chain of Responsibility)实现预处理逻辑的解耦和有序执行。解耦了预处理逻辑,使每个处理器专注于特定任务,易于扩展和维护
  2. 覆盖数据库、Redis 缓存、MongoDB 和状态机等多维度的数据初始化,确保系统状态全面一致。在高并发秒杀场景下,多数据源的一致性至关重要,这种全面覆盖的设计有效避免了历史数据干扰和状态不一致问题。
  3. 预处理管道具备容错能力,单个处理器出错不会中断整体流程。提高了系统的健壮性,确保即使部分预处理失败,也不会影响整体流程,同时通过异常和日志便于问题定位。

D. SeckillServiceImpl

package com.goodskill.service.impl.dubbo

秒杀系统的核心服务实现类

亮点:

  1. 使用 Apache Dubbo 实现微服务通信,支持远程调用和分布式部署。
  2. Redis 缓存优化: 利用 Redis 缓存秒杀活动列表和热点数据,减少数据库压力。
  3. 事务性库存管理: 使用 @Transactional 确保库存扣减和记录插入的原子性,防止超卖。在高并发环境下保证了数据一致性,避免了库存超卖或少卖问题
  4. 通过状态机控制秒杀活动生命周期,确保状态转换的正确性。状态机提供了统一的流程控制,防止非法状态跳转,提升了业务逻辑的可靠性。
  5. 多数据源查询支持: 支持从数据库和 MongoDB 查询成功记录,增强了数据访问的灵活性。适应了分布式系统中数据分散的场景,提供了容错机制(MongoDB 不可用时记录日志并抛异常)。
  6. 预处理管道集成: 通过 PreRequestPipeline 统一调度预处理逻辑,确保活动开始前的资源准备。

E. GoodsEsServiceImpl

package com.goodskill.service.impl

是一个基于Elasticsearch 的服务实现类,负责对商品信息进行 Elasticsearch 的增删查操作

亮点:

  1. Elasticsearch 集成: 使用 Spring Data Elasticsearch 集成 Elasticsearch,提供了高效的全文搜索和索引管理功能。利用 Elasticsearch 的分布式搜索能力,支持秒杀系统中商品信息的高性能查询,尤其适合模糊搜索和实时索引更新场景。
  2. 高亮搜索功能: 实现商品名称的模糊搜索,并对匹配关键字进行高亮显示,提升了搜索结果的可读性。
  3. 分页与结果限制: 搜索结果限制为最多 3 条,并支持分页查询,优化了性能和返回数据量. 在高并发场景下,限制返回数据量避免了过多的结果处理开销,同时满足了快速响应的需求。

F. mock.strategy秒杀策略

package com.goodskill.service.mock.strategy.impl

策略对比表格

策略名称 核心机制 优点 缺点 适用场景
AtomicUpdateStrategy 数据库原子更新(乐观锁) 简单高效,依赖数据库事务保证一致性 高并发下数据库压力大,可能出现锁冲突 单机或小规模分布式系统
AtomicWithCanalStrategy 数据库原子更新 + Canal 监听 binlog 结合数据库一致性和异步处理(如通知),细粒度锁降低冲突 实现复杂,依赖 Canal 配置和部署 需要异步处理的分布式系统
RedisMongoReactiveStrategy Redis 计数 + 异步 MongoDB 保存 高性能,异步解耦,库存检查无需数据库 Redis 数据一致性需额外保障,复杂性增加 高并发、需要快速响应的分布式系统
RedissonStrategy Redisson 分布式锁 分布式环境下线程安全,简单易用,可重入锁支持 锁获取和释放有网络开销,性能稍逊 多节点分布式系统
SentinelLimitStrategy Sentinel 限流 保护系统资源,防止过载,降级处理优雅 不直接解决库存一致性,需配合其他机制 高流量场景,需保护后端服务
SynchronizedLockStrategy Java 本地锁(synchronized) 实现简单,适用于单机,按商品加锁减少锁粒度 不支持分布式部署,高并发下性能瓶颈明显 单机或低并发场景
ZookeeperLockStrategy Zookeeper 分布式锁 强一致性,可重入阻塞锁,适合分布式环境 Zookeeper 依赖部署,锁竞争时延迟较高 分布式系统,强调一致性而非极致性能

G. StreamMessageConsumer

package com.goodskill.service.mq

基于 Spring Cloud Stream 的消息消费者配置类,用于处理通过消息队列(如 RabbitMQ)接收的秒杀请求

亮点:

  1. Spring Cloud Stream 集成: 使用 Spring Cloud Stream 实现消息驱动的微服务架构,简化了消息消费的配置。利用 Spring Cloud Stream 的绑定机制(默认绑定到 RabbitMQ 或其他 MQ),无需手动配置连接和队列,降低了开发复杂性,提升了代码可读性。
  2. 异步解耦设计: 通过消息队列实现秒杀请求的异步处理,解耦了请求发送端和执行端。在高并发场景下,异步处理提高了系统的响应速度和吞吐量,避免了请求堆积导致的性能瓶颈

H. StateMachineService

package com.goodskill.service.util

从你提供的 StateMachineService 类代码来看,这是一个基于 Spring StateMachine 实现的状态机管理服务,用于在秒杀系统中协调和管理每个秒杀活动(以 seckillId 区分)的状态生命周期

亮点:

  1. Spring StateMachine 集成: 利用 Spring StateMachine 框架实现状态管理,清晰定义秒杀活动的生命周期。状态机将复杂的业务流程抽象为状态和事件,降低了逻辑复杂性,提升了流程的可控性和可维护性。
  2. 分布式状态持久化: 通过 Redis 持久化状态机上下文,支持分布式环境下的状态一致性和恢复。在微服务架构中,节点故障或重启后仍能恢复状态,确保分布式系统的高可用性和一致性。
  3. 并发安全的实例管理: 使用 ConcurrentHashMap 缓存状态机实例,支持多线程环境下的并发访问。在高并发场景下,避免了状态机实例的重复创建或覆盖,保证了线程安全和性能。

I. ZookeeperLockUtil

package com.goodskill.service.util

ZookeeperLockUtil 的主要作用是提供基于 ZooKeeper 的分布式锁功能

亮点:

  1. Curator 框架集成: 使用 Apache Curator 简化 ZooKeeper 的分布式锁实现,提供高级抽象和健壮性。
  2. 分布式锁支持: 提供基于 ZooKeeper 的分布式锁功能,确保多节点环境下的资源互斥访问。在分布式系统中(如微服务架构),通过 ZooKeeper 的强一致性保证了锁的正确性,防止并发冲突(如库存超卖)。
  3. 超时与容错机制: 锁获取设置了超时时间,并通过重试策略提升容错性。超时机制防止锁竞争时的死锁,重试策略增强了连接 ZooKeeper 的鲁棒性,提升了系统稳定性。

13.3 goodskill-web

A. GlobalExceptionHandler

package com.goodskill.web.config

基于 Spring Boot 的全局异常处理类,利用 @RestControllerAdvice 和 @ExceptionHandler 注解集中处理应用中的异常,并返回统一的响应格式。

亮点:

  1. 全局异常处理: 使用 @RestControllerAdvice 实现全局异常捕获,集中管理应用中的错误处理逻辑。避免了在每个控制器中重复编写异常处理代码,降低了代码冗余,提高了可维护性。
  2. 统一的响应格式: 返回标准化的 ApiResult 对象,确保错误响应的格式一致。
  3. 细粒度的异常分类: 针对不同类型的异常(通用异常、参数绑定异常、校验异常)提供特定处理逻辑。根据异常类型返回更具体的错误信息(如字段级别的校验失败原因),提高了错误处理的精准性和实用性。

B. SeckillMockController

package com.goodskill.web.controller

用于模拟秒杀场景的控制器类,通过 RESTful 接口提供多种秒杀策略的测试入口。它集成了多种技术(如 Dubbo、消息队列、线程池、Redis 等)

亮点:

  1. 多种秒杀策略支持: 通过单一控制器支持多种秒杀策略(如同步锁、Redisson、Kafka、RabbitMQ、Sentinel 等),体现了灵活性和可扩展性。
  2. 线程池并发模拟: 使用 ThreadPoolTaskExecutor 模拟高并发请求,支持动态调整线程池参数。
  3. 消息队列集成: 集成 Kafka 和 RabbitMQ(通过 StreamBridge)实现异步秒杀请求,提升响应速度。异步处理解耦了请求发送和实际执行,提高了系统的吞吐量和可扩展性,适合高并发场景。

13.4 goodskill-job

ElasticJob 的分片机制

package com.goodskill.job.simple

亮点:

  1. ElasticJob 分片机制的分布式任务拆分
    • ElasticJob 通过分片机制将任务分布式拆分到多个节点执行,MyJob 类中的 ShardingContext 参数允许任务根据 shardingItem(分片项)动态分配不同的执行逻辑。这种设计特别适合处理大规模数据同步任务,例如将商品数据同步到 Elasticsearch。
      • 负载均衡: 分片机制结合 Zookeeper 注册中心,可以将任务均匀分配到集群中的多个节点,避免单点压力。
      • 可扩展性: 当数据量或节点数增加时,只需调整分片数量(shardingTotalCount)和逻辑,无需大幅改动代码。
      • 容错性: ElasticJob 自带任务失败重试和节点故障转移功能,确保任务执行的可靠性。
  2. 分布式环境下高效同步商品数据到 Elasticsearch
    • MyJob 通过 Dubbo 远程调用 GoodsService 获取商品数据,并利用 parallelStream() 并行处理数据转换,最后通过 goodsEsService.saveBatch() 批量更新 Elasticsearch 索引。这种设计兼顾了分布式调用和批量操作的效率。
      • 高性能: 并行流和批量操作显著提升了数据处理的吞吐量。
      • 解耦性: 通过 Dubbo 调用商品服务,任务模块与业务逻辑分离,便于维护和扩展。

13.5 goodskill-gateway

A. gateway.config 网关配置

com.goodskill.gateway.config

本项目基于 Spring Cloud Gateway 和 Sa-Token 实现了统一认证网关,封装了登录鉴权、路由转发、权限控制、白名单机制等核心功能,提升了系统的安全性和可维护性。通过注入自定义权限接口并结合远程调用,完成了微服务间认证授权体系的统一设计。

功能模块 类名 优势
路由转发 GatewayConfiguration 实现集中式网关路由、负载均衡、服务发现、前缀剥离等,提高接口统一性与可维护性
安全认证 SaTokenConfiguration 提供全局统一认证入口,支持路径匹配、白名单控制、用户信息传递等,保障安全、解耦业务
权限控制 StpInterfaceImpl 支持远程权限查询、动态角色管理,实现细粒度权限控制,支持未来权限管理升级

亮点:

  1. GatewayConfiguration —— 网关路由配置类
    • 集中式路由管理: 通过 RouteLocatorBuilder 明确配置了多个微服务(如 goodskill-order、goodskill-auth 等)的路由路径。
    • 服务注册发现集成:使用了 服务注册中心(如 Nacos、Eureka),通过负载均衡调用后端服务,便于服务治理。
  2. SaTokenConfiguration —— Sa-Token 全局过滤器配置类
    • 全局认证统一入口:通过 SaReactorFilter 实现全局请求拦截,在业务处理前进行统一认证。
    • 认证增强:登录校验:StpUtil.checkLogin() 强制要求登录状态。
    • 异常统一处理:所有认证异常都由 .setError(…) 捕捉并返回统一格式响应,提升接口一致性。
  3. StpInterfaceImpl —— Sa-Token 权限与角色提供器
    • 动态权限加载:实现 StpInterface 接口,结合远程 AuthFeignClient,实现权限和角色的动态查询。

B. gateway.service

package com.goodskill.gateway.service

微服务网关动态路由的核心,配合 Spring Cloud Gateway 和 Nacos,实现了路由配置的动态管理与热更新

在网关模块中设计并实现了基于 Nacos 配置中心的动态路由管理机制,支持路由配置的热更新与自动同步。通过监听配置中心变更事件,结合 Spring Cloud Gateway 的事件机制,完成网关路由的实时刷新,大幅提升系统的可维护性和扩展性,确保网关在高可用和服务频繁变更场景下依然保持路由一致性与稳定性。

亮点:

  1. DynamicRouteConfigWatcher —— 动态路由监听器
    • 自动监听 Nacos 路由配置变更: 实现了网关无感知自动刷新,不需要重启服务,真正实现配置热更新。
    • 与 DynamicRouteRefresh 联动,实时更新路由
    • 启动时自动拉取已有路由配置
  2. DynamicRouteRefresh —— 动态路由刷新服务类
    • 利用 Spring 提供的事件机制 RefreshRoutesEvent 通知 Spring Cloud Gateway 刷新路由
    • 路由删除与更新逻辑分离: updateList() 实现全量刷新逻辑,先删除已有路由,再重新添加,实现幂等化路由更新。
模块 类名 功能亮点 微服务优势
动态路由监听器 DynamicRouteConfigWatcher 监听 Nacos 变更、启动时初始化路由 实现动态感知路由配置变更,无需重启网关
路由刷新服务 DynamicRouteRefresh 内存路由维护、事件驱动刷新 高性能路由更新机制,支持大规模服务路由动态管理

13.6 goodskill-ai

1
2
3
4
5
6
7
用户请求 → AssistantController.chat(chatId, userMessage)

UserSupportAssistant.chat(...)

ChatClient:生成回复 + 调用函数 + 管理上下文

返回 Flux<String> 流式消息

A. AssistantController

package com.goodskill.ai.controller

功能点 说明
聊天入口 提供 /api/assistant/chat 接口作为用户与 AI 助手的对话入口
调用 AI 服务 将用户输入交由 UserSupportAssistant(核心 AI 服务类)处理
流式响应 使用 Flux<String> 结合 TEXT_EVENT_STREAM_VALUE 返回流式消息,实现类似 ChatGPT 的“实时回复”体验

亮点:

  1. 使用 Spring WebFlux 实现 流式响应(Server-Sent Events, SSE)
    • 返回 Flux<String> 实现“逐条消息实时推送”,提升前端交互体验。
    • 减少用户等待时间,模拟“打字式”AI回复效果,交互更自然。

B. UserSupportAssistant

package com.goodskill.ai.service

功能 描述
AI 助手服务类 这是 AI 微服务中真正处理用户聊天请求的核心逻辑类,控制了聊天流程
AI 模型驱动 使用 ChatClient 封装大语言模型(如 ChatGPT),实现自然语言对话
支持 RAG 检索式问答 集成向量数据库(VectorStore)来支持上下文问答,增强对话智能
记忆管理 集成 ChatMemory 实现多轮对话记忆
函数调用 支持通过对话触发系统函数(如:开启秒杀、获取任务耗时)

亮点:

  1. 系统提示词 System Prompt 灵活配置
  2. RAG(Retrieval-Augmented Generation)增强问答能力
  3. 支持函数式调用: 模型可识别用户意图并触发如 startSeckill()、getTaskTimeInfo() 等后端实际方法 → 实现 对话即操作 的交互体验。
  4. 多轮对话上下文管理: 利用 ChatMemory 和 PromptChatMemoryAdvisor 实现记忆追踪,模型可记住用户上下文信息。
  5. 响应式编程: 返回 Flux<String> 流式响应,提升对话实时性与用户体验。

14. 项目亮点

  1. 实现多种高并发秒杀策略,支持同步与异步双通道,涵盖Sychronized同步锁、数据库乐观锁、Redisson分布式锁、RabbitMQ消息队列、Redis 计数 + MongoDB 异步持久化,全面保障高并发场景下的数据一致性
  2. 基于 Nacos 实现统一配置中心管理;使用 OpenFeign 实现微服务之间的远程调用和服务熔断,并设计相应的服务降级逻辑处理;使用 Gateway 构建微服务 API 网关,支持动态路由、配置热更新与统一身份认证
  3. 采用基于责任链模式的预处理器机制,在秒杀前对数据库、缓存、状态机等进行初始化预处理
  4. 利用 Spring StateMachine 状态机定义和管理秒杀活动生命周期,统一控制活动状态,便于状态的集中维护
  5. 集成Elasticsearch实现高效商品检索,支持模糊搜索与关键词高亮,提升用户检索体验
  6. 基于Spring AI Alibaba构建秒杀AI对话服务,通过流式响应实现实时交互,支持多轮对话和智能问答触发秒杀操作

15. 亮点详解

1. 实现多种高并发秒杀策略,支持同步与异步双通道,涵盖Sychronized同步锁、数据库乐观锁、Redisson分布式锁、RabbitMQ消息队列、Redis 计数 + MongoDB 异步持久化,全面保障高并发场景下的数据一致性

1.1 分别介绍你的这些更新策略怎么实现的?

1.1.1 Sychronized同步锁

一、synchronized 是怎么用的?
SynchronizedLockStrategy 策略类中,synchronized 被用于 按商品粒度加锁,控制对同一个商品的并发请求

1
2
3
synchronized (seckillIdList.get(seckillId)) {
// 核心秒杀逻辑(扣库存 + 记录秒杀成功信息)
}
1
2
3
4
5
6
// 初始化锁对象(按商品 ID 分配一把锁)
Object seckillMonitor = seckillIdList.get(seckillId);
if (seckillMonitor == null) {
Object value = new Object();
seckillIdList.put(seckillId, value);
}

二、锁加在了哪里?
锁加在了 ConcurrentHashMap<Long, Object> seckillIdList 中的 商品 ID 对应的锁对象 上:

  • 每个商品 ID 有一把独立的锁。
  • 同一时间内,只能有一个线程执行同一个商品的秒杀逻辑
    总结:对每个商品进行单独的加锁

三、它解决了秒杀中的哪些问题?

  • 高并发下库存超卖: 同时只有一个线程能扣库存
  • 多线程重复下单: 同步锁保障同一商品只被一个线程修改
  • 数据不一致: 扣库存 + 记录成功信息在一个锁内,保证原子性

四、但它有什么局限?

  • 只适合单机: synchronized 是 JVM 内部锁,不适用于多台服务器部署
  • 性能差 在高并发下会导致线程等待,吞吐量低

总结一句话:
synchronized 同步锁在 GoodsKill 中加在了 商品 ID 映射的锁对象上,用于控制每个商品的并发请求,解决了并发下的库存超卖和重复下单问题,但仅适合单机部署的秒杀系统。

1.1.2 数据库乐观锁

一、什么是乐观锁?
乐观锁是一种并发控制策略,它假设多个事务并发操作时不会发生冲突,因此在操作数据时不加锁。只有在更新数据时才检查数据有没有被其他事务修改

二、GoodsKill 中的乐观锁使用场景

解决超卖问题:在 秒杀操作 → 减库存 这个流程中,如果多个用户同时抢购同一个商品,而没有并发控制,会导致库存被减成负数,即“超卖”。

乐观锁的作用就是:在更新库存时校验库存是否仍是“预期值”,如果不是,说明库存已经被别的线程改过了,就放弃更新,防止错误减库存。

三、实现方式(源码分析)
SeckillServiceImplreduceNumberInner方法中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
//真正减库存:插入秒杀记录,然后更新库存(带条件限制)。更新失败会抛出“活动已结束”异常。
@Transactional(rollbackFor = Exception.class)
@Override
public int reduceNumberInner(SuccessKilledDTO successKilled) {
SuccessKilled entity = BeanUtil.copyProperties(successKilled, SuccessKilled.class);
successKilledMapper.insert(entity);
Seckill wrapper = new Seckill();
wrapper.setSeckillId(entity.getSeckillId());
UpdateWrapper<Seckill> updateWrapper = new UpdateWrapper(wrapper);
updateWrapper.gt("end_time", entity.getCreateTime());
updateWrapper.lt("start_time", entity.getCreateTime());
updateWrapper.gt("number", 0); // 乐观锁校验点:库存必须大于 0
updateWrapper.setSql("number = number - 1");
int update = baseMapper.update(null, updateWrapper);
if (update <= 0) {
throw new SeckillCloseException("seckill is closed");
} else {
return update;
}
}
  • 商品库存 number > 0
  • 如果满足条件,则执行 SQL:UPDATE seckill SET number = number - 1
  • 如果 update == 0,说明:库存已经被抢完了,或者数据被其他线程修改了 → 当前线程更新失败(秒杀失败)。

四、总结
GoodsKill 使用乐观锁策略,确保在高并发下只有在库存仍然大于 0 时才允许减库存,从而有效防止超卖和数据不一致的问题,同时保持系统高性能运行。

1.1.3 Redisson分布式锁

一、Redisson 分布式锁加在了哪里?

在策略类 RedissonStrategy 中使用的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@Component
@Slf4j
public class RedissonStrategy implements GoodsKillStrategy {
@Autowired
private RedissonClient redissonClient;
@Autowired
private SeckillExecutor seckillExecutor;

/*
为当前商品获取一个分布式锁(锁名为商品 ID 的字符串)。
使用的是 可重入锁 RLock,多个节点下的并发用户都会竞争这个锁,确保同一时间只有一个线程能执行下面的逻辑。
*/
@Override
public void execute(SeckillMockRequestDTO requestDto) {
RLock lock = redissonClient.getLock(requestDto.getSeckillId() + "");
lock.lock();
try {
seckillExecutor.dealSeckill(requestDto.getSeckillId(), requestDto.getPhoneNumber(), REDISSION_LOCK.getName(), requestDto.getTaskId());
} finally {
lock.unlock();
}
}
}
  • getLock(requestDto.getSeckillId() + “”) 是为 每个商品 ID 动态生成一把分布式锁。
  • 加锁粒度是:商品级别,每个商品 ID 拥有自己独立的锁。
  • 使用的是 Redisson 提供的 RLock 分布式可重入锁(基于 Redis 实现)。

二、它解决了秒杀中的哪些核心问题?

  • 分布式环境并发控制 多台服务器部署时,确保某一时刻只有一个线程在处理某个商品的秒杀逻辑
  • 避免超卖: 通过对库存扣减加锁,防止多个线程同时扣库存导致超卖
  • 保证库存一致性: 保证在执行秒杀逻辑(减库存、写成功记录)时数据一致性
  • 替代 synchronized: synchronized 只在单机有效,而 Redisson 适用于集群环境

三、如何测试比较SynchronizedLock和RedissonLock
✅ 单机测试(使用 SynchronizedLock)

  1. 使用多线程并发模拟 1000 个线程访问同一资源
  2. 检查并发安全性、耗时

✅ 集群测试(使用 RedissonLock)

  1. 部署到多个 Tomcat 节点或用 Spring Boot 模拟集群
  2. 使用 Redis 配置 Redisson 分布式锁
  3. 使用 JMeter / Apache Bench 模拟并发请求
  4. 检查锁竞争下的正确性、响应时间、Redis 压力

总结一句话:

在 GoodsKill 项目中,Redisson 分布式锁被加在每个商品 ID 的锁对象上,用于控制分布式环境中同一商品的并发秒杀请求,从而有效防止库存超卖、数据错乱、秒杀异常等问题,是应对高并发场景下跨节点秒杀操作的关键手段之一。

1.1.4 RabbitMQ消息队列

一、RabbitMQ在秒杀中的实现方式

步骤 1️⃣:秒杀请求入队
在用户发起秒杀请求时,系统会:

  • 生成一个请求对象(包括用户手机号、商品ID、任务ID等);
  • 将其序列化成 JSON;
  • 发送到 RabbitMQ 指定主题(如 “goodskill-RabbitMQ”)中。

步骤 2️⃣:消费者异步处理
在后端,RabbitMQ 消费者(如 SeckillRabbitmqConsumer 类)通过 @RabbitmqListener 监听该主题:

  • 拿到消息后反序列化;
  • 调用核心秒杀逻辑(如判断库存、锁定、扣减、下单等);
  • 完成业务处理并记录日志或状态。
1
2
3
4
5
6
7
8
9
10
11
@Slf4j
@Component
public class SeckillKafkaConsumer {
@Autowired
private SeckillExecutor seckillExecutor;

@KafkaListener(topics = "goodskill-kafka")
public void onMessage(Object data) {
// 解析并处理秒杀请求
}
}

步骤 3️⃣:线程池并发模拟(压测)
在模拟场景中,系统使用线程池同时并发触发 RabbitMQ 消息发送,模拟大规模用户抢购,达到压测目的。

二、解决了哪些秒杀中的核心问题?

  • 高并发瞬时流量: MQ 充当缓冲层,消息排队后由消费者有序处理,削峰填谷
  • 请求响应慢: 前端快速响应“请求已接收”,实际处理异步进行,提高用户体验
  • 数据库压力大: 减少瞬时写入压力,避免数据库连接爆炸

小结一句话
RabbitMQ 在秒杀系统中是 高并发场景下的关键中间件,通过异步、削峰、解耦、扩展性强等特性,有效解决了传统同步处理架构在秒杀场景中遇到的性能瓶颈和系统稳定性问题。

1.1.5 Redis 计数 + MongoDB 异步持久化

RedisMongoReactiveStrategy

使用Redis进行秒杀商品减库存操作,秒杀结束后异步发送MQ,使用MongoDB完成数据落地

核心逻辑

  • Redis 计数:利用 Redis 的原子性操作(如 INCR 或 DECR)来快速扣减库存,记录秒杀成功的请求。
    • 对 Redis 中的商品秒杀计数进行 +1;
    • 若计数 <= 库存,则秒杀成功;
    • 发送秒杀成功消息(包含手机号、商品ID、注释等)到绑定通道,异步落库 MongoDB(即“最终写入数据库”)。
  • MongoDB 异步持久化:将秒杀成功的订单数据异步写入 MongoDB,避免数据库操作阻塞主流程。

异步持久化流程

  • 消息队列:项目使用 Spring Cloud Stream(默认绑定 RabbitMQ 或 Kafka)作为消息中间件。StreamBridge 将订单数据(SeckillMockSaveVo)发送到消息队列的 seckillMongoSave 通道。
  • 订单服务消费:goodskill-order-provider 模块的 OrderApplication 配置了一个消息消费者,监听 seckillMongoSave 通道,接收消息后将订单数据保存到 MongoDB。

解决了秒杀中的哪些问题

  1. 高并发下的性能瓶颈
    • 问题:传统秒杀系统中,库存扣减和订单写入通常直接操作关系型数据库(如 MySQL)。在高并发场景下,数据库的锁竞争和 I/O 操作会导致性能瓶颈,甚至引发数据库宕机。
    • 解决方案:
      • Redis 计数:将库存扣减操作移到 Redis内存操作速度远超数据库,单节点 Redis 可处理数万 QPS,极大提升了并发处理能力。
      • 异步持久化:订单写入 MongoDB 的操作通过消息队列异步执行,主流程无需等待数据库响应,响应时间从毫秒级降到微秒级。
  2. 超卖问题
    • 问题:在高并发下,多个请求可能同时读取到相同的库存值,导致库存扣减不准确,出现超卖(卖出数量超过实际库存)
    • 解决方案:
      • Redis 原子操作:使用 DECR 命令确保库存扣减是原子性的,只有当库存大于 0 时才允许扣减,彻底避免超卖
  3. 数据库压力过大
    • 问题:秒杀场景下,短时间内大量订单写入会导致数据库压力激增,可能引发连接池耗尽或慢查询。
    • 解决方案:
      • MongoDB 异步写入:MongoDB 是一个高性能的 NoSQL 数据库,适合高吞吐写入场景。通过消息队列异步写入,订单数据逐步持久化,平滑了数据库的瞬时压力。
      • 消息队列缓冲:消息队列作为缓冲层,吸收了高并发请求的峰值流量,订单服务可以按自身节奏消费消息,避免数据库直接面对流量洪峰。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
/**
* Redis + MongoDB 响应式秒杀策略实现
*
* 该类使用线程池异步处理秒杀请求,结合 Redis 进行库存控制,
* 秒杀成功后通过 StreamBridge 异步写入 MongoDB,避免高并发下数据库压力过大;
* 当库存耗尽时,结合状态机进行状态更新,并发送“活动结束”消息通知其他模块。
*
* 技术点:线程池 + Redis + Mongo + StreamBridge + 状态机
*/
@Component
@Slf4j
public class RedisMongoReactiveStrategy implements GoodsKillStrategy {

// Redis 缓存服务,封装了秒杀商品信息的获取与存储
@Resource
private RedisService redisService;

// Redis 原生操作工具,用于库存计数(自增)
@Resource
private RedisTemplate<String, Object> redisTemplate;

// 异步任务线程池,避免主线程阻塞,提升系统吞吐量
@Resource(name = "taskExecutor")
private ThreadPoolExecutor taskExecutor;

// 消息发送器,用于向绑定通道发送消息(如:保存订单,通知活动结束)
@Resource
private StreamBridge streamBridge;

// 状态机服务,控制活动状态(如:进行中、已结束)
@Resource
private StateMachineService stateMachineService;

/**
* 接收秒杀请求,交由线程池异步执行主逻辑
* @param requestDto 秒杀请求参数(包含秒杀ID、手机号、任务ID等)
*/
@Override
public void execute(SeckillMockRequestDTO requestDto) {
taskExecutor.execute(() -> {
try {
doExecute(requestDto);
} catch (Exception e) {
log.error("秒杀失败", e);
}
});
}

/**
* 执行具体的秒杀处理逻辑
* 1. Redis 中进行库存递增判断是否成功
* 2. 秒杀成功:通过消息队列异步发送保存订单信息
* 3. 秒杀失败:判断是否已结束并触发状态机更新,同时广播活动结束通知
*/
private void doExecute(SeckillMockRequestDTO requestDto) {
long seckillId = requestDto.getSeckillId();

// 从 Redis 获取秒杀商品详情
Seckill seckill = redisService.getSeckill(seckillId);

// 尝试使用 Redis 自增模拟库存扣减操作
if (redisTemplate.opsForValue().increment(String.valueOf(seckillId)) <= seckill.getNumber()) {

// 秒杀成功:通过消息队列异步将订单写入 MongoDB
taskExecutor.execute(() ->
streamBridge.send(DEFAULT_BINDING_NAME_MONGO_SAVE, MessageBuilder.withPayload(
SeckillMockSaveVo.builder()
.seckillId(seckillId)
.userPhone(requestDto.getPhoneNumber())
.note(REDIS_MONGO_REACTIVE.getName())
.build())
.build())
);

} else {
// 秒杀失败:库存超限,可能需要广播活动结束
synchronized (this) {
// 再次获取 Redis 中商品状态(防止并发一致性问题)
seckill = redisService.getSeckill(seckillId);

// 判断当前活动是否仍处于进行中
if (stateMachineService.checkState(seckillId, States.IN_PROGRESS)) {
// 触发状态机事件:计算 -> 活动结束
stateMachineService.feedMachine(Events.ACTIVITY_CALCULATE, seckillId);

log.info("秒杀商品暂无库存,发送活动结束消息!");

// 发送活动结束消息(用于前端提示、关闭页面等)
streamBridge.send(DEFAULT_BINDING_NAME, MessageBuilder.withPayload(
SeckillMockResponseDTO.builder()
.status(true)
.seckillId(seckillId)
.note(REDIS_MONGO_REACTIVE.getName())
.taskId(requestDto.getTaskId())
.build())
.build());

// 更新 Redis 中该商品状态为“结束”
seckill.setStatus(SeckillStatusConstant.END);
redisService.putSeckill(seckill);
}
}
}
}
}

1.2 哪些是同步,哪些是异步?

系统在处理秒杀请求时,提供了两种并行的处理方式——同步通道和异步通道

  • 同步通道是指请求的处理流程是实时的、阻塞的,用户需要等待处理完成。
  • 异步通道是指请求的处理流程是非阻塞的,用户发出请求后,系统立即返回一个初步响应, 用户无需等待所有操作完成

同步通道的实现

  • SynchronizedLockStrategy:使用 Java 的 synchronized 本地锁,按商品 ID 加锁,串行处理请求。
  • AtomicUpdateStrategy:通过数据库的乐观锁(如 UPDATE … WHERE stock > 0)实现库存扣减。
  • Redisson分布式锁

异步通道的实现

  • RabbitMQStrategy:通过 RabbitMQ 消息队列异步处理秒杀请求。
  • RedisMongoReactiveStrategy:使用 Redis 计数扣减库存,通过消息队列异步写入 MongoDB

1.3 如何测试的这些方法?

  1. sychronized同步锁
1
2
3
4
5
6
/**
* 通过同步锁控制秒杀并发(秒杀未完成阻塞主线程)
* 场景一:初始化当前库存为1000,通过线程池调度,模拟总共有2000人参与秒杀,期望值为最后成功笔数为1000
* 结果:多次运行,最终的结果为1000
* 总结:加上同步锁可以解决秒杀问题,适用于单机模式,扩展性差。
*/
  1. redisson分布式锁
1
2
3
4
5
6
/**
* 通过同步锁控制秒杀并发(秒杀未完成阻塞主线程)
* 场景二:初始化当前库存为1000,通过线程池调度,模拟总共有2000人参与秒杀,期望值为最后成功笔数为1000
* 结果:多次运行,最终的结果为1000
* 总结:加上同步锁可以解决秒杀问题,适用于分布式环境,但速度不如加同步锁。
*/
  1. RabbitMQ消息队列
1
2
3
4
5
6
/**
* 异步秒杀
* 场景三:初始化当前库存为1000,通过线程池调度,模拟总共有2000人参与秒杀,期望值为最后成功笔数为1000
* 结果:多次运行,最终的结果为1000
* 总结:速度快。
*/
  1. 数据库乐观锁
1
2
3
4
5
6
/**
* 通过同步锁控制秒杀并发,秒杀过程使用存储过程(秒杀未完成阻塞主线程)
* 场景五:初始化当前库存为1000,通过线程池调度,模拟总共有2000人参与秒杀,期望值为最后成功笔数为1000
* 结果:多次运行,最终的结果为1000
* 总结:速度快
*/
  1. Redis 计数 + MongoDB 异步持久化

使用redis缓存执行库存-1操作,最后通过发送MQ完成数据落地(存入mongoDB)

1.4 五种策略总结

五种秒杀策略对比表格

策略名称 作用 实现方式 优点 缺点 适用场景
Synchronized 同步锁 使用 Java 本地锁串行处理请求,确保库存扣减的线程安全,防止超卖。 通过 synchronized 关键字对商品 ID 加锁,同步执行库存检查和扣减,订单直接写入数据库。 1. 实现简单,无需额外组件
2. 单机环境下保证一致性
3. 锁粒度细(按商品)
1. 不支持分布式部署
2. 高并发下性能瓶颈明显(锁竞争)
3. 单线程处理,吞吐量低
单机、低并发场景
数据库乐观锁 通过数据库版本号或条件更新实现库存扣减的并发控制,保证一致性。 使用 SQL(如 UPDATE seckill SET stock = stock - 1 WHERE stock > 0 AND id = ?)原子更新库存。 1. 简单高效,依赖数据库事务
2. 无需额外中间件
3. 单机或小规模分布式一致性强
1. 高并发下数据库压力大
2. 锁冲突频繁,成功率降低
3. 性能受数据库瓶颈限制
单机或中小规模分布式系统
Redisson 分布式锁 在分布式环境下通过 Redis 分布式锁实现库存扣减的互斥性,防止超卖。 使用 Redisson 的 RLock 对商品 ID 加锁,锁住后执行库存扣减和订单写入。 1. 支持分布式部署
2. 简单易用,可重入锁支持
3. Redis 性能高,锁粒度可控
1. 锁获取/释放有网络开销
2. Redis 故障影响锁可用性
3. 高并发下锁竞争可能导致延迟
多节点分布式系统,强调一致性
RabbitMQ 消息队列 通过消息队列异步处理秒杀请求,快速响应用户,缓解数据库压力。 Redis 检查库存,成功后将订单消息发送到 RabbitMQ,后台消费者异步写入数据库(如 MongoDB)。 1. 高吞吐量,异步解耦
2. 快速响应用户
3. 缓冲峰值流量,保护数据库
1. 最终一致性,延迟可能影响体验
2. 系统复杂性增加
3. RabbitMQ 故障需容错机制
高并发场景,需快速响应
Redis 计数 + MongoDB 异步持久化 使用 Redis 原子计数快速扣减库存,MongoDB 异步持久化订单,提升性能。 Redis DECR 扣减库存,订单数据通过消息队列(如 RabbitMQ/Kafka)异步写入 MongoDB。 1. 极高性能(Redis 微秒级响应)
2. 异步解耦,吞吐量高
3. 库存一致性强,防超卖
1. 最终一致性需业务容忍
2. 架构复杂,依赖多组件
3. Redis 故障可能丢失库存数据
高并发、分布式系统,追求极致性能

2. 基于 Nacos 实现统一配置中心管理;使用 OpenFeign 实现微服务之间的远程调用和服务熔断,并设计相应的服务降级逻辑处理;使用 Gateway 构建微服务 API 网关,支持动态路由、配置热更新与统一身份认证

2.1 基于 Nacos 实现统一配置中心管理

Nacos 作为配置中心和服务注册中心,在 GoodsKill 项目中用于集中管理微服务的配置(如数据库连接、消息队列参数等),并支持配置的动态刷新。

  1. 依赖引入: 在 pom.xml 中引入 Nacos 配置管理的依赖
  2. 配置文件: 在 bootstrap.yml(优先于 application.yml 加载)中配置 Nacos 地址和配置信息
  3. 配置 Nacos 管理界面: 在 Nacos 控制台创建配置文件
  4. 服务启动: 启动服务时,Spring Boot 会从 Nacos 拉取配置,覆盖本地 application.yml,并注册服务到 Nacos。

2.2 使用 OpenFeign 实现微服务之间的远程调用和服务熔断,并设计服务降级逻辑处理

跳转到OrderService

OrderService
package com.goodskill.order.api

基于 Spring Cloud OpenFeign 实现的 订单微服务 (goodskill-order) 远程调用接口,并提供了 熔断降级处理。

  • 使用Spring Cloud Alibaba 提供了 Sentinel 来做服务熔断、限流、降级。
  1. 添加依赖
  2. Feign 客户端开启降级支持
1
2
3
feign:
sentinel:
enabled: true
  1. 编写接口和 fallback 类
1
2
3
4
5
@FeignClient(name = "goodskill-order", fallback = OrderServiceFallback.class)
public interface OrderService {
@GetMapping("/count")
long count(@RequestParam("seckillId") long seckillId);
}

2.3 使用 Gateway 构建微服务 API 网关,支持动态路由、配置热更新与统一身份认证

跳转到goodskill-gateway微服务网关API

跳转到goodskill-gateway解析

2.3.1 动态路由支持(基于 Nacos)

实现原理:
通过 DynamicRouteConfigWatcher + DynamicRouteRefresh 实现:

DynamicRouteConfigWatcher
  • 初始化阶段(@PostConstruct)

    • 通过 initConfigService() 连接到 Nacos 配置中心。
    • 注册监听器监听动态路由配置(dataId + group)。
    • 启动时从 Nacos 拉取当前网关路由配置,通过 run() 方法注入到网关中。
  • 监听配置变化

    • 当配置变更时触发 receiveConfigInfo() 方法,将变更的路由配置解析为 RouteDefinition 列表,调用 dynamicRouteService.updateList(...) 更新网关路由。
DynamicRouteRefresh
  • 提供 save()delete()updateList() 方法对网关路由进行动态维护。
  • 每次操作都会通过 ApplicationEventPublisher 发布 RefreshRoutesEvent实时刷新网关路由

2.3.2 配置热更新(依赖 Nacos)

  • 使用 Nacos 管理所有路由规则,通过监听机制(Listener 接口)自动感知配置变动。
  • 一旦监听到配置更新,立刻刷新网关路由表(无需重启服务)。
  • 动态加载的数据格式是标准的 RouteDefinition JSON 数组,如:
1
2
3
4
5
6
7
8
[
{
"id": "route1",
"uri": "lb://goodskill-order",
"predicates": [{"name": "Path", "args": {"_genkey_0": "/api/order/**"}}],
"filters": [{"name": "StripPrefix", "args": {"_genkey_0": "2"}}]
}
]

2.3.3. 统一身份认证与权限控制(基于 Sa-Token)

SaTokenConfiguration(注册全局认证过滤器)
  • 注册全局认证过滤器 SaReactorFilter,在请求进入微服务前进行统一身份校验:
    • 对所有路径 /** 进行拦截;
    • 白名单(如 /favicon.ico/actuator/**)放行;
    • 判断是否登录(StpUtil.checkLogin());
    • 支持登录用户白名单(放行某些不需权限的接口);
    • 权限控制预留接口(可启用 StpUtil.checkPermission() 实现更精细的权限控制);
    • 所有异常统一捕获并格式化返回(通过 .setError(...) 实现)。
StpInterfaceImpl(权限和角色动态加载)
  • 实现 StpInterface 接口:
    • 通过 Feign 客户端调用 goodskill-auth 服务远程加载用户的权限码、角色列表。
    • 异步执行(CompletableFuture),提升调用效率。

2.3.4. 路由定义配置(GatewayConfiguration)

  • 在静态模式下定义默认路由转发规则(例如:/api/order/** → lb://goodskill-order),用于兜底或未配置动态路由时使用。
  • 支持 stripPrefix 功能,剥离 /api 前缀,确保内部服务 URL 正确映射。

总结:Gateway 主要能力

能力 实现机制
动态路由 Nacos + DynamicRouteConfigWatcher + DynamicRouteRefresh
路由热更新 监听 Nacos 配置变化,实时刷新路由
服务发现与转发 lb:// URI + Spring Cloud Gateway
路由前缀剥离 stripPrefix 过滤器
登录认证 Sa-Token 全局过滤器 SaReactorFilter
权限与角色控制 自定义 StpInterfaceImpl 接口
统一异常处理 .setError() 返回标准错误响应结构
白名单机制 支持全局与登录用户级白名单

3. 基于责任链模式的预处理器机制,在秒杀前对数据库、缓存、状态机等进行初始化预处理

跳转到handler包

通过定义 PreRequestHandler 接口及其抽象实现 AbstractPreRequestHandler, 将“秒杀活动初始化”过程拆解为多个独立步骤,如数据库初始化、Redis 缓存清理、状态机启动、Mongo 数据清除等

  • PreRequestHandler:接口
  • PreRequestPipeline
  • AbstractPreRequestHandler ;抽象类

3.1 Handler 流程的执行顺序?

执行顺序:

  1. DatabasePreRequestHandler
  2. RedisPreRequestHandler
  3. StateMachinePreRequestHandler
  4. MongoPreRequestHandler
顺序 Handler 名称 主要作用
0 DatabasePreRequestHandler 重置数据库中指定秒杀商品的库存、清理成功秒杀记录
1 RedisPreRequestHandler 清空 Redis 中相关缓存、设置商品状态为“进行中”
2 StateMachinePreRequestHandler 初始化状态机,触发状态变更(重置 → 开始)
3 MongoPreRequestHandler 删除 MongoDB 中历史订单数据
1
2
3
4
5
6
7
8
9
10
                            ┌────────────────────────────┐
│ PreRequestPipeline │
└────────────┬───────────────┘

依顺序调用 handle()

┌──────────────┬────────────────┬──────────────┬────────────────────┐
▼ ▼ ▼ ▼ ▼
DatabaseHandler → RedisHandler → StateMachineHandler → MongoHandler → 秒杀主逻辑开始
(重置库存) (清理缓存) (状态流转) (清理Mongo) (执行压测/请求)

3.2 在高并发下如何确保这些 Handlers 的顺序性?

  • 核心机制:顺序性由单线程调用责任链保证
  • 责任链的触发入口 PreRequestPipeline.handle() 是同步串行调用的。
  • 系统可以同时有多个请求并发进来,每个请求各自启动一条完整的预处理流程(在自己线程内),但每条链上的 handler 是顺序执行的,不会乱序或并发执行。

3.3 是否存在线程安全问题?

  • 每个 handler 类本身是单例的(@Component),但 handler 只读取配置或执行外部操作不依赖共享可变状态,因此是线程安全的。

3.4 为什么不使用异步方式?是否可以用多线程提升性能?

预处理流程本质上是“初始化 + 校验 + 清理”的串行步骤,比如先清数据库、再清 Redis、最后初始化状态机,顺序不能乱。
如果你用异步或并行执行,反而会破坏这套语义正确性,比如:

  • Redis 还没清理就启动状态机,可能导致状态异常;
  • 数据库还没清空,Mongo 清理了也没意义;
  • 某个异常未处理会提前终止整个链条。

4. 利用 Spring StateMachine 管理秒杀活动生命周期,并集成 Redis 持久化,在分布式环境中实现状态的共享与恢复

4.1 状态机的作用是什么?

问题 状态机的作用
活动状态不好管理 用状态机统一管理“未开始 → 进行中 → 结束”等流程
状态容易被乱改 状态只能通过“事件驱动”变更,避免随意修改
多线程状态混乱 状态变更具备原子性和线程安全,防止并发状态错乱
扩展性弱 使用状态机更易扩展更多活动状态(如取消、失败、计算中)

4.2 GoodsKill 中状态机控制了什么?

主要控制的是 秒杀活动(Seckill)状态

活动状态流转示意图:

1
2
3
4
INIT(初始状态)
└──[ACTIVITY_START]──▶ IN_PROGRESS(进行中)
└──[ACTIVITY_CALCULATE]──▶ CALCULATING(计算中)
└──[ACTIVITY_END]──▶ END(已结束)

4.3 项目中如何实现 Spring StateMachine?

状态机的配置主要在模块:goodskill-seckill-provider 中。

4.3.1 配置类:StateMachineConfig

该类定义了**状态、事件、状态迁移规则、监听器、持久化机制(Redis)**等核心内容。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@Configuration
@EnableStateMachineFactory
public class StateMachineConfig extends StateMachineConfigurerAdapter<States, Events> {

@Override
public void configure(StateMachineStateConfigurer<States, Events> states) {
states.withStates()
.initial(States.INIT)
.state(States.IN_PROGRESS)
.state(States.CALCULATING)
.end(States.END);
}

@Override
public void configure(StateMachineTransitionConfigurer<States, Events> transitions) {
transitions
.withExternal().source(States.INIT).target(States.IN_PROGRESS).event(Events.ACTIVITY_START)
.and()
.withExternal().source(States.IN_PROGRESS).target(States.CALCULATING).event(Events.ACTIVITY_CALCULATE)
.and()
.withExternal().source(States.CALCULATING).target(States.END).event(Events.ACTIVITY_END);
}
}

4.3.2 状态枚举

1
2
3
4
5
6
7
8
9
10
11
12
public enum States {
INIT, // 初始
IN_PROGRESS, // 进行中
CALCULATING, // 结算中
END // 已结束
}

public enum Events {
ACTIVITY_START, // 启动活动
ACTIVITY_CALCULATE, // 活动完成、统计
ACTIVITY_END // 活动结束
}

4.3.3 调用方式:通过 StateMachineService

1
2
3
4
5
// 初始化状态机
stateMachineService.initStateMachine(seckillId);

// 触发状态变更事件
stateMachineService.feedMachine(Events.ACTIVITY_START, seckillId);

这种方式用在多个地方,比如:

  • 秒杀前准备阶段(prepareSeckill)→ 进入 IN_PROGRESS
  • 秒杀库存售罄 → 进入 CALCULATING
  • 结束清理 → 进入 END

4.3.4 配合 Redis 持久化

使用了 Spring StateMachine 的 Redis 持久化扩展,可以把每个活动的状态存在 Redis 中,保证状态:

  • 可恢复(容器重启不丢失)
  • 可共享(多个节点共享状态)
  • 可监控(可通过 Redis 查看活动状态)

4.4 Spring StateMachine 如何和 Redis 做持久化集成?

4.4.1 为什么需要 Redis 持久化?

状态机默认是内存状态,如果项目是分布式部署、多节点,每个服务实例维护自己的状态,很容易出现状态不一致问题。

→ 使用 Redis 作为 状态存储中心,可以让所有实例共享状态,实现:

  • 状态可恢复(服务重启不丢失)
  • 状态共享一致
  • 状态可视化调试

4.4.2 如何使用Redis做持久化的?

在JAVA17中,引入依赖就可以直接用了

JAVA17的类: RedisStateMachinePersister

每次使用状态机时:

  • 从 Redis 加载状态 → redisPersister.restore(machine, seckillId.toString())
  • 执行状态流转
  • 保存新状态 → redisPersister.persist(machine, seckillId.toString())

5. 集成 ElasticSearch 实现高效商品检索,支持模糊搜索与关键词高亮,提升用户检索体验

5.1 如何实现的ElasticSearch 搜索?

GoodsEsServiceImpl

跳转到GoodsEsServiceImpl

  • Elasticsearch 集成: 使用 Spring Data Elasticsearch 集成 Elasticsearch,提供了高效的全文搜索和索引管理功能
  • 高亮搜索功能: 实现商品名称的模糊搜索,并对匹配关键字进行高亮显示

6. 基于 Spring AI 构建秒杀AI对话服务,通过流式响应实现实时交互,支持多轮对话和智能问答触发秒杀操作

  • 基于Spring AI Alibaba构建的简易秒杀机器人服务,可以通过机器人服务完成服务调用,目前仅支持开启秒杀活动,以及输出活动耗时情况

以下是一个对话示例:

1
2
3
4
5
6
7
我: 帮我执行秒杀

AI: 当然可以帮助您执行秒杀活动啦!为了顺利进行,请告诉我以下几个信息:1.秒杀活动的ID是多少?2. 这次秒杀的商品数量是多少?3.您打算这次请求的次数是多少?有了这些信息,我就能帮您开始了!😊

我: 秒杀活动id 1001,请求次数20,商品数量10

AI: 秒杀活动已成功开启!我们现在等待10秒钟,然后我会为您获取任务的耗时统计信息。秒杀活动已成功启动,并在10秒后获得了任务耗时统计信息:总耗时:6.673773375秒具体耗时分布如下:- 秒杀活动id:1001,秒杀场景五(数据库原子性更新update setnum = num -1)耗时 3.528280125 秒,占比 53%-秒杀活动id:1001,秒杀场景五(数据库原子性更新updateset num = num-1)耗时3.14549325 秒,占比 47%希望这些信息对您有帮助!如果还有其他问题或需要进一步的帮助,请随时告诉我哦!😊

6.1 如何构建的AI聊天?

跳转到goodskill-ai

跳转到AI亮点解析

1
2
3
4
5
6
7
用户请求 → AssistantController.chat(chatId, userMessage)

UserSupportAssistant.chat(...)

ChatClient:生成回复 + 调用函数 + 管理上下文

返回 Flux<String> 流式消息

AssistantController

作为对话接口的入口,接收用户请求,调用后端助手逻辑进行处理,并以 Server-Sent Events(SSE)方式将响应内容实时返回给前端。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
/**
* AssistantController 是一个 REST 控制器,用于处理与用户支持助手(UserSupportAssistant)的对话请求。
*
* 主要职责是接收前端传来的对话参数,调用 agent(UserSupportAssistant)进行会话处理,并以 SSE 流方式返回响应内容。
*
*/
@RestController
@RequestMapping("/api/assistant")
public class AssistantController {

private final UserSupportAssistant agent;
public AssistantController(UserSupportAssistant agent) {
this.agent = agent;
}

/**
* 与用户助手进行对话的接口
*
* @param chatId 当前对话的唯一 ID(用于关联上下文)
* @param userMessage 用户当前输入的消息内容
* @return Flux<String>:服务端事件流响应,实时返回 AI 助手的回答内容
*/
@RequestMapping(path = "/chat", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
public Flux<String> chat(@RequestParam String chatId, @RequestParam String userMessage) {
return agent.chat(chatId, userMessage);
}
}
  1. 对外提供 HTTP API 接口:

    • 路径前缀是 /api/assistant,子路径是 /chat,用于接收用户的聊天请求。
  2. 参数接收:

    • chatId:唯一标识一次会话(便于上下文管理)。
    • userMessage:用户输入的消息文本。
  3. 调用后端聊天逻辑:

    • 将请求委托给注入的 UserSupportAssistant 组件处理,这个组件封装了实际的对话处理流程,如:上下文管理、大模型交互、回复生成等。
  4. 使用 SSE(Server-Sent Events)返回流式响应:

    • 设置 produces = MediaType.TEXT_EVENT_STREAM_VALUE,表明该接口会返回 实时流式数据(Flux<String>
    • 前端可以一边接收一边展示回复,提高交互体验(类似 ChatGPT 打字效果)。

UserSupportAssistant

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
/**
* 用户聊天支持助手服务类。
*
* 该类封装了一个智能聊天客户端 ChatClient,用于与用户进行多轮对话交互,支持基于上下文的语义理解与函数调用,
* 可实现对秒杀系统中的活动查询、活动重置、活动开启、耗时统计等功能的智能问答与流程控制。
*
* ChatClient 是一个结合大语言模型(LLM)、聊天上下文记忆(ChatMemory)、知识向量检索(VectorStore)、
* 以及函数调用(function calling)能力的智能聊天代理。
*/
@Service
public class UserSupportAssistant {

// ChatClient 封装了聊天上下文管理、语义检索、日志记录等多个对话增强功能
private final ChatClient chatClient;

/**
* 构造函数注入核心依赖:
* @param modelBuilder 用于构建 ChatClient 的 Builder 实例(含 LLM 配置)
* @param vectorStore 向量存储器,用于语义相似度检索(知识库问答)
* @param chatMemory 聊天上下文记忆组件,用于保持多轮对话的历史信息
*/
public UserSupportAssistant(ChatClient.Builder modelBuilder, VectorStore vectorStore, ChatMemory chatMemory) {
this.chatClient = modelBuilder
// 设置系统默认提示词(system prompt),指导 AI 如何回复
.defaultSystem("""
您是用户聊天支持代理。请以友好、乐于助人且愉快的方式来回复。
您正在通过在线聊天系统与用户互动。
您能够支持已有秒杀商品详情查询、开启秒杀活动、重置秒杀活动等操作,其余功能将在后续版本中添加,如果用户问的问题不支持请告知详情。
在提供有关秒杀活动查询、开启秒杀活动、重置秒杀活动等操作之前,您必须始终从用户处获取以下信息:秒杀活动id、秒杀商品数量、请求次数。
如果需要提前结束秒杀活动,您必须在继续之前征得用户同意。
使用提供的功能获取秒杀活动以及商品详细信息、开启秒杀活动和重置秒杀。
用户开始秒杀活动后,等待10秒,然后调用 获取任务耗时统计信息 函数,最后将函数结果返回给用户。
如果需要,您可以调用相应函数辅助完成。
请讲中文。
今天的日期是 {current_date}.
""")
// 设置对话增强模块(顾问):记忆、语义检索、日志记录
.defaultAdvisors(
new PromptChatMemoryAdvisor(chatMemory),
new QuestionAnswerAdvisor(vectorStore, SearchRequest.builder().topK(4).similarityThresholdAll().build()),
new LoggingAdvisor()
)
// 注册默认可调用的函数(与后端业务逻辑绑定)
.defaultFunctions("startSeckill", "getTaskTimeInfo")
.build();
}

/**
* 主聊天处理方法。
* @param chatId 当前用户对话的唯一标识(用于区分不同会话、绑定上下文)
* @param userMessageContent 用户输入的消息文本
* @return 返回一个 Flux<String>,表示流式的聊天响应(逐句返回)
*/
public Flux<String> chat(String chatId, String userMessageContent) {
return this.chatClient
.prompt()
.system(s -> s.param("current_date", LocalDate.now().toString())) // 填充系统提示词中的动态日期
.user(userMessageContent) // 设置用户输入内容
.advisors(a -> a
.param(CHAT_MEMORY_CONVERSATION_ID_KEY, chatId) // 绑定上下文 ID
.param(CHAT_MEMORY_RETRIEVE_SIZE_KEY, 100) // 设置历史会话最大检索数
)
.stream() // 启动流式响应
.content(); // 获取内容部分作为 Flux<String> 返回
}
}
  1. 构造阶段:

    • 构建 ChatClient,这是一个智能聊天代理。
    • 配置了系统提示词(System Prompt),用于设定 AI 的语气和行为边界。
    • 添加多个 “Advisor” 顾问模块:
      • 记忆顾问(ChatMemoryAdvisor):保持对话上下文。
      • 语义检索顾问(VectorStore QA):从知识库中查找匹配回答。
      • 日志顾问(LoggingAdvisor):记录每次交互日志。
    • 注册可调用的函数(startSeckill、getTaskTimeInfo)用于增强对话执行力。
  2. 聊天阶段(chat 方法):

    • 动态设置当前日期到 prompt 中。
    • 接收用户输入作为对话内容。
    • 使用 chatId 绑定会话上下文。
    • 返回一个 SSE 流式响应(Flux<String>,前端可逐句实时展示对话。

6.2 什么是Flux<String> 流式消息?

特性 Flux 普通 String
响应类型 异步流(多条) 同步文本(单条)
返回模式 一条条推送 一次性返回
性能优势 减少内存、提升体验 容易阻塞
场景 聊天、日志流、实时通知 表单提交、固定响应

假设我们要模拟一个“AI小助手”连续发送消息的场景,每 1 秒发送一条消息,总共发 5 条。

1
2
3
4
5
6
7
8
9
10
11
@RestController
@RequestMapping("/api/demo")
public class ChatController {

@GetMapping(value = "/chat", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
public Flux<String> streamChat() {
return Flux.interval(Duration.ofSeconds(1)) // 每秒发送一次
.take(5) // 总共发 5 次
.map(i -> "AI助手消息 #" + (i + 1));
}
}

当你访问前端页面时,AI助手会每秒输出一句话:

1
2
3
4
5
AI助手消息 #1
AI助手消息 #2
AI助手消息 #3
AI助手消息 #4
AI助手消息 #5

消息一条条推送,页面自动更新,无需刷新或等待整体响应返回!

在 AssistantController 中使用 Flux<String> 就是实现了这种 流式推送聊天体验,让用户感受到“AI 正在实时回复”,不是等很久一口气发一段长文本。

6.3 项目中如何使用Flux<String> 流式消息?

看下面这条关键语句:

1
produces = MediaType.TEXT_EVENT_STREAM_VALUE

它的意思是:

告诉浏览器/前端:“我要用 SSE(Server-Sent Events)协议 来推送文本流,像聊天一样一条一条发”。

再看 UserSupportAssistant.chat() 的返回:

1
2
3
4
5
6
7
8
public Flux<String> chat(String chatId, String userMessageContent) {
return this.chatClient.prompt()
.system(s -> s.param("current_date", LocalDate.now().toString()))
.user(userMessageContent)
.advisors(a -> a.param(CHAT_MEMORY_CONVERSATION_ID_KEY, chatId).param(CHAT_MEMORY_RETRIEVE_SIZE_KEY, 100))
.stream()
.content(); // 返回 Flux<String>
}

这段逻辑可以拆解理解为:

  1. prompt() 设置了 AI 聊天上下文(当前日期、用户内容、聊天历史等)
  2. .stream() 开启流式响应(不是一次性生成完整回答,而是边生成边推送
  3. .content() 把一条条生成的回复内容包装成 Flux<String> —— Spring WebFlux 会将它转化成流式响应返回给前端。

6.4 AI是怎么实现的?

在我的项目中,使用的是Spring AI Alibaba提供的ChatClient接口

要在项目中使用 Spring AI Alibaba 提供的 ChatClient 接口,您可以按照以下步骤进行:

  1. 引入依赖:在您的 Maven 项目的 pom.xml 文件中添加 spring-ai-alibaba-starter 依赖:

    1
    2
    3
    4
    5
    <dependency>
    <groupId>com.alibaba.cloud.ai</groupId>
    <artifactId>spring-ai-alibaba-starter</artifactId>
    <version>1.0.0-M6.1</version>
    </dependency>

    请注意,由于 spring-ai 相关依赖尚未发布到中央仓库,您可能需要在 pom.xml 中添加 Spring 的里程碑仓库:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    <repositories>
    <repository>
    <id>spring-milestones</id>
    <name>Spring Milestones</name>
    <url>https://repo.spring.io/milestone</url>
    <snapshots>
    <enabled>false</enabled>
    </snapshots>
    </repository>
    </repositories>

    如果您的本地 Maven settings.xml 文件中配置了通配符 * 的镜像,请根据以下示例进行调整:

    1
    2
    3
    4
    5
    <mirror>
    <id>central</id>
    <mirrorOf>central</mirrorOf>
    <url>https://repo.maven.apache.org/maven2</url>
    </mirror>
  2. 创建 ChatClient 实例:在您的 Spring Boot 应用中,您可以通过以下方式注入 ChatClient:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    @RestController
    public class ChatController {

    private final ChatClient chatClient;

    public ChatController(ChatClient.Builder builder) {
    this.chatClient = builder.build();
    }

    @GetMapping("/chat")
    public String chat(@RequestParam String input) {
    return this.chatClient.prompt()
    .user(input)
    .call()
    .content();
    }
    }

    在上述代码中,ChatClient.Builder 用于构建 ChatClient 实例,chat 方法接收用户输入,并通过 ChatClient 与 AI 模型进行交互,返回生成的响应。

  3. 配置模型服务:确保您已在应用的配置文件中设置了所需的模型服务参数,例如阿里云的 API 密钥等。这些配置通常包括服务的访问密钥、区域等信息,以便 ChatClient 能够正确地与模型服务进行通信。

  4. 处理响应:使用 ChatClientprompt() 方法设置用户输入,然后调用 call() 方法发送请求并获取响应。您可以根据需要处理返回的内容,例如将其映射为实体类、进行流式处理等。

通过上述步骤,您可以在项目中集成并使用 Spring AI Alibaba 提供的 ChatClient 接口,实现与 AI 模型的交互。

6.5 ChatClient接口

Spring AI Alibaba官网

Spring AI进阶:AI聊天机器人之ChatMemory持久化(二)

Spring AI Alibaba ChatClient使用

END