ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

AI+DDD+Redis+Nginx:构建高可用后端架构实战

AI+DDD+Redis+Nginx:构建高可用后端架构实战 1. 为什么是 AI DDD Redis Nginx 这套组合1.1 后端架构的现状与痛点后端系统进入微服务或中台化阶段后最大的问题往往不是某个框架不会用而是整个链路的职责划分、缓存策略、网关接入和团队协作方式缺乏统一约束。我在业务迭代中经常见到下面这类现象Service层越写越厚订单逻辑、缓存逻辑、权限判断、外部接口调用全部堆在一个类里改一个需求需要同时动十几个方法。Redis 缓存被“到处塞”同一个订单数据在多个 Key 中重复存放过期时间靠拍脑袋线上偶尔出现脏数据。Nginx 配置零散在不同运维手里有人做了限流有人没做后端服务扩容后 upstream 列表没有同步更新。AI 能力开始接入业务系统时团队直接把“调用大模型的代码”写在 Controller 里没有超时、没有降级、没有审计日志。这套组合之所以值得系统学习是因为它把四个看似独立的技术点串成了一个闭环DDD 负责把业务边界划清楚Redis 负责把热点数据扛住Nginx 负责把流量入口管起来AI 则作为外部能力被有序地接入到应用链路中。四个技术点互为补充而不是各自为政。1.2 四个技术各自的定位为了后面不绕晕我先把四个技术栈的定位说清楚。AI在本文讨论的架构中AI 大模型是后端服务的能力供给方比如智能摘要、语义检索、售后问答、订单异常分析。它不改变系统的分层结构而是作为“外部服务”被基础设施层适配。DDD领域驱动设计核心是把业务复杂度通过限界上下文、实体、值对象、聚合、仓储等概念进行建模让代码结构反映业务结构而不是反映数据库表结构。Redis高性能键值存储承担热点缓存、分布式锁、分布式会话、计数器等职责解决数据库压力和并发安全问题。Nginx高性能 Web 服务器与反向代理负责负载均衡、路由转发、限流、SSL 证书、静态资源托管是后端服务对外唯一的流量入口。用一句话概括Nginx 管“进得来”Redis 管“扛得住”DDD 管“理得清”AI 管“做得更好”。1.3 本文的实战场景与读者收益后面我会用一个完整的示例项目来讲透这套架构。场景设定为一个订单查询与智能分析系统。用户在商城查询最近订单列表系统先走 Redis 缓存缓存未命中再查数据库然后通过 AI 服务对订单列表生成一句摘要比如“您最近 7 天共消费 5 笔最大金额 1299 元有一笔订单收货地址信息不完整建议补充”。读者学完本文可以掌握如何按 DDD 四层结构拆分工程依赖方向应该怎么控制。Redis 缓存读写怎么设计如何避免缓存穿透、击穿、雪崩。Nginx 反向代理、负载均衡、限流配置怎么写才规范。AI 大模型接口在后端链路中如何安全、优雅地接入。2. 系统整体架构设计2.1 分层架构总览先给出一张整体分层图后面所有代码都围绕这张图展开。客户端 ↓ Nginx反向代理 / 负载均衡 / 限流 ↓ Spring Boot 应用 ├── 接口层Controller ├── 应用层Application Service ├── 领域层Domain └── 基础设施层Infrastructure ├── MySQL订单数据 ├── Redis缓存 / 分布式锁 └── AI 大模型 API智能摘要这是 DDD 经典的四层架构思路。接口层接收 HTTP 请求并做参数校验应用层负责用例编排比如“查询订单并生成摘要”这个完整流程领域层承载订单实体、状态枚举、仓储接口基础设施层实现仓储、缓存、外部 AI 调用等细节。需要注意的是依赖方向是向内的接口层依赖应用层应用层依赖领域层领域层不依赖任何外部框架基础设施层反向实现领域层定义的接口。这是 DDD 最容易出错的地方后面实战部分我会专门演示。2.2 一次请求的完整流转用户发来一个查询请求后完整链路是这样的客户端请求到达 NginxNginx 根据location /api/规则将请求转发给后端 Spring Boot 实例。Spring Boot 的 Controller 接收请求校验参数后调用应用层服务。应用层服务先去 Redis 查询缓存如果命中就直接返回。如果缓存未命中通过领域层定义的仓储接口查询数据实际由基础设施层的实现去 MySQL 读取。数据返回后应用层将领域对象转换为 DTO 并回填 Redis设置过期时间。应用层再调用 AI 客户端服务传入订单摘要所需的文本内容获取 AI 返回结果。最后将订单数据与 AI 摘要组装后返回给前端。这个流程里DDD 决定了第 3、4、5 步的代码放在哪一层Redis 决定了数据如何被临时存储Nginx 决定了请求从哪个入口进入AI 决定了第 6 步的外部能力如何被编排。2.3 AI 能力在后端架构中的接入位置很多团队接入 AI 时容易犯一个错误直接在 Controller 或 Service 里写死模型接口 URL、API Key并且把大段提示词和业务逻辑混在一起导致后续模型升级或更换供应商时改动面很大。在 DDD 约束下AI 能力应该被看作一个外部适配器。领域层只定义“需要什么”应用层只负责“编排流程”真正调用大模型 HTTP 接口的逻辑放在基础设施层并且通过配置类进行参数隔离。这样当模型从 A 供应商切换到 B 供应商时领域层和应用层代码完全不用改。我的建议是在项目初期就把 AI 调用封装成独立的 Client并做好超时、降级、日志审计。不要等到线上因为模型接口慢导致请求堆积时再补救。3. 环境准备与项目初始化3.1 运行环境说明不同团队的开发环境差异较大下面表格中的版本只代表本文示例环境的常见组合你需要根据自己项目的实际情况调整。组件示例版本说明JDK17使用 record、switch 表达式等新特性Spring Boot3.x以你创建项目时的稳定版为准Maven3.8项目构建工具MySQL8.x订单数据持久化存储Redis6.x / 7.x缓存与分布式锁Nginx1.24 / 1.25反向代理与负载均衡IDEIntelliJ IDEA也可以使用 Eclipse 或 VS Code文章重点演示配置思路如果你的版本不同不要慌张重点看分层结构和配置逻辑而不是机械复制版本号。3.2 创建 Spring Boot 项目推荐使用 Spring Initializr 创建基础工程也可以直接在 IDEA 中新建 Spring Boot 项目。创建时需要选择以下依赖Spring Web提供 REST 接口能力。Spring Data Redis操作 Redis。Spring Data JPA简化数据访问便于实现 DDD 仓储。MySQL Driver连接 MySQL。Validation参数校验。Lombok减少样板代码。项目基础坐标可以设置为groupId: com.example artifactId: arch-demo package: com.example.archdemo创建完成后项目目录结构如下后面代码都放在对应包中arch-demo ├── src/main/java/com/example/archdemo │ ├── interfaces # 接口层 │ ├── application # 应用层 │ ├── domain # 领域层 │ └── infrastructure # 基础设施层 ├── src/main/resources │ └── application.yml └── pom.xml3.3 添加核心依赖pom.xml 中的关键依赖片段如下。Lombok 不是必须的但能减少实体类的 getter/setter 代码读者可以按团队规范选择是否使用。dependencies dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-data-redis/artifactId /dependency dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-data-jpa/artifactId /dependency dependency groupIdcom.mysql/groupId artifactIdmysql-connector-j/artifactId scoperuntime/scope /dependency dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-validation/artifactId /dependency dependency groupIdorg.projectlombok/groupId artifactIdlombok/artifactId optionaltrue/optional /dependency /dependencies如果你的项目使用的是 MyBatis 或 MyBatis-Plus也没有问题Mapper 可以放在基础设施层去实现领域层定义的仓储接口思路是完全一致的。下面示例以 Spring Data JPA 为例因为它与 DDD 的 Repository 模式契合度更高。3.4 基础配置在src/main/resources/application.yml中写入基础配置。数据库密码和 AI API Key 不要硬编码在配置文件里建议通过环境变量注入。server: port: 8080 spring: application: name: arch-demo datasource: url: jdbc:mysql://localhost:3306/arch_demo?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/Shanghai username: root password: ${DB_PASSWORD:root} jpa: hibernate: ddl-auto: update show-sql: true data: redis: host: localhost port: 6379 password: ${REDIS_PASSWORD:} timeout: 3s ai: endpoint: ${AI_ENDPOINT:http://localhost:8001/chat/completions} api-key: ${AI_API_KEY:} model: ${AI_MODEL:demo-model} timeout-seconds: 3配置说明spring.jpa.hibernate.ddl-auto在测试环境可以使用update生产环境推荐使用 Flyway 管理数据库脚本避免实体变更导致表结构不可控。Redis 的timeout指的是连接超时时间不能设置太长否则 Redis 故障时应用线程会被拖垮。ai.*配置由后面自定义的AiProperties读取AI_API_KEY必须通过环境变量注入不能提交到 Git 仓库。4. 基于 DDD 的领域模型落地4.1 DDD 工程模块划分DDD 并不强制要求多模块对大多数中小团队来说在单模块内按包名划分是性价比最高的方式。本文采用以下分包方式interfaces # 控制器层只做参数接收与响应封装 application # 应用服务层负责用例编排 domain # 领域层实体、值对象、枚举、仓储接口、领域服务 infrastructure # 基础设施层JPA 实现、Redis 实现、AI 客户端核心约束是domain包不能依赖spring、jpa、redis这些技术框架它是纯业务逻辑infrastructure依赖domain反向实现仓储接口application依赖domain和必要的 DTO 转换interfaces依赖application。4.2 领域层实体、值对象与枚举先定义一个订单聚合根Order。订单包含订单 ID、客户 ID、订单状态、总金额、下单时间以及订单明细列表。这里将 JPA 注解直接写在领域实体上是为了让示例尽量精简更严格的架构可以使用单独的映射对象但核心思想是领域行为要放在实体内部。// 文件路径src/main/java/com/example/archdemo/domain/order/Order.java package com.example.archdemo.domain.order; import jakarta.persistence.*; import java.math.BigDecimal; import java.time.LocalDateTime; import java.util.ArrayList; import java.util.List; Entity Table(name t_order) public class Order { Id GeneratedValue(strategy GenerationType.IDENTITY) private Long id; private Long customerId; Enumerated(EnumType.STRING) private OrderStatus status; private BigDecimal totalAmount; private LocalDateTime createdAt; OneToMany(cascade CascadeType.ALL, fetch FetchType.LAZY) JoinColumn(name order_id) private ListOrderItem items new ArrayList(); protected Order() { } public Order(Long customerId, ListOrderItem items) { this.customerId customerId; this.status OrderStatus.CREATED; this.createdAt LocalDateTime.now(); this.items items; this.totalAmount calculateTotal(items); } private BigDecimal calculateTotal(ListOrderItem items) { return items.stream() .map(OrderItem::subtotal) .reduce(BigDecimal.ZERO, BigDecimal::add); } public void markPaid() { if (this.status ! OrderStatus.CREATED) { throw new IllegalStateException(只有已创建状态的订单可以变更为已支付); } this.status OrderStatus.PAID; } public Long getId() { return id; } public Long getCustomerId() { return customerId; } public OrderStatus getStatus() { return status; } public BigDecimal getTotalAmount() { return totalAmount; } public LocalDateTime getCreatedAt() { return createdAt; } public ListOrderItem getItems() { return items; } }订单明细OrderItem是一个值对象它没有自己的身份标识不可被单独修改。// 文件路径src/main/java/com/example/archdemo/domain/order/OrderItem.java package com.example.archdemo.domain.order; import jakarta.persistence.Embeddable; import java.math.BigDecimal; Embeddable public class OrderItem { private String productName; private Integer quantity; private BigDecimal price; protected OrderItem() { } public OrderItem(String productName, Integer quantity, BigDecimal price) { this.productName productName; this.quantity quantity; this.price price; } public BigDecimal subtotal() { return price.multiply(BigDecimal.valueOf(quantity)); } public String getProductName() { return productName; } public Integer getQuantity() { return quantity; } public BigDecimal getPrice() { return price; } }订单状态枚举// 文件路径src/main/java/com/example/archdemo/domain/order/OrderStatus.java package com.example.archdemo.domain.order; public enum OrderStatus { CREATED, PAID, SHIPPED, COMPLETED, CANCELLED }这里的关键点是实体的状态变更通过领域方法完成比如markPaid()内部会校验当前状态是否合法外部不能直接对状态字段赋值。这样可以避免业务规则散落在应用层或 Controller 中。4.3 应用层编排用例应用层不写业务规则只做编排。这里定义订单应用服务里面包含“查询订单详情并生成摘要”的用例方法。真正的数据来源是领域层接口OrderRepository真正的 AI 调用是基础设施层的AiAnalysisClient应用层只是把它们按顺序组合。// 文件路径src/main/java/com/example/archdemo/application/OrderApplicationService.java package com.example.archdemo.application; import com.example.archdemo.application.dto.OrderDetailDTO; import com.example.archdemo.domain.order.Order; import com.example.archdemo.domain.order.OrderRepository; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; Service public class OrderApplicationService { private final OrderRepository orderRepository; private final OrderCacheService orderCacheService; private final AiAnalysisClient aiAnalysisClient; public OrderApplicationService(OrderRepository orderRepository, OrderCacheService orderCacheService, AiAnalysisClient aiAnalysisClient) { this.orderRepository orderRepository; this.orderCacheService orderCacheService; this.aiAnalysisClient aiAnalysisClient; } Transactional(readOnly true) public OrderDetailDTO loadOrderDetail(Long orderId) { // 1. 先查缓存 OrderDetailDTO cached orderCacheService.get(orderId); if (cached ! null) { return cached; } // 2. 缓存未命中查数据库 Order order orderRepository.findById(orderId); // 3. 转换为 DTO 并写入缓存 OrderDetailDTO dto OrderDetailDTO.fromDomain(order); orderCacheService.put(orderId, dto); // 4. 调用 AI 生成摘要失败时降级 String summary aiAnalysisClient.generateSummary(dto.descriptionText()); dto.setAiSummary(summary); return dto; } }这个类体现了 DDD 缓存 AI 整合的核心领域对象出现在应用层但缓存逻辑、AI 调用逻辑都被封装到各自的基础设施组件中应用层代码保持稳定。4.4 基础设施层仓储实现与数据访问领域层定义仓储接口基础设施层用 JPA 实现。先看接口// 文件路径src/main/java/com/example/archdemo/domain/order/OrderRepository.java package com.example.archdemo.domain.order; public interface OrderRepository { Order findById(Long orderId); Order save(Order order); }然后在基础设施层定义 JPA 仓储注意它继承 Spring Data JPA 的JpaRepository但 DDD 的OrderRepository接口由下面的实现类负责实现。// 文件路径src/main/java/com/example/archdemo/infrastructure/repository/OrderJpaRepository.java package com.example.archdemo.infrastructure.repository; import com.example.archdemo.domain.order.Order; import org.springframework.data.jpa.repository.JpaRepository; public interface OrderJpaRepository extends JpaRepositoryOrder, Long { }最后是 DDD 仓储实现类// 文件路径src/main/java/com/example/archdemo/infrastructure/repository/OrderRepositoryImpl.java package com.example.archdemo.infrastructure.repository; import com.example.archdemo.domain.order.Order; import com.example.archdemo.domain.order.OrderRepository; import org.springframework.stereotype.Repository; Repository public class OrderRepositoryImpl implements OrderRepository { private final OrderJpaRepository orderJpaRepository; public OrderRepositoryImpl(OrderJpaRepository orderJpaRepository) { this.orderJpaRepository orderJpaRepository; } Override public Order findById(Long orderId) { return orderJpaRepository.findById(orderId) .orElseThrow(() - new IllegalArgumentException(订单不存在: orderId)); } Override public Order save(Order order) { return orderJpaRepository.save(order); } }依赖方向体现得很清楚应用层依赖domain包中的OrderRepository而实际 Bean 是OrderRepositoryImplSpring 容器在启动时会自动完成注入。以后想把 JPA 换成 MyBatis只需要在基础设施层新增一个实现类领域层和应用层一行都不用改。5. Redis 缓存整合与分布式锁实战5.1 缓存设计原则Redis 缓存不是越多越好接入前必须想清楚三个问题数据是否适合缓存读多写少、数据一致性要求不高的数据适合缓存强一致性的账户余额等数据不适合。Key 如何设计推荐使用“业务域:业务类型:业务ID”的格式例如order:detail:1001方便按前缀统一管理。过期时间多长根据数据变化频率设置一般业务数据建议 5 到 30 分钟热点数据可以单独延长。Redis 数据类型中本文主要使用 String 类型存储 JSON 字符串。如果你的场景需要存储对象部分字段也可以使用 Hash把字段名作为 Hash 的 field。5.2 缓存服务封装为了避免业务代码中散落大量StringRedisTemplate操作我建议封装一个OrderCacheService把订单 DTO 的序列化和反序列化统一管理。// 文件路径src/main/java/com/example/archdemo/infrastructure/cache/OrderCacheService.java package com.example.archdemo.infrastructure.cache; import com.example.archdemo.application.dto.OrderDetailDTO; import com.fasterxml.jackson.databind.ObjectMapper; import org.springframework.data.redis.core.StringRedisTemplate; import org.springframework.stereotype.Service; import java.time.Duration; Service public class OrderCacheService { private static final String ORDER_DETAIL_KEY_PREFIX order:detail:; private static final Duration ORDER_DETAIL_TTL Duration.ofMinutes(10); private final StringRedisTemplate stringRedisTemplate; private final ObjectMapper objectMapper; public OrderCacheService(StringRedisTemplate stringRedisTemplate, ObjectMapper objectMapper) { this.stringRedisTemplate stringRedisTemplate; this.objectMapper objectMapper; } public OrderDetailDTO get(Long orderId) { String key ORDER_DETAIL_KEY_PREFIX orderId; String json stringRedisTemplate.opsForValue().get(key); if (json null) { return null; } try { return objectMapper.readValue(json, OrderDetailDTO.class); } catch (Exception e) { // 反序列化失败时删除脏数据避免缓存永远无法命中 stringRedisTemplate.delete(key); return null; } } public void put(Long orderId, OrderDetailDTO dto) { String key ORDER_DETAIL_KEY_PREFIX orderId; try { String json objectMapper.writeValueAsString(dto); stringRedisTemplate.opsForValue().set(key, json, ORDER_DETAIL_TTL); } catch (Exception e) { // 序列化失败不要影响主流程记录日志后继续 throw new IllegalStateException(订单缓存写入失败, e); } } }这里有几个工程细节反序列化失败时主动删除 Key 而不是直接抛异常因为脏数据留在 Redis 里会导致接口一直报错TTL 统一管理避免每个调用方随意指定过期时间。5.3 缓存空值防穿透如果订单 ID 在数据库中不存在每次都穿透到数据库查询就会造成缓存穿透。常规方案是缓存一个值为 null 的标记同时设置较短 TTL比如 2 分钟。public OrderDetailDTO getWithNullCache(Long orderId) { String key ORDER_DETAIL_KEY_PREFIX orderId; String json stringRedisTemplate.opsForValue().get(key); if (json ! null) { if (NULL.equals(json)) { return null; } try { return objectMapper.readValue(json, OrderDetailDTO.class); } catch (Exception e) { stringRedisTemplate.delete(key); return null; } } return null; }这种方式需要与查询逻辑配合当数据库查不到时写入一个“NULL”字符串标记并设置短 TTL。注意只能缓存“确定不存在”的 Key不能缓存临时查询失败导致的数据。5.4 分布式锁与并发安全高并发场景下同一个订单的首次查询可能同时有多个线程打到数据库造成缓存击穿。解决办法是加分布式锁让只有一个线程去查询数据库并回填缓存其它线程等待后直接取缓存。Redisson 是生产环境最推荐的分布式锁实现因为它支持看门狗自动续期避免业务执行时间过长导致锁过期。以下示例使用 Spring Data Redis 的setIfAbsent演示核心原理生产环境建议替换为 Redisson。// 文件路径src/main/java/com/example/archdemo/infrastructure/cache/OrderDetailLoader.java package com.example.archdemo.infrastructure.cache; import org.springframework.data.redis.core.StringRedisTemplate; import org.springframework.stereotype.Component; import java.time.Duration; import java.util.UUID; Component public class OrderDetailLoader { private final StringRedisTemplate stringRedisTemplate; public OrderDetailLoader(StringRedisTemplate stringRedisTemplate) { this.stringRedisTemplate stringRedisTemplate; } public boolean tryLock(String key, String requestId, Duration timeout) { return Boolean.TRUE.equals( stringRedisTemplate.opsForValue().setIfAbsent(key, requestId, timeout) ); } public void unlock(String key, String requestId) { String value stringRedisTemplate.opsForValue().get(key); if (requestId.equals(value)) { stringRedisTemplate.delete(key); } } }注意释放锁时不能直接delete必须先判断持有者是否为当前线程否则可能会误删其它线程的锁。生产环境要使用 Redisson 的RLock配合tryLock和unlock更加安全。缓存雪崩的应对方案是过期时间增加随机值避免大量 Key 在同一时刻集中过期。比如在固定的 10 分钟基础上给每个 Key 增加 0 到 60 秒的随机偏差。6. Nginx 网关与反向代理配置6.1 Nginx 在网络链路中的角色Nginx 在整个架构中承担流量入口的职责。客户端只知道 Nginx 的域名和端口不知道后端到底有几个 Spring Boot 实例。Nginx 根据配置把请求转发给后端实现负载均衡和故障转移。说到代理需要区分两个概念。正向代理是站在客户端一侧替客户端访问外部资源反向代理是站在服务端一侧替后端服务接收客户端请求。Nginx 在这里就是反向代理。6.2 反向代理与负载均衡配置下面是一份完整的 Nginx 配置示例重点看upstream和location两个部分。# 文件路径/etc/nginx/nginx.conf 或 /etc/nginx/conf.d/arch-demo.conf upstream backend_order { server 127.0.0.1:8080 weight3; server 127.0.0.1:8081 weight1; keepalive 32; } server { listen 80; server_name api.example.com; location /api/ { proxy_pass http://backend_order; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; proxy_connect_timeout 5s; proxy_read_timeout 10s; proxy_send_timeout 10s; } }参数说明weight表示权重3 和 1 意味着 8080 端口实例承担的流量是 8081 的三倍。keepalive 32表示 Nginx 与后端之间保持 32 个长连接减少频繁建连的开销。proxy_set_header X-Real-IP $remote_addr用于把客户端真实 IP 传给后端否则后端日志里看到的都是 Nginx 的 IP。proxy_read_timeout建议根据业务耗时设置如果后端需要调用 AI 接口需要预留足够的响应时间但也不能过长否则后端故障时前端会一直等待。6.3 限流与安全配置高并发场景下Nginx 限流是保护后端的第一道防线。这里使用limit_req_zone实现基于客户端 IP 的请求限流。limit_req_zone $binary_remote_addr zoneapi_limit:10m rate10r/s; server { listen 80; server_name api.example.com; location /api/ { limit_req zoneapi_limit burst20 nodelay; proxy_pass http://backend_order; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; } }rate10r/s表示平均每秒允许 10 个请求burst20表示允许瞬时超过限制的请求排队nodelay表示超出的请求直接返回 503。除此之外生产环境还应该开启 gzip 压缩、隐藏 Nginx 版本号、配置访问日志的格式以及按需配置 HTTPS 证书。如果配置了多个后端节点升级或重启后端服务时不需要重启 Nginx。完成变更后先执行nginx -t检查配置语法确认无误后再执行nginx -s reload平滑加载这个过程不会中断正在处理的请求。7. 完整实战AI 辅助的订单查询链路7.1 业务场景设定现在把前面所有模块串成一个可以运行的最小项目。假设我们有如下需求用户通过GET /api/orders/{orderId}接口查询订单详情接口返回订单基础信息以及 AI 生成的消费摘要。如果 AI 服务不可用接口仍然返回订单数据AI 摘要字段显示降级提示。这个需求刚好覆盖了本文全部技术点Nginx 入口、Controller 接收请求、应用层编排、领域仓储查询、Redis 缓存、AI 客户端调用。7.2 DTO 与应用服务先定义订单详情 DTO它承载返回给前端的结构// 文件路径src/main/java/com/example/archdemo/application/dto/OrderDetailDTO.java package com.example.archdemo.application.dto; import com.example.archdemo.domain.order.Order; import com.example.archdemo.domain.order.OrderItem; import java.math.BigDecimal; import java.time.LocalDateTime; import java.util.List; public class OrderDetailDTO { private Long orderId; private Long customerId; private String status; private BigDecimal totalAmount; private LocalDateTime createdAt; private ListString productNames; private String aiSummary; public static OrderDetailDTO fromDomain(Order order) { OrderDetailDTO dto new OrderDetailDTO(); dto.orderId order.getId(); dto.customerId order.getCustomerId(); dto.status order.getStatus().name(); dto.totalAmount order.getTotalAmount(); dto.createdAt order.getCreatedAt(); dto.productNames order.getItems().stream() .map(OrderItem::getProductName) .toList(); return dto; } public String descriptionText() { return 订单号 orderId 客户 customerId 共 productNames.size() 件商品总金额 totalAmount 元状态 status; } public Long getOrderId() { return orderId; } public Long getCustomerId() { return customerId; } public String getStatus() { return status; } public BigDecimal getTotalAmount() { return totalAmount; } public LocalDateTime getCreatedAt() { return createdAt; } public ListString getProductNames() { return productNames; } public String getAiSummary() { return aiSummary; } public void setAiSummary(String aiSummary) { this.aiSummary aiSummary; } }注意AI 摘要字段不作为缓存的一部分因为每次摘要内容可能不同。缓存只缓存订单基础信息摘要每次请求时动态生成这样更合理。7.3 AI 客户端实现AI 客户端放在基础设施层使用 Spring 的RestTemplate调用大模型 HTTP 接口。不要硬编码 URL 和 Key通过自定义配置类读取。// 文件路径src/main/java/com/example/archdemo/infrastructure/ai/AiAnalysisClient.java package com.example.archdemo.infrastructure.ai; import org.springframework.boot.web.client.RestTemplateBuilder; import org.springframework.stereotype.Component; import org.springframework.web.client.RestClientException; import org.springframework.web.client.RestTemplate; import java.time.Duration; import java.util.Map; Component public class AiAnalysisClient { private final AiProperties aiProperties; private final RestTemplate restTemplate; public AiAnalysisClient(AiProperties aiProperties, RestTemplateBuilder builder) { this.aiProperties aiProperties; this.restTemplate builder .setConnectTimeout(Duration.ofSeconds(aiProperties.timeoutSeconds())) .setReadTimeout(Duration.ofSeconds(aiProperties.timeoutSeconds())) .build(); } public String generateSummary(String content) { // 这里采用通用请求体结构实际接入时需要按模型供应商协议调整 MapString, Object requestBody Map.of( model, aiProperties.model(), messages, new Object[]{ Map.of(role, system, content, 你是一个订单分析助手请用一句话概括用户订单特征。), Map.of(role, user, content, content) } ); try { String response restTemplate.postForObject( aiProperties.endpoint(), requestBody, String.class ); return parseSummary(response); } catch (RestClientException e) { // 降级处理AI 服务不可用时不影响主流程 return AI 摘要暂不可用请稍后重试。; } } private String parseSummary(String response) { if (response null || response.isBlank()) { return AI 摘要暂不可用请稍后重试。; } // 不同模型返回格式不同这里只做简单截断 String text response.replace(\n, ); return text.length() 200 ? text.substring(0, 200) : text; } }配置类// 文件路径src/main/java/com/example/archdemo/infrastructure/ai/AiProperties.java package com.example.archdemo.infrastructure.ai; import org.springframework.boot.context.properties.ConfigurationProperties; ConfigurationProperties(prefix ai) public record AiProperties( String endpoint, String apiKey, String model, long timeoutSeconds ) { }别忘了在启动类或配置类上启用配置绑定ConfigurationPropertiesScan7.4 控制层代码Controller 只做参数接收和响应返回不包含业务逻辑// 文件路径src/main/java/com/example/archdemo/interfaces/OrderController.java package com.example.archdemo.interfaces; import com.example.archdemo.application.OrderApplicationService; import com.example.archdemo.application.dto.OrderDetailDTO; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; RestController RequestMapping(/api/orders) public class OrderController { private final OrderApplicationService orderApplicationService; public OrderController(OrderApplicationService orderApplicationService) { this.orderApplicationService orderApplicationService; } GetMapping(/{orderId}) public OrderDetailDTO getOrderDetail(PathVariable Long orderId) { return orderApplicationService.loadOrderDetail(orderId); } }7.5 完整 Nginx 配置将 6.2 和 6.3 的配置合并形成一份可用的上游转发配置# 文件路径/etc/nginx/conf.d/arch-demo.conf upstream backend_order { server 127.0.0.1:8080 weight3; server 127.0.0.1:8081 weight1; keepalive 32; } limit_req_zone $binary_remote_addr zoneapi_limit:10m rate10r/s; server { listen 80; server_name api.example.com; access_log /var/log/nginx/arch-demo-access.log; error_log /var/log/nginx/arch-demo-error.log; location /api/ { limit_req zoneapi_limit burst20 nodelay; proxy_pass http://backend_order; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; proxy_connect_timeout 5s; proxy_read_timeout 15s; proxy_send_timeout 10s; } }7.6 运行与验证确认 MySQL 中已创建arch_demo数据库Redis 和 Nginx 均已启动。然后执行mvn spring-boot:run此时再向 Nginx 发起请求假设本机 Nginx 监听 80 端口curl http://localhost/api/orders/1第一次请求数据库和 AI 都会被调用响应中会包含aiSummary字段。第二次请求订单数据命中 Redis 缓存日志中不会出现 SQL 打印。停止 AI 服务后再次请求接口依然正常返回aiSummary降级为“AI 摘要暂不可用请稍后重试”。这验证了整条链路的完整性Nginx 正常转发Redis 正确缓存AI 故障被优雅降级。8. 常见问题排查问题现象常见原因解决思路缓存与数据库数据不一致更新数据库后没有删除缓存并发场景下旧数据回填先更新数据库再删除缓存设置过期时间兜底必要时使用延迟双删或版本号Redis 连接超时Redis 未启动、密码错误、网络不通检查redis-cli ping确认配置主机端口和密码生产环境使用连接池并设置合理超时Nginx 返回 502 Bad Gateway后端服务未启动、后端进程崩溃、Nginx 访问超时检查后端端口连通性curl 127.0.0.1:8080/api/orders/1然后查看 Nginx error logNginx 返回 503 Service Temporarily Unavailable限流触发upstream 中所有节点不可用查看limit_req配置和upstream节点健康状态必要时增加max_fails和fail_timeoutAI 摘要接口拖慢主流程AI 服务响应慢超时设置过长设置独立读超时AI 调用失败必须降级还可以引入异步任务或消息队列领域实体被 Spring 注解侵入团队对 DDD 约束不一致通过架构测试保障依赖方向评审阶段重点检查 domain 包是否引用了框架Redis 缓存雪崩大量 Key 同一时间过期TTL 增加随机值使用多级缓存热点 Key 设永不过期并手动更新遇到问题时建议按照“入口 Nginx → 应用层 → 缓存 → 数据库 → 外部 AI 服务”的顺序逐层排查先确认请求是否到达后端再确认缓存是否命中最后检查外部依赖的响应时间。9. 工程实践与生产建议9.1 分层约束与代码规范DDD 落地最容易出现的问题是领域层被“偷渡”依赖。例如在实体上写Autowired或者直接注入 RedisTemplate 到领域服务中。这些做法都会破坏领域层的独立性导致后续框架升级时业务代码被迫跟着改动。建议在 CI 中加入架构约束检查。比如使用 ArchUnit 编写测试强制验证domain 包不能依赖 application 包。domain 包不能依赖 infrastructure 包。infrastructure 包可以实现 domain 包接口但不能反向依赖 domain 的业务行为。命名规范同样重要。实体、值对象、仓储接口、应用服务的命名应该从业务语言中提炼而不是数据库表名。比如OrderRepository而不是OrderMapperOrderApplicationService而不是OrderService。9.2 缓存与并发安全缓存设计必须提前考虑容灾。如果 Redis 宕机应用应该可以暂时降级为直连数据库而不是所有请求直接报错。Spring Data Redis 的连接超时时间要短同时捕获异常后做降级。分布式锁在业务中要慎用。锁只应该保护“无法通过乐观锁或业务状态机解决”的临界区不要用锁去掩盖并发设计问题。生产环境推荐 Redisson不要自己基于setnx封装一个看起来能用但缺少看门狗机制的锁。另外缓存必须有监控。建议在 Redis 管理后台或监控系统中关注三个指标缓存命中率、慢查询数量、内存淘汰量。命中率长期低于 50% 时说明缓存设计有问题。9.3 Nginx 安全与性能Nginx 在生产环境应以最小权限运行配置文件的属主和权限要严格控制。修改配置前先备份nginx -t校验通过后再reload。涉及 upstream 变更时先在测试环境模拟流量的切换再操作生产节点。如果后端服务需要灰度发布可以准备两套 upstream一套稳定节点一套灰度节点。通过 Nginx 的split_clients按照一定比例将部分流量导向灰度节点观察日志和监控指标后再逐步扩大比例。注意这种灰度方案只适合无状态接口涉及用户身份粘性的请求要谨慎。9.4 AI 接口接入规范AI 接入是本文的亮点也是最容易出现安全问题的部分。我的建议如下API Key 必须通过环境变量或密钥管理平台注入严禁提交到代码仓库。每次 AI 调用都要设置短超时并且提供降级方案不能让外部模型的抖动影响核心订单查询。对 AI 调用做审计日志记录调用时间、业务单号、模型名称、耗时和返回状态方便追溯。对 AI 返回结果做长度和格式校验防止异常内容直接透传给前端。AI 生成的代码必须经过人工 Review不能直接合入主干。AI 能提高效率但代码质量责任始终在工程师身上。10. 总结与下一步学习方向这篇文章从一个完整的订单查询场景出发把 AI 大模型接入、DDD 分层建模、Redis 缓存设计、Nginx 网关配置四个技术栈串成了一条可运行的链路。你掌握的不应该是几个孤立的配置片段而是“请求从 Nginx 进来之后每一层该做什么”的完整思路。继续深入学习时建议按下面顺序推进把本文示例复制到本地确保 Nginx、Redis、MySQL、Spring Boot 全部连起来跑通。深入学习 DDD 的事件风暴、限界上下文划分在真实业务中尝试识别聚合边界。研究 Redisson 的看门狗机制和 Redis 集群模式下的缓存一致性。阅读 Nginx 官方文档理解location匹配规则、upstream健康检查和动态负载均衡。了解 Spring AI 项目对模型接入的抽象设计更通用的 AI 能力接入方案。最后提醒一句架构不是写出来好看而是要在线上能稳定运行。不要一次性在项目里堆砌全部概念先在一个足够简单的业务模块验证这套组合跑通后再逐步推广。只有在真实流量下验证过你才算真正掌握了这套架构。
返回列表