AI 网关实战(三):怎么做技术选型?

0 阅读14分钟

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 Boot17 LTS + 3.2.x
响应式Spring WebFlux6.1.x
ORMMyBatis-Plus3.5.x
数据库PostgreSQL16
缓存Redis7.x
熔断Resilience4j2.2.x
构建Maven3.9.x
部署Docker + Docker Compose24.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 MVCSpring 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 2Spring Boot 3
Java 版本基线Java 8/11/17Java 17 LTS(强制)
虚拟线程不支持原生支持(GraalVM)
响应式栈WebFlux 功能较弱显著增强
长期维护2025 年底停止持续维护中

Java 17 的性能提升

  • 更高效的垃圾回收器(ZGC)
  • 记录类(Record)减少样板代码
  • 模式匹配提升代码可读性
  • 更强的类型推断

四、为什么选择 PostgreSQL

4.1 对比 MySQL

维度PostgreSQLMySQL为什么 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?

问题JPAMyBatis-Plus为什么 MyBatis-Plus 胜出
SQL 控制自动生成 SQL,复杂查询难控制SQL 手写,完全可控银行审计要求 SQL 可追溯
性能优化懒加载、二级缓存逻辑复杂,调优难SQL 直接,性能一目了然性能问题易排查
学习曲线JPA 规则多,容易踩坑(N+1 问题等)SQL 为主,学习成本低降低学习成本
团队经验银行普遍熟悉 MyBatisMyBatis-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 是官方推荐的替代品:

维度HystrixResilience4j优势
维护状态已停止维护活跃维护持续更新
响应式支持不支持原生支持完美适配 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/KafkaMVP 不需要异步,调用是同步的简单的线程池
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 完整技术栈

组件选型版本核心作用
语言Java17 LTS团队技能成熟,金融行业标准
框架Spring Boot3.2.x快速开发,生态成熟
响应式Spring WebFlux6.1.x流式响应(SSE),高并发
ORMMyBatis-Plus3.5.xSQL 可控,简化 CRUD
数据库PostgreSQL16分区表、JSONB、合规性
缓存Redis7.x限流、配额、会话
熔断Resilience4j2.2.x熔断、重试、限流
加密Bouncy Castle1.7xAES-256-GCM
HTTP ClientWebClient6.1.x响应式 HTTP 客户端
JWTjjwt0.12.xJWT Token 生成/校验
构建Maven3.9.xJava 生态标准
部署Docker24.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,届时会在公众号和系列文章里同步通知。