
在企业级后端架构实战中DDD领域驱动设计、Redis 缓存、Nginx 网关和 AI 能力经常被放在同一个技术栈里出现但很多人只记住了名词没有理清它们各自解决什么问题。DDD 负责管理业务复杂度Redis 解决数据访问速度和并发压力Nginx 处理入口流量和负载均衡AI 能力则是在特定业务节点上提供智能生成、问答或摘要。这篇文章会基于一个订单查询与智能摘要综合项目演示如何用 DDD 把 Redis 和 AI 服务封装成领域能力并让 Nginx 成为统一流量入口。最终你会得到一个可以本地启动、调用、压测和排错的企业级高可用基础架构同时理解每一层背后的取舍。1. 先拆解 DDD、Redis、Nginx 与 AI 在架构里的职责边界1.1 从一条业务请求链路看四个角色的协作先不讨论代码而是看一次完整请求会经过哪些环节。以“用户查询订单详情并生成摘要”为例请求链路可以描述为客户端 - Nginx 网关 - Spring Boot 接口层 - 应用服务 - 领域服务 - 仓储接口 - Redis 缓存 - MySQL 数据库 - AI 领域服务 - 大模型 HTTP 接口Nginx 先接收请求做反向代理、负载均衡、限流决定请求转发到哪个后端实例。Spring Boot 接口层只负责解析 HTTP 参数、封装响应不写业务规则。应用服务负责编排用例先调仓储查订单再调 AI 服务生成摘要。领域服务表达业务规则比如订单状态判断、摘要输入数据的拼装。仓储接口屏蔽数据来源基础设施层具体实现 Redis 缓存和 MySQL 访问。AI 能力在这里也是领域服务而不是一个独立的架构层。这条链路的价值在于每一层都可以独立替换。Redis 换成 Caffeine 或者本地缓存Nginx 换成其他网关AI 服务从 Mock 接口换成真实大模型都不会污染业务代码。1.2 DDD 管理的是业务边界不是缓存工具DDD 的核心不是教你把项目分成 controller、service、dao 三个包而是帮助你找到稳定的业务边界。边界找对了后续修改需求时只会改一个聚合或一个领域服务不会牵连到与业务无关的缓存逻辑。一个常见反例是为了“性能优化”直接在 Controller 里写 RedisTemplate 查询订单缓存。这样做在小项目里很直接但订单缓存规则一旦变复杂比如需要处理缓存穿透、击穿、多级缓存所有写缓存的地方都要跟着改。DDD 的思路是让领域层只依赖仓储接口缓存是否启用、用 Redis 还是 Caffeine全部由基础设施层决定。业务人员看到的是“订单仓储可以拿到订单”至于数据来自缓存还是数据库业务代码不关心。1.3 Redis 与 Nginx 各自解决的是数据侧和流量侧问题Redis 在架构里通常承担三类职责作为缓存加速读多写少的数据访问。作为分布式锁组件避免多个实例同时重建缓存。作为计数器或滑动窗口实现接口级限流、热点统计。Nginx 在架构里承担的是流量接入职责反向代理隐藏后端真实地址。负载均衡把请求分发到多个后端实例。请求限流在入口层拦截突发流量。动静分离静态资源由 Nginx 直接返回动态请求转发给后端。可以用一句话区分Redis 是“数据侧的加速器”Nginx 是“流量侧的闸门”。两者关注点不同但在高并发系统中缺一不可。1.4 AI 在本项目中的位置领域能力而非独立架构层AI 能力刚引入时容易把它包装成一个“AI 中台”或者“AI 网关”导致所有业务都要经过它。实际上在多数企业后端项目里AI 只是某项业务能力的具体实现。以订单摘要为例用户需要一句话看懂订单包含哪些商品、总价多少、当前状态如何。这个需求在 DDD 中很适合建模成OrderSummaryService领域服务输入订单数据输出摘要文本。摘要文本可以由模板生成也可以由大模型生成。大模型版本升级、接口超时、费用控制都是OrderSummaryService内部细节上层调用方不需要感知。这样做的好处是 AI 能力可以平滑降级。大模型接口超时时OrderSummaryService返回模板生成的摘要接口照常工作用户不会看到一个 500 错误。2. 设计基于 DDD 的订单与智能摘要项目结构2.1 需求范围我们最终要完成的最小闭环为了让项目可运行、可验证本文的范围不做过大设计。假设有两个后端服务实例提供以下功能查询订单详情包含订单基础信息、商品明细、客户信息。根据订单数据生成摘要默认走模板配置开启后走大模型接口。所有查询先走 Redis 缓存缓存未命中再查 MySQL。Nginx 作为统一入口将/api/路径请求转发到两个后端实例。订单表结构只需要支持演示客户表、订单表、订单明细表。不引入复杂的支付、库存、消息队列否则主题会被冲淡。2.2 多模块 Maven 工程目录划分DDD 的典型落地方式是分模块让依赖方向始终从外层指向内层。目录结构可以这样设计ddd-cache-gateway ├── interfaces │ └── src/main/java/com/example/interfaces │ └── controller/OrderController.java ├── application │ └── src/main/java/com/example/application │ └── service/OrderAppService.java ├── domain │ └── src/main/java/com/example/domain │ ├── model/order/Order.java │ ├── model/order/OrderItem.java │ ├── model/customer/Customer.java │ ├── repository/OrderRepository.java │ └── service/OrderSummaryService.java ├── infrastructure │ └── src/main/java/com/example/infrastructure │ ├── repository/OrderRepositoryImpl.java │ ├── cache/RedisCacheConfig.java │ └── ai/LlmClient.java └── bootstrap └── src/main/java/com/example/bootstrap/Application.java实际开发中不一定必须拆成多个 Maven 模块但包结构必须遵守依赖规则infrastructure依赖domainapplication依赖domaininterfaces依赖application。禁止domain依赖infrastructure或application。2.3 定义聚合、实体、值对象和领域服务在这个订单查询场景中可以定义以下领域对象领域对象类型说明Order聚合根包含订单 ID、客户 ID、订单状态、商品明细OrderItem实体订单内的商品行有独立 ID但生命周期依赖订单Address值对象收货地址没有独立 ID属于订单属性Customer实体客户信息OrderRepository仓储接口定义订单持久化和缓存读取抽象OrderSummaryService领域服务负责生成订单摘要内部可能使用 AIOrder聚合根的简化代码如下public class Order { private Long id; private Long customerId; private String status; private ListOrderItem items; public Order(Long id, Long customerId, String status, ListOrderItem items) { this.id id; this.customerId customerId; this.status status; this.items items; } public boolean canGenerateSummary() { return items ! null !items.isEmpty(); } // getter 方法与业务方法省略 }领域对象中不出现 RedisTemplate、ObjectMapper、HTTP Client 等基础设施类型。这是 DDD 最容易忽略的一点。2.4 数据库表结构设计为了配合示例设计三张表CREATE TABLE customer ( id BIGINT PRIMARY KEY, name VARCHAR(64) NOT NULL, level VARCHAR(32) ); CREATE TABLE order ( id BIGINT PRIMARY KEY, customer_id BIGINT NOT NULL, status VARCHAR(32) NOT NULL, create_time DATETIME NOT NULL ); CREATE TABLE order_item ( id BIGINT PRIMARY KEY, order_id BIGINT NOT NULL, product_name VARCHAR(128) NOT NULL, quantity INT NOT NULL, price DECIMAL(10,2) NOT NULL, INDEX idx_order_id(order_id) );这里表名order是 MySQL 保留字实际建表时可以加反引号或改成orders。表结构不需要复杂重点是为了演示仓储实现。2.5 领域层、应用层、基础设施层的依赖规则项目里的依赖规则可以总结为domain层不依赖 Spring、Redis、MyBatis 等外部框架。application层组织用例调用领域服务和仓储接口不直接拼 SQL。infrastructure层实现仓储接口负责 Redis 缓存、MySQL 访问、外部 AI API 调用。interfaces层只做 HTTP 请求转换。这样的约束会让项目看起来多一些类和接口但换来的是长周期可维护性。如果项目很小、业务规则一成不变DDD 可能显得重。一旦业务规则复杂、团队规模变大边界清晰的价值就会显现出来。3. 把 Redis 缓存和分布式锁封装进基础设施层3.1 为什么缓存不能散落在业务代码里缓存本身是一个技术实现细节。同样的订单查询可能今天用 Redis明天用 Redis Caffeine 二级缓存后天引入读写分离。如果业务代码里到处是redisTemplate.opsForValue().get(order: id)更换缓存组件时需要改动所有业务方法。正确做法是在domain层定义仓储接口public interface OrderRepository { Order findById(Long id); void save(Order order); }然后在infrastructure层实现这个接口内部先查 Redis再查 MySQL并回填缓存。领域层完全不知道数据曾经经过缓存。3.2 Redis 环境准备与 Spring Boot 依赖配置本地开发可以使用 Docker 启动 Redis命令如下docker run -d --name redis-demo -p 6379:6379 redis:7启动后可以用redis-cli ping验证返回PONG表示正常。Spring Boot 项目需要引入依赖dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-data-redis/artifactId /dependency dependency groupIdorg.redisson/groupId artifactIdredisson-spring-boot-starter/artifactId /dependency依赖版本不写死因为不同 Spring Boot 版本对应关系不同。落地前需要根据实际 Spring Boot 版本确认依赖版本。在application.yml中配置 Redisspring: data: redis: host: localhost port: 6379 timeout: 3s lettuce: pool: max-active: 16 max-idle: 8 min-idle: 4配置项默认值说明spring.data.redis.timeout60s连接超时时间建议设置 2-5 秒lettuce.pool.max-active8最大连接数过大浪费资源过小容易排队lettuce.pool.max-idle8最大空闲连接lettuce.pool.min-idle0最小空闲连接建议在流量波动大的场景设置如果使用 Redis 6需要额外确认是否开启 ACL 用户认证。Redis 云服务通常还有内网地址、白名单、连接池最小空闲等参数生产环境需要单独核查。3.3 实现基于 Redis 的订单仓储缓存下面是一个OrderRepositoryImpl的示例使用 Spring Data Redis 的RedisTemplate保存 JSON 字符串。Component public class OrderRepositoryImpl implements OrderRepository { private static final String CACHE_KEY_PREFIX order:; Autowired private RedisTemplateString, String redisTemplate; Autowired private ObjectMapper objectMapper; Autowired private OrderMapper orderMapper; Override public Order findById(Long id) { String key CACHE_KEY_PREFIX id; String cached redisTemplate.opsForValue().get(key); if (cached ! null) { try { return objectMapper.readValue(cached, Order.class); } catch (JsonProcessingException e) { // 反序列化失败时删除脏缓存避免持续报错 redisTemplate.delete(key); } } Order order orderMapper.selectById(id); if (order ! null) { try { String json objectMapper.writeValueAsString(order); redisTemplate.opsForValue().set(key, json, 600 ThreadLocalRandom.current().nextInt(120), TimeUnit.SECONDS); } catch (JsonProcessingException e) { // 序列化失败只记录日志不阻塞主流程 } } return order; } Override public void save(Order order) { orderMapper.insert(order); String key CACHE_KEY_PREFIX order.getId(); try { redisTemplate.opsForValue().set(key, objectMapper.writeValueAsString(order), 600, TimeUnit.SECONDS); } catch (JsonProcessingException e) { // ignore } } }这段代码有三个关键点缓存 key 带业务前缀order:避免与其他业务缓存冲突。缓存 TTL 做了随机化避免大量 key 同一时间过期造成雪崩。反序列化失败时删除缓存宁可下次查库也不要让脏数据卡住整个查询。3.4 防止缓存穿透、击穿、雪崩的处理Redis 缓存最常见的三个问题是穿透、击穿、雪崩。它们的表现和应对思路如下问题类型现象常见原因处理方式穿透大量请求绕过缓存直接打数据库查询了不存在的 ID缓存空值或使用布隆过滤器击穿热点 key 过期瞬间并发请求打到数据库热门数据缓存过期互斥锁重建缓存或设置逻辑过期雪崩大量 key 同时失效TTL 设置相同过期时间添加随机值使用多级缓存在仓储实现中最简单的穿透处理是缓存空值。当数据库查询结果为 null 时也写入一个短 TTL 的空缓存。不过要注意缓存空值会占用内存建议 TTL 设置为 30-60 秒。击穿处理比较适合使用分布式锁下一节会给出示例。这里先强调不要在领域层加synchronized因为分布式环境下多个服务实例之间的同步锁并不互斥。3.5 使用 Redisson 实现分布式锁当缓存失效且并发请求较高时只允许一个请求去数据库查询其他请求等待缓存重建。使用 Redisson 的RLock可以非常简洁地实现Autowired private RedissonClient redissonClient; public Order getOrderWithLock(Long id) { String key order:lock: id; RLock lock redissonClient.getLock(key); boolean locked false; try { locked lock.tryLock(2, 5, TimeUnit.SECONDS); if (locked) { Order order orderMapper.selectById(id); // 缓存回填省略 return order; } else { // 没有拿到锁的线程可以短暂休眠后再次尝试读缓存 Thread.sleep(50); return findById(id); } } catch (InterruptedException e) { Thread.currentThread().interrupt(); return null; } finally { if (locked lock.isHeldByCurrentThread()) { lock.unlock(); } } }这段代码里有几个必须记住的坑tryLock的第一个参数是等待时间第二个参数是自动释放时间。自动释放时间不要设置成 0否则 Redisson 默认看门狗续期一旦业务方法卡住锁会一直不释放。释放锁前必须判断isHeldByCurrentThread防止 A 线程释放了 B 线程持有的锁。锁的粒度要控制在 key 级别order:lock:1001只锁当前订单不要用一个大锁锁住所有订单。4. 用 Nginx 构建统一入口网关与负载均衡4.1 Nginx 的定位与安装Nginx 在这个项目里不是业务服务它是流量入口。它需要在后端服务前面完成四件事反向代理、负载均衡、限流、动静分离。本地安装 Nginx 的方式因系统而异# Debian/Ubuntu apt-get update apt-get install -y nginx # CentOS/RHEL yum install -y nginx # 使用 Docker docker run -d --name nginx-demo -p 80:80 -v /path/to/nginx.conf:/etc/nginx/nginx.conf:ro nginx:1.27安装完成后检查配置和重载配置是两个最常用的命令nginx -t nginx -s reloadnginx -t会检查配置文件语法如果输出syntax is ok再执行 reload否则不要盲目重载。4.2 配置反向代理与 upstream 负载均衡假设后端有两个服务实例分别运行在 8081 和 8082 端口。Nginx 的upstream可以定义后端服务器组upstream backend { server 127.0.0.1:8081 weight3; server 127.0.0.1:8082 weight1; keepalive 32; } server { listen 80; location /api/ { proxy_pass http://backend; 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 3s; proxy_read_timeout 10s; } }关键配置解释配置项作用weight3权重为 38081 会接收更多流量keepalive 32复用后端 TCP 连接降低握手开销proxy_set_header把客户端真实 IP 和协议透传给后端proxy_connect_timeout连接后端超时建议 2-5 秒proxy_read_timeout等待后端响应超时根据业务接口耗时调整这里要注意如果后端服务需要获取客户端 IP必须配置X-Real-IP和X-Forwarded-For否则所有请求在后端日志里都会显示 Nginx 的 IP。4.3 配置请求限流与连接限制Nginx 限流使用limit_req_zone定义共享内存区域和速率在location中引用limit_req_zone $binary_remote_addr zoneapi_limit:10m rate10r/s; server { listen 80; location /api/ { limit_req zoneapi_limit burst20 nodelay; proxy_pass http://backend; } }rate10r/s表示每秒处理 10 个请求burst20表示允许 20 个请求排队或瞬时通过nodelay表示排队期间不延迟直接放行突发请求。限流会返回 503 给超限请求后端日志不会出现这些请求。连接限制对应的配置是limit_conn_zone和limit_conn可以限制单个 IP 的并发连接数limit_conn_zone $binary_remote_addr zoneconn_limit:10m; server { listen 80; location /api/ { limit_conn conn_limit 10; proxy_pass http://backend; } }限流配置在生产环境需要结合真实业务量调整。rate设置得太低会把正常用户挡在门外设置得过高又达不到保护后端的效果。建议先压测出单机支撑能力再按比例设置 Nginx 限流阈值。4.4 验证 Nginx 转发与负载均衡效果配置完成后用nginx -t检查语法然后重载nginx -t nginx -s reload启动两个后端实例使用相同的 Spring Boot 服务分别监听 8081 和 8082。之后反复调用 Nginx 入口curl http://localhost/api/orders/1001观察两个后端日志可以看到请求按 weight 比例被分发到 8081 和 8082。还可以通过响应头确认来源在后端接口中增加一个自定义响应头response.setHeader(X-Backend-Port, String.valueOf(port));这样 curl 时带上-i参数就能直接从响应头看到请求实际打到哪个后端实例。4.5 生产高可用Nginx 主备与网关层注意事项Nginx 自身不能保证高可用因为单台 Nginx 挂了后续请求就全部断掉。常见做法是在 Nginx 前面再加虚拟 IP通过 Keepalived 实现主备切换或者直接把 Nginx 放在云负载均衡器后面。如果使用 Keepalived核心思路是两个 Nginx 节点共享一个虚拟 IP主节点正常时虚拟 IP 指向主节点主节点异常时虚拟 IP 自动漂移到备节点。这个方案会增加部署复杂度但它解决的是“Nginx 进程所在机器宕机”的问题。对于后端服务的健康检查Nginx 的upstream默认不做主动探测。如果某个后端已经宕机但未被摘除请求转发过去会得到 502。可以在upstream中开启简单检查upstream backend { server 127.0.0.1:8081; server 127.0.0.1:8082; } server { location /api/ { proxy_next_upstream error timeout http_502 http_504; proxy_pass http://backend; } }proxy_next_upstream会在当前后端返回错误或超时时自动尝试下一个后端节点。这是低成本提高可用性的方式但要注意如果业务接口不具备幂等性错误重试可能造成重复提交。在订单查询这种只读接口上可以使用重试在写入接口上要谨慎。5. 把 AI 应答能力封装成领域服务5.1 选择可落地的 AI 应用场景订单摘要AI 在项目里最容易落地的场景不是完全开放的智能问答而是有明确输入和输出格式的文本生成。订单摘要就是一个好例子输入订单商品明细、总价、状态输出一段自然语言摘要。在这篇文章中为了不依赖外部大模型服务可以先实现一个本地 Mock 接口。Mock 接口返回固定格式的 JSON用来验证整个调用链。生产环境替换成真实大模型服务时只需要改infrastructure层的客户端实现领域层逻辑不用动。5.2 使用 Spring RestClient 调用大模型 HTTP 接口Spring Framework 6.1 开始推荐使用RestClient替代RestTemplate。可以定义一个LlmClient负责与大模型服务通信。Component public class LlmClient { private final RestClient restClient; public LlmClient(RestClient.Builder builder) { this.restClient builder .baseUrl(http://localhost:9090/v1) .build(); } public String chat(String prompt) { MapString, Object request new HashMap(); request.put(model, demo-model); request.put(messages, List.of(Map.of(role, user, content, prompt))); Map response restClient.post() .uri(/chat/completions) .body(request) .retrieve() .body(Map.class); if (response null || response.get(choices) null) { throw new IllegalStateException(LLM response invalid); } List? choices (List?) response.get(choices); MapString, Object first (MapString, Object) choices.get(0); MapString, Object message (MapString, Object) first.get(message); return (String) message.get(content); } }这里使用Map是为了减少代码量。实际项目中建议定义请求和响应 DTO并做好字段校验。如果大模型服务是标准 OpenAI 兼容接口这种请求结构可以直接复用如果使用其他厂商 SDK则应使用厂商提供的客户端类。5.3 超时、重试、降级与敏感信息过滤调用外部大模型服务有三个风险延迟高、费用高、结果不稳定。延迟高时需要设置连接超时和读取超时。Spring Boot 中可以通过ClientHttpRequestFactorySettings定制RestClientBean public RestClient.Builder restClientBuilder() { var settings ClientHttpRequestFactorySettings.defaults() .withConnectTimeout(Duration.ofSeconds(3)) .withReadTimeout(Duration.ofSeconds(10)); var requestFactory ClientHttpRequestFactories.get(settings); return RestClient.builder().requestFactory(requestFactory); }超时设置建议连接超时 2-3 秒读取超时 10-15 秒整体接口响应时间控制在 3 秒以内。结果不稳定时要提供降级方案。在OrderSummaryService中先调用 AI 服务如果抛异常就使用模板生成摘要Service public class OrderSummaryService { private final LlmClient llmClient; public String generateSummary(Order order) { if (!order.canGenerateSummary()) { return 订单无可展示商品; } try { String prompt buildPrompt(order); return llmClient.chat(prompt); } catch (Exception e) { return buildFallbackSummary(order); } } }降级逻辑不能让用户感知到“AI 挂了”而是返回一个仍然可读的模板摘要。敏感信息过滤非常关键。订单数据里包含客户姓名、手机号、地址这些信息不应该原样发送给大模型。构建 prompt 时只传订单号、商品名称、数量、金额、状态不传手机号和详细地址。如果某个场景确实需要地址也要先做脱敏处理private String maskAddress(String address) { if (address null || address.length() 6) { return ***; } return address.substring(0, 3) **** address.substring(address.length() - 2); }5.4 缓存 AI 响应降低成本和延迟大模型接口调用成本高、延迟高对重复请求做结果缓存是性价比很高的优化。可以用 prompt 内容的哈希作为缓存 key在 Redis 中保存 AI 响应。public String chatWithCache(String prompt) { String key ai:summary: DigestUtils.md5DigestAsHex(prompt.getBytes(StandardCharsets.UTF_8)); String cached redisTemplate.opsForValue().get(key); if (cached ! null) { return cached; } String result llmClient.chat(prompt); redisTemplate.opsForValue().set(key, result, Duration.ofHours(12)); return result; }这里要注意 prompt 可能很长直接用完整内容做 key 不现实使用 MD5 哈希可以固定长度。但 MD5 不用于安全场景只用于缓存 key碰撞概率在业务数据量下可以接受。如果要求更高可以使用 SHA-256。同时要控制缓存时间。订单状态变化后AI 摘要也应该失效。所以更准确的方案是在订单更新时删除或者更新ai:summary:{orderId}对应的缓存而不是设置固定 12 小时。真实项目中建议把订单状态变化作为缓存失效的触发条件。6. 串起完整接口从 HTTP 入口到 Redis、MySQL、AI 服务6.1 最终项目依赖与启动顺序一个完整的演示环境需要按以下顺序启动组件1. MySQL 2. Redis 3. 后端服务实例 18081 4. 后端服务实例 28082 5. Nginx80后端服务可以使用 Maven 启动mvn spring-boot:run -Dspring-boot.run.arguments--server.port8081 mvn spring-boot:run -Dspring-boot.run.arguments--server.port8082如果使用打包方式先执行mvn package然后分别启动两个 jarjava -jar app.jar --server.port8081 java -jar app.jar --server.port8082两个实例使用同一个数据库和 Redis。生产环境两个实例应该部署在不同机器或容器并接入注册中心演示环境本地启动两个端口即可。6.2 编写 Controller、ApplicationService 与 Repository接口层比较简单只做参数接收和结果返回RestController RequestMapping(/api/orders) public class OrderController { Autowired private OrderAppService orderAppService; GetMapping(/{id}) public MapString, Object getOrder(PathVariable Long id, RequestParam(required false, defaultValue false) boolean summary) { return orderAppService.getOrderDetail(id, summary); } }应用服务负责组装用例先查订单再按配置决定是否生成摘要。Service public class OrderAppService { Autowired private OrderRepository orderRepository; Autowired private OrderSummaryService orderSummaryService; public MapString, Object getOrderDetail(Long id, boolean needSummary) { Order order orderRepository.findById(id); if (order null) { throw new NotFoundException(订单不存在); } MapString, Object result new LinkedHashMap(); result.put(orderId, order.getId()); result.put(status, order.getStatus()); result.put(items, order.getItems()); if (needSummary) { result.put(summary, orderSummaryService.generateSummary(order)); } return result; } }这里要注意OrderAppService不关心订单数据来自 Redis 还是 MySQL也不关心摘要来自 AI 还是模板。它只负责“编排”。6.3 运行验证请求、缓存、AI 摘要的一次完整调用启动所有组件后执行下面的请求curl -i http://localhost/api/orders/1001?summarytrue第一次请求时Redis 中没有缓存仓储会查数据库然后回填 Redis。AI 领域服务会调用大模型接口并把结果缓存到 Redis。预期响应类似{ orderId: 1001, status: PAID, items: [ { productName: 无线键盘, quantity: 1, price: 199.00 } ], summary: 该订单包含 1 件无线键盘总价 199 元当前已支付。 }再次执行同样的请求后端日志不应该出现 SQL 查询日志说明缓存已经命中。如果要验证 AI 响应缓存可以用redis-cli查看 keyredis-cli keys ai:summary:* redis-cli keys order:*可以看到分别存在 AI 摘要缓存和订单缓存。6.4 压测与故障模拟高可用能力验证使用wrk对 Nginx 入口做简单压测wrk -t4 -c50 -d10s http://localhost/api/orders/1001?summaryfalse观察两个后端实例的请求分布可以看出 Nginx 是否按照 weight 比例分配流量。也可以观察 Redis 缓存命中率缓存命中后数据库压力会明显下降。故障模拟方面可以做三个实验停止 Redis 服务。此时订单仓储如果配置了降级查询应该直接走数据库如果没配置降级接口会 500。停止其中一个后端服务。Nginx 配合proxy_next_upstream请求会自动转发到另一个实例。使用ab工具突发大量请求触发 Nginx 的limit_req超限请求会返回 503。每个实验都应该配合日志确认问题定位。压测时不要只记录平均延迟还要关注 P99 延迟和错误率这两个指标更能反映系统稳定性。7. 常见问题排查与上线前检查清单7.1 Redis 相关问题定位问题现象可能原因检查方式处理建议接口报连接超时Redis 服务未启动或网络不通redis-cli ping检查 Redis 进程、端口、防火墙缓存写入后读出乱码使用了 JDK 序列化查看 Redis 中的 key 内容配置 JSON 序列化或使用 StringRedisTemplate重启后缓存全部丢失Redis 未开启持久化查看 Redis 日志生产开启 AOF 或 RDB根据业务要求配置高并发时连接池耗尽max-active 过小或查询过慢查看连接池监控调大连接池优化慢查询缩短超时时间Redis 缓存最常见的错误是使用 RedisTemplate 默认序列化方式存取对象。默认 JDK 序列化生成的二进制数据在redis-cli里不可读而且占用空间大。建议显式配置StringRedisSerializer和GenericJackson2JsonRedisSerializer。7.2 Nginx 502、504 排查502 Bad GatewayNginx 无法连接后端服务。先检查后端进程是否存活再检查 8081、8082 端口是否监听最后看upstream配置是否有拼写错误。504 Gateway TimeoutNginx 等待后端响应超时。检查后端接口执行时间调大proxy_read_timeout同时排查后端是不是有慢 SQL 或外部依赖超时。502 出现但后端日志无记录请求可能在连接阶段就失败说明后端连接数满或线程池耗尽。需要查看后端访问日志和线程栈。常见故障是修改了 Nginx 配置但没有重载或者nginx -t验证失败。切换到备份配置前先执行nginx -t确认没问题再 reload。7.3 DDD 分层落地时的过度设计问题DDD 不是所有项目的银弹。小项目强行按 DDD 拆分会增加大量 Value Object、Repository 接口、ApplicationService开发效率反而下降。常见过度设计表现每一个实体都写一个仓储接口哪怕只是简单字典表。应用服务只做一行代码转发却单独建类。领域服务命名抽象实际没有任何业务规则。推荐做法先从订单、商品这类核心业务边界开始落地 DDD简单配置查询可以用轻量服务。聚合不要设计得过大如果一个聚合包含十几个实体后续事务和并发控制会非常困难。7.4 上线前检查清单检查项说明配置外置数据库地址、Redis 地址、大模型 API Key 不要写死在代码中日志规范Redis 缓存未命中、AI 降级都要有日志方便排查缓存时间确认哪些 key 需要缓存TTL 是否设置随机值限流阈值根据压测结果调整 Nginx 限流阈值不要拍脑袋敏感信息大模型 prompt 中不能包含手机号、地址等隐私数据分布式锁Redisson 锁的自动释放时间是否合理unlock 是否安全健康检查后端服务是否提供/actuator/healthNginx 是否配置健康检查回滚方案如果新版本上线异常能否快速切回上一版本监控告警Redis 内存、后端 P99 延迟、Nginx 5xx 比例是否接入监控这张清单在项目上线前逐项打钩可以避免很多低级但影响较大的故障。特别是 AI 相关服务如果大模型接口依赖外部网络一定要提前确认密钥、配额、降级逻辑否则线上一个小抖动会直接拖垮查询接口。实际落地这套架构时建议不要一次性把 DDD、Redis、Nginx、AI 全部叠加。先跑通一条最简单的查询链路再逐步加入缓存、网关、AI 摘要和分布式锁。技术栈越复杂验证成本越高只有每一步都有明确的可观测入口后续优化和排错才不会失控。