AI 网关实战(三):技术选型详解 - Spring Boot 3 WebFlux + PostgreSQL + Redis
银行 AI 网关实战系列第 3 篇,深入讲解技术选型的决策过程,包括 Java、Spring Boot 3 WebFlux、PostgreSQL、Redis、MyBatis-Plus、Resilience4j 等核心组件的选择理由。
目录
- 一、前置约束明确
- 二、为什么选择 Java
- 三、为什么选择 Spring Boot 3 WebFlux
- 四、为什么选择 PostgreSQL
- 五、为什么选择 Redis
- 六、为什么选择 MyBatis-Plus
- 七、为什么选择 Resilience4j
- 八、不引入的组件及原因
- 九、技术栈汇总表
一、前置约束明确
在做技术选型之前,必须先明确项目的约束条件,这是所有决策的基础:
- 业务场景:银行内部 AI 网关,不是通用的 AI 应用
- 关键指标优先级:安全合规 > 稳定性 > 开发效率 > 性能
- 团队技能:Java 后端团队,运维资源有限
- 部署环境:内网私有部署,Docker 容器化
基于这些约束,最终选定的技术栈如下:
| 组件 | 选型 | 版本 |
|---|---|---|
| 语言 & 框架 | Java + Spring Boot | 17 LTS + 3.2.x |
| 响应式 | Spring WebFlux | 6.1.x |
| ORM | MyBatis-Plus | 3.5.x |
| 数据库 | PostgreSQL | 16 |
| 缓存 | Redis | 7.x |
| 熔断 | Resilience4j | 2.2.x |
| 构建 | Maven | 3.9.x |
| 部署 | Docker + Docker Compose | 24.x |
二、为什么选择 Java
2.1 与 Python 的对比
在 AI 领域,Python 确实是主流,但对于银行内部 AI 网关这个场景,Java 有不可替代的优势:
| 优势 | 详细说明 |
|---|---|
| 团队技能成熟度 | 银行 Java 后端团队成熟,新人上手快,降低培训成本 |
| 生态完整性 | 安全框架、监控工具、运维方案丰富,金融行业验证充分 |
| 类型安全 | 编译期错误检查,减少运行时异常,符合金融系统稳定性要求 |
| 部署经验积累 | 内网已有大量 Java 应用,运维流程、监控告警都很成熟 |
| 长期支持(LTS) | Java 17 LTS 承诺长期维护,适合金融系统 5-10 年的生命周期 |
2.2 为什么不选择 Python?
| 问题 | 说明 |
|---|---|
| 团队技能 | 银行内部 Python 开发团队较少,学习成本高 |
| 部署运维 | Python 在金融内网的部署经验相对较少,运维工具链不如 Java 完善 |
| 类型安全 | 动态类型在大型项目中维护成本高,不符合金融系统严谨性要求 |
| 性能调优 | Java 的 JVM 性能调优经验和工具链更成熟 |
总结:Python 在 AI 训练、数据科学领域是首选,但在"企业级网关"这个场景下,Java 更符合银行的技术约束和团队能力。
三、为什么选择 Spring Boot 3 WebFlux
3.1 Spring MVC vs Spring WebFlux
传统 Spring Boot 使用 Spring MVC(阻塞式 IO),但 AI 网关有两个核心需求:
- 流式响应(SSE):AI 对话必须是流式的,否则用户体验极差
- 高并发代理:网关本质是"中间人",吞吐量直接影响系统承载能力
Spring WebFlux 的响应式编程模型正好满足这两点:
3.1.1 代码对比
Spring MVC(阻塞式):
@RestController
@RequestMapping("/api/v1")
public class ChatController {
@PostMapping("/chat")
public ChatResponse chat(@RequestBody ChatRequest request) {
// 阻塞等待 AI 响应
ChatResponse response = aiService.callBlocking(request);
return response;
}
}
Spring WebFlux(非阻塞式):
@RestController
@RequestMapping("/api/v1")
public class ChatController {
@PostMapping(value = "/chat", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
public Flux<ServerSentEvent<String>> chat(@RequestBody ChatRequest request) {
// 流式返回,非阻塞
return aiService.callStreaming(request)
.map(data -> ServerSentEvent.<String>builder()
.data(data)
.build());
}
}
3.1.2 性能对比
| 场景 | Spring MVC | Spring WebFlux | 差异 |
|---|---|---|---|
| 并发连接数 | 受线程池大小限制(通常 200-500) | 单机可支持数万连接 | 10-50 倍 |
| 内存占用 | 每个连接占用一个线程栈(~1MB) | 每个连接占用极少量内存 | 显著降低 |
| 流式响应 | 需要额外处理 SSE/长轮询 | 原生支持 Flux | 开箱即用 |
3.2 为什么不选择其他框架?
| 框架 | 缺点 | 不选理由 |
|---|---|---|
| Vert.x | 学习曲线陡,文档相对少 | 团队学习成本高,开发效率低 |
| Go (Gin/Echo) | 与现有 Java 生态不匹配 | 运维、监控、安全工具链需要重新搭建 |
| Node.js | 银行内部 JavaScript 经验少 | 团队技能不匹配,部署运维成本高 |
3.3 Spring Boot 3 vs Spring Boot 2
选择 3 而不是 2,核心原因:
| 特性 | Spring Boot 2 | Spring Boot 3 |
|---|---|---|
| Java 版本基线 | Java 8/11/17 | Java 17 LTS(强制) |
| 虚拟线程 | 不支持 | 原生支持(GraalVM) |
| 响应式栈 | WebFlux 功能较弱 | 显著增强 |
| 长期维护 | 2025 年底停止 | 持续维护中 |
Java 17 的性能提升:
- 更高效的垃圾回收器(ZGC)
- 记录类(Record)减少样板代码
- 模式匹配提升代码可读性
- 更强的类型推断
四、为什么选择 PostgreSQL
4.1 对比 MySQL
| 维度 | PostgreSQL | MySQL | 为什么 PostgreSQL 胜出 |
|---|---|---|---|
| 分区表 | 原生支持,工具链成熟 | 8.0 开始支持,功能较弱 | 日志表必须按月分区 |
| JSONB | 原生支持,查询能力强 | 5.7+ 支持,但性能弱 | 审计日志存储变更快照 |
| 审计能力 | 触发器完善,支持行级安全 | 可以做,但稍麻烦 | 银行合规要求高 |
| 数据一致性 | MVCC,严格事务 | 也强,但 PostgreSQL 更严谨 | 金融系统要求极高 |
| 行业使用 | 金融行业更广泛 | 互联网更多 | 合规性参考案例多 |
4.2 关键特性:分区表
调用日志表会快速增长,必须按月分区:
-- PostgreSQL 分区语法简洁
CREATE TABLE call_logs (
id BIGSERIAL,
api_key_id BIGINT,
user_id BIGINT,
channel_id BIGINT,
model VARCHAR(64),
input_tokens INTEGER DEFAULT 0,
output_tokens INTEGER DEFAULT 0,
latency_ms INTEGER,
status_code SMALLINT,
is_stream BOOLEAN DEFAULT FALSE,
created_at TIMESTAMP NOT NULL DEFAULT NOW(),
PRIMARY KEY (id, created_at)
) PARTITION BY RANGE (created_at);
-- 创建 2026 年 5 月的分区
CREATE TABLE call_logs_2026_05 PARTITION OF call_logs
FOR VALUES FROM ('2026-05-01') TO ('2026-06-01');
-- 创建 2026 年 6 月的分区
CREATE TABLE call_logs_2026_06 PARTITION OF call_logs
FOR VALUES FROM ('2026-06-01') TO ('2026-07-01');
-- 配合 pg_partman 插件自动创建后续分区
pg_partman 自动分区管理:
-- 安装插件
CREATE EXTENSION pg_partman;
-- 配置自动创建分区
SELECT create_parent(
'public.call_logs', -- 表名
'created_at', -- 分区字段
'native', -- 分区方式
'monthly' -- 分区粒度
);
-- 提前 3 个月创建分区
UPDATE part_config
SET premake = 3
WHERE parent_table = 'public.call_logs';
4.3 关键特性:JSONB
审计日志需要存储"变更前后快照",JSONB 是最佳选择:
CREATE TABLE audit_logs (
id BIGSERIAL PRIMARY KEY,
operator_id BIGINT NOT NULL,
operator_name VARCHAR(64) NOT NULL,
action VARCHAR(32) NOT NULL, -- CREATE/UPDATE/DELETE
resource_type VARCHAR(32) NOT NULL, -- API_KEY/CHANNEL/CONFIG
resource_id BIGINT,
detail JSONB, -- 变更前后快照
ip_address INET,
created_at TIMESTAMP NOT NULL DEFAULT NOW()
);
-- 插入审计日志
INSERT INTO audit_logs (
operator_id, operator_name, action, resource_type, resource_id, detail
) VALUES (
1, 'admin', 'UPDATE', 'API_KEY', 100,
'{
"table": "api_keys",
"id": 100,
"before": {
"quota_rpm": 60,
"status": 1
},
"after": {
"quota_rpm": 120,
"status": 1
},
"changed_fields": ["quota_rpm"]
}'::jsonb
);
-- 查询特定字段的变更记录
SELECT * FROM audit_logs
WHERE detail->>'changed_fields' @> '["quota_rpm"]';
-- 查询修改了特定值的记录
SELECT * FROM audit_logs
WHERE detail->>'new_value' LIKE '%admin%';
4.4 其他优势
| 特性 | 说明 |
|---|---|
| 全文搜索 | 内置全文搜索能力,可用于日志查询 |
| 扩展性 | 丰富的插件生态(如 pg_stat_statements 性能监控) |
| 数据类型 | 支持更丰富的数据类型(如数组、IP 地址、UUID) |
| 备份恢复 | 物理备份和逻辑备份都很完善 |
五、为什么选择 Redis
5.1 为什么不用本地缓存?
| 缓存方案 | 问题 | 为什么不适合 |
|---|---|---|
| Guava / Caffeine | 多节点数据不同步,限流失效 | 限流必须是全局的 |
| Ehcache | 同步成本高,配置复杂 | 运维成本高 |
| 数据库直接查询 | 性能差,连接数扛不住 | QPS 无法满足要求 |
5.2 Redis 的核心优势
| 优势 | 详细说明 |
|---|---|
| 原子性 | Lua 脚本保证多步操作原子执行,避免并发问题 |
| 高性能 | 内存存储,毫秒级响应,单机可支撑 10W+ QPS |
| 分布式 | 多个网关节点共享状态,数据一致性有保障 |
| 成熟工具 | 限流 Lua 脚本、过期策略、持久化方案都很完善 |
| 运维经验 | 银行内部 Redis 运维经验丰富 |
5.3 限流 Lua 脚本实现
令牌桶算法的 Redis + Lua 实现:
-- token_bucket.lua
local key = KEYS[1]
local capacity = tonumber(ARGV[1]) -- 桶容量
local rate = tonumber(ARGV[2]) -- 每秒填充速率
local requested = tonumber(ARGV[3]) -- 请求数量
local now = tonumber(ARGV[4]) -- 当前时间戳(毫秒)
-- 获取当前令牌数和上次补充时间
local tokens = tonumber(redis.call('hget', key, 'tokens') or capacity)
local last_time = tonumber(redis.call('hget', key, 'last_time') or now)
-- 计算补充的令牌
local elapsed = (now - last_time) / 1000
local new_tokens = math.min(capacity, tokens + elapsed * rate)
if new_tokens >= requested then
new_tokens = new_tokens - requested
redis.call('hset', key, 'tokens', new_tokens)
redis.call('hset', key, 'last_time', now)
redis.call('expire', key, math.ceil(capacity / rate) + 1)
return 1 -- 允许
else
redis.call('hset', key, 'tokens', new_tokens)
redis.call('hset', key, 'last_time', now)
return 0 -- 拒绝
end
Java 调用示例:
@Service
public class RateLimitService {
@Autowired
private StringRedisTemplate redisTemplate;
public boolean tryAcquire(String key, int capacity, int rate, int requested) {
DefaultRedisScript<Long> script = new DefaultRedisScript<>();
script.setScriptSource(new ResourceScriptSource(new ClassPathResource("lua/token_bucket.lua")));
script.setResultType(Long.class);
Long result = redisTemplate.execute(
script,
Collections.singletonList("ratelimit:" + key),
String.valueOf(capacity),
String.valueOf(rate),
String.valueOf(requested),
String.valueOf(System.currentTimeMillis())
);
return result != null && result == 1L;
}
}
5.4 Redis 在系统中的应用场景
| 场景 | 用途 | 数据结构 |
|---|---|---|
| 限流 | API Key 级、模型级、全局限流 | Hash + Lua |
| 配额 | Token 配额、请求数配额 | String(计数器) |
| 会话管理 | JWT 黑名单 | Set |
| 渠道熔断状态 | 存储熔断器状态 | Hash |
| 缓存 | 配置热加载、敏感词缓存 | String(过期时间) |
六、为什么选择 MyBatis-Plus
6.1 为什么不选择 Spring Data JPA?
| 问题 | JPA | MyBatis-Plus | 为什么 MyBatis-Plus 胜出 |
|---|---|---|---|
| SQL 控制 | 自动生成 SQL,复杂查询难控制 | SQL 手写,完全可控 | 银行审计要求 SQL 可追溯 |
| 性能优化 | 懒加载、二级缓存逻辑复杂,调优难 | SQL 直接,性能一目了然 | 性能问题易排查 |
| 学习曲线 | JPA 规则多,容易踩坑(N+1 问题等) | SQL 为主,学习成本低 | 降低学习成本 |
| 团队经验 | 银行普遍熟悉 MyBatis | MyBatis-Plus 是增强版 | 降低迁移成本 |
6.2 MyBatis-Plus 的核心优势
6.2.1 自动 CRUD
@Mapper
public interface ApiKeyMapper extends BaseMapper<ApiKey> {
// 自动继承 CRUD 方法:
// - insert(entity)
// - deleteById(id)
// - updateById(entity)
// - selectById(id)
// - selectList(wrapper)
// ...
}
// 使用示例
@Service
public class ApiKeyService {
@Autowired
private ApiKeyMapper apiKeyMapper;
public ApiKey createApiKey(CreateApiKeyRequest request) {
ApiKey apiKey = new ApiKey();
apiKey.setKeyPrefix(request.getKeyPrefix());
apiKey.setKeyHash(request.getKeyHash());
apiKey.setUserId(request.getUserId());
apiKey.setQuotaRpm(request.getQuotaRpm());
apiKey.setQuotaTpm(request.getQuotaTpm());
apiKey.setStatus(1);
apiKeyMapper.insert(apiKey);
return apiKey;
}
public ApiKey getByPrefix(String prefix) {
LambdaQueryWrapper<ApiKey> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(ApiKey::getKeyPrefix, prefix)
.eq(ApiKey::getStatus, 1);
return apiKeyMapper.selectOne(wrapper);
}
}
6.2.2 复杂查询手写 SQL
@Mapper
public interface ApiKeyMapper extends BaseMapper<ApiKey> {
@Select("SELECT * FROM api_keys " +
"WHERE key_prefix = #{prefix} AND status = 1")
ApiKey findByPrefix(@Param("prefix") String prefix);
@Select("SELECT ak.*, u.username, COUNT(cl.id) AS call_count " +
"FROM api_keys ak " +
"LEFT JOIN users u ON ak.user_id = u.id " +
"LEFT JOIN call_logs cl ON ak.id = cl.api_key_id " +
"WHERE ak.user_id = #{userId} " +
"GROUP BY ak.id " +
"ORDER BY ak.created_at DESC " +
"LIMIT #{limit}")
List<ApiKeyWithStats> listWithStats(@Param("userId") Long userId,
@Param("limit") int limit);
}
6.2.3 分页查询
@Service
public class ApiKeyService {
@Autowired
private ApiKeyMapper apiKeyMapper;
public PageResult<ApiKeyVO> list(int page, int size, Long userId) {
Page<ApiKey> pageParam = new Page<>(page, size);
LambdaQueryWrapper<ApiKey> wrapper = new LambdaQueryWrapper<>();
if (userId != null) {
wrapper.eq(ApiKey::getUserId, userId);
}
wrapper.eq(ApiKey::getStatus, 1)
.orderByDesc(ApiKey::getCreatedAt);
Page<ApiKey> resultPage = apiKeyMapper.selectPage(pageParam, wrapper);
return PageResult.<ApiKeyVO>builder()
.list(convertToVO(resultPage.getRecords()))
.total(resultPage.getTotal())
.page(page)
.size(size)
.build();
}
}
七、为什么选择 Resilience4j
7.1 对比 Hystrix(已停止维护)
Hystrix 是 Netflix 开源的熔断器,但已于 2018 年停止维护。Resilience4j 是官方推荐的替代品:
| 维度 | Hystrix | Resilience4j | 优势 |
|---|---|---|---|
| 维护状态 | 已停止维护 | 活跃维护 | 持续更新 |
| 响应式支持 | 不支持 | 原生支持 | 完美适配 WebFlux |
| 学习曲线 | 较陡 | 较平缓 | 降低学习成本 |
| 功能丰富度 | 基础 | 更完善(限流、Bulkhead) | 扩展性更强 |
7.2 核心模块
| 模块 | 作用 | 在本系统中的应用 |
|---|---|---|
| CircuitBreaker | 熔断器,故障时自动切断 | 渠道故障时自动熔断 |
| Retry | 重试机制,指数退避 | 渠道调用失败时重试 |
| RateLimiter | 应用级限流 | 配合 Redis 实现多层限流 |
| TimeLimiter | 超时控制 | 防止长时间阻塞 |
7.3 配置示例
resilience4j:
circuitbreaker:
instances:
aiService:
failure-rate-threshold: 50 # 失败率阈值 50%
sliding-window-size: 10 # 滑动窗口大小 10 次
minimum-number-of-calls: 5 # 最少 5 次调用后开始计算
wait-duration-in-open-state: 60s # 熔断开启后等待 60 秒
permitted-number-of-calls-in-half-open-state: 3 # 半开状态允许 3 次探测
retry:
instances:
aiService:
max-attempts: 2 # 最多重试 2 次(共 3 次请求)
wait-duration: 1s # 初始等待 1 秒
wait-duration-multiplier: 2 # 指数退避倍数 2
retry-exceptions: # 可重试的异常
- java.net.SocketTimeoutException
- java.net.ConnectException
retryable-status-codes: # 可重试的 HTTP 状态码
- 429 # 限流
- 500 # 服务器错误
- 502 # 网关错误
- 503 # 服务不可用
timelimiter:
instances:
aiService:
timeout-duration: 30s # 超时时间 30 秒
cancel-running-future: true # 超时后取消执行
7.4 代码使用示例
@Service
@RequiredArgsConstructor
public class ChatService {
private final OpenAIAdapter openAIAdapter;
private final CircuitBreaker circuitBreaker;
private final Retry retry;
@Retry(name = "aiService")
@CircuitBreaker(name = "aiService", fallbackMethod = "fallback")
public Mono<ChatResponse> chat(ChatRequest request) {
return openAIAdapter.chat(request)
.timeout(Duration.ofSeconds(30));
}
// 熔断后的降级方法
public Mono<ChatResponse> fallback(ChatRequest request, Exception e) {
log.warn("AI 服务不可用,触发降级: {}", e.getMessage());
// 尝试切换到备用渠道
return channelService.selectBackupChannel(request.getModel())
.flatMap(channel -> {
request.setProvider(channel.getProvider());
return openAIAdapter.chat(request);
});
}
}
八、不引入的组件及原因
| 组件 | 原因 | 替代方案 |
|---|---|---|
| Spring Security | 太重,JWT + API Key Filter 够用 | 自定义 AuthFilter |
| RabbitMQ/Kafka | MVP 不需要异步,调用是同步的 | 简单的线程池 |
| Elasticsearch | 分区表 + 索引够用,运维成本高 | PostgreSQL 全文搜索 |
| 向量数据库 | 知识库(RAG)不是网关的职责边界 | 越界,不实现 |
| Spring Cloud | 单体部署,不需要微服务治理 | Spring Boot 够用 |
8.1 为什么不用 Spring Security?
Spring Security 功能强大,但配置复杂,学习成本高。对于我们的场景:
@Component
@Slf4j
public class AuthFilter implements WebFilter {
@Autowired
private JwtTokenProvider jwtTokenProvider;
@Autowired
private ApiKeyValidator apiKeyValidator;
@Override
public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) {
String path = exchange.getRequest().getPath().value();
// 跳过登录接口
if (path.equals("/api/v1/auth/login")) {
return chain.filter(exchange);
}
String auth = exchange.getRequest().getHeaders().getFirst("Authorization");
if (auth == null || !auth.startsWith("Bearer ")) {
return unauthorized(exchange);
}
String token = auth.substring(7);
// 管理端:JWT 认证
if (path.startsWith("/api/v1/admin")) {
if (!jwtTokenProvider.validateToken(token)) {
return unauthorized(exchange);
}
}
// 调用端:API Key 认证
else if (path.startsWith("/api/v1/chat") ||
path.startsWith("/api/v1/models")) {
if (!apiKeyValidator.validate(token)) {
return unauthorized(exchange);
}
}
return chain.filter(exchange);
}
private Mono<Void> unauthorized(ServerWebExchange exchange) {
exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED);
return exchange.getResponse().setComplete();
}
}
100 行代码解决问题,何必引入 Spring Security?
8.2 为什么不用消息队列?
AI 调用是同步的(用户等结果),不需要异步队列。
如果未来需要"异步任务队列"(如批量生成报表),可以再引入 RabbitMQ。
九、技术栈汇总表
9.1 完整技术栈
| 组件 | 选型 | 版本 | 核心作用 |
|---|---|---|---|
| 语言 | Java | 17 LTS | 团队技能成熟,金融行业标准 |
| 框架 | Spring Boot | 3.2.x | 快速开发,生态成熟 |
| 响应式 | Spring WebFlux | 6.1.x | 流式响应(SSE),高并发 |
| ORM | MyBatis-Plus | 3.5.x | SQL 可控,简化 CRUD |
| 数据库 | PostgreSQL | 16 | 分区表、JSONB、合规性 |
| 缓存 | Redis | 7.x | 限流、配额、会话 |
| 熔断 | Resilience4j | 2.2.x | 熔断、重试、限流 |
| 加密 | Bouncy Castle | 1.7x | AES-256-GCM |
| HTTP Client | WebClient | 6.1.x | 响应式 HTTP 客户端 |
| JWT | jjwt | 0.12.x | JWT Token 生成/校验 |
| 构建 | Maven | 3.9.x | Java 生态标准 |
| 部署 | Docker | 24.x | 容器化,一键启动 |
9.2 Maven 依赖配置
<properties>
<java.version>17</java.version>
<spring-boot.version>3.2.5</spring-boot.version>
</properties>
<dependencies>
<!-- Spring Boot Starter WebFlux -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webflux</artifactId>
</dependency>
<!-- Spring Boot Starter Data Redis -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis-reactive</artifactId>
</dependency>
<!-- Spring Boot Starter Validation -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
<!-- PostgreSQL Driver -->
<dependency>
<groupId>org.postgresql</groupId>
<artifactId>postgresql</artifactId>
<scope>runtime</scope>
</dependency>
<!-- MyBatis-Plus -->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.5.5</version>
</dependency>
<!-- R2DBC (响应式数据库访问) -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-r2dbc</artifactId>
</dependency>
<dependency>
<groupId>org.postgresql</groupId>
<artifactId>r2dbc-postgresql</artifactId>
<scope>runtime</scope>
</dependency>
<!-- Resilience4j -->
<dependency>
<groupId>io.github.resilience4j</groupId>
<artifactId>resilience4j-spring-boot3</artifactId>
<version>2.2.2</version>
</dependency>
<!-- JWT -->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-api</artifactId>
<version>0.12.3</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-impl</artifactId>
<version>0.12.3</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-jackson</artifactId>
<version>0.12.3</version>
<scope>runtime</scope>
</dependency>
<!-- Bouncy Castle (AES-256-GCM) -->
<dependency>
<groupId>org.bouncycastle</groupId>
<artifactId>bcprov-jdk18on</artifactId>
<version>1.77</version>
</dependency>
<!-- Lombok -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<!-- Test Dependencies -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.projectreactor</groupId>
<artifactId>reactor-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
9.3 Docker Compose 配置
version: '3.8'
services:
postgres:
image: postgres:16
container_name: bank-ai-gateway-postgres
environment:
POSTGRES_DB: bank_ai_gateway
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
ports:
- "5432:5432"
volumes:
- postgres-data:/var/lib/postgresql/data
- ./sql:/docker-entrypoint-initdb.d
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres"]
interval: 10s
timeout: 5s
retries: 5
redis:
image: redis:7-alpine
container_name: bank-ai-gateway-redis
ports:
- "6379:6379"
volumes:
- redis-data:/data
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 10s
timeout: 5s
retries: 5
volumes:
postgres-data:
redis-data:
🔔 关注有价值
关注「亦暖筑序」,第一时间获取系列更新:
已发布
- 第 01 章:参考 OpenAI 架构设计企业级 AI 网关
- 第 02 章:7 个模块详解 + 请求完整流转路径
- 第 03 章:技术选型:为什么是 Spring Boot 3 WebFlux + PostgreSQL + Redis(本篇)
下一篇预告
- 第 04 章:项目初始化、依赖配置、Docker Compose 一键启动
源码进度
🐙 开发进度在 GitHub 持续更新:github.com/ynzz-j/bank…
📦 完成最新 MVP 后,完整源码统一上传 Gitee
关于源码和开发进度
这个项目的开发进度会在 GitHub 上持续更新,每一章对应的代码推进都会提交记录,你可以随时看到项目从零到一的成长过程。
GitHub 地址:github.com/ynzz-j/bank…
完整源码计划在完成最新 MVP 版本后,统一上传到 Gitee,届时会在公众号和系列文章里同步通知。