AI 网关实战(三):技术选型详解 - Spring Boot 3 WebFlux + PostgreSQL + Redis

银行 AI 网关实战系列第 3 篇,深入讲解技术选型的决策过程,包括 Java、Spring Boot 3 WebFlux、PostgreSQL、Redis、MyBatis-Plus、Resilience4j 等核心组件的选择理由。

目录


一、前置约束明确

在做技术选型之前,必须先明确项目的约束条件,这是所有决策的基础:

  1. 业务场景:银行内部 AI 网关,不是通用的 AI 应用
  2. 关键指标优先级:安全合规 > 稳定性 > 开发效率 > 性能
  3. 团队技能:Java 后端团队,运维资源有限
  4. 部署环境:内网私有部署,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 网关有两个核心需求:

  1. 流式响应(SSE):AI 对话必须是流式的,否则用户体验极差
  2. 高并发代理:网关本质是"中间人",吞吐量直接影响系统承载能力

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 持续更新:https://github.com/ynzz-j/bank-ai-gateway

📦 完成最新 MVP 后,完整源码统一上传 Gitee


关于源码和开发进度

这个项目的开发进度会在 GitHub 上持续更新,每一章对应的代码推进都会提交记录,你可以随时看到项目从零到一的成长过程。

GitHub 地址:https://github.com/ynzz-j/bank-ai-gateway

完整源码计划在完成最新 MVP 版本后,统一上传到 Gitee,届时会在公众号和系列文章里同步通知。

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐