DDD+Redis+Nginx:企业级.NET架构落地实战指南
先说一个我最近遇到的真实场景。一个做 .NET 业务中台的团队找我聊架构改造他们的系统已经跑了三年多功能都能用但每次加需求都像拆地雷一到高峰期数据库 CPU 就直接报警。技术负责人提了一个方向用 DDD 把核心领域重新建模前面挂 Redis 做缓存再在网关层用 Nginx 做负载均衡。听起来很完整但团队成员普遍困惑这三件事我单独都听过可到底怎么串成一套能落地的架构这个问题其实非常典型。很多 .NET 开发者的现状是DDD 知道一些概念Redis 会用基础命令Nginx 也配过反向代理但真要在一个企业级项目里让它们协同工作就不知道从哪里下手了。更麻烦的是网上教程大多只讲单点要么纯讲 DDD 怎么分层要么纯讲 Redis 缓存穿透要么纯讲 Nginx 配置很少有一篇文章把链路打通。这篇文章不打算科普概念而是沿着一条真实落地的路径走一遍先看 AI 怎么辅助 DDD 领域建模再看 Redis 在企业级架构里的缓存职责最后看 Nginx 如何承接流量。我的核心判断只有一句话DDD、Redis、Nginx 不是三个孤立的技术加分项它们分别解决企业级系统的三个根本问题——业务复杂度、数据访问热度和流量容量。只有当这三层各司其职、接口清晰时整套架构才算真正落地。1. 一个真实问题系统能跑但改不动也扛不住1.1 很多 .NET 团队都会走到这一步我接触过不少从单体应用一路演进过来的 .NET 项目它们通常有相似的经历第一版为了快速交付Controller 里直接写业务逻辑Service 层又厚又重所有表都通过一个通用 Repository 暴露出来。系统上线初期没问题但业务量起来之后两个问题会同时爆发。第一个问题是改不动。一个订单状态的变化可能散落在七个 Service 方法里一个看似简单的需求调整需要先搞清楚十几个类之间的调用关系。团队不敢重构因为没有人能说清楚一个改动的影响面。第二个问题是扛不住。热点数据被反复查库同样的用户信息在十个请求里被查了十次数据库连接池被打满后整个系统的响应时间就像坐过山车。这时候如果有人提我们要引入 DDD、Redis、Nginx很多人的第一反应是这是三个不同层面的东西为什么要放在一起讲1.2 为什么是 DDD、Redis、Nginx 这三件套因为它们解决的不是同一个问题而是三个挨在一起的问题。DDD 解决的是业务复杂度。系统之所以改不动本质是业务规则的边界没有划清楚。DDD 的价值不是把代码分成几层而是让你先想清楚哪些是核心业务规则哪些是支撑逻辑哪些可以独立演进。Redis 解决的是数据访问热度。系统之所以扛不住很多时候不是数据库容量不够而是大量重复查询把资源浪费在了不该浪费的地方。Redis 作为缓存层能把热点数据的访问从数据库层面卸载掉。Nginx 解决的是流量容量。单实例应用无论优化得多好都有物理上限。Nginx 在前面做负载均衡让请求均匀地分散到多个 .NET 实例上系统才有横向扩展的能力。这三者不是竞争关系而是前后衔接的层次关系。业务模型负责把系统内部梳理清楚缓存负责让数据访问变快负载均衡负责让整个集群能顶住流量。每一层都有自己的职责边界独立演进又通过接口协同。理解了这一点就不会再把这三项技术当成三个教程来学而是会带着它们在我的系统里分别承担什么角色这个问题往下走。2. AI 辅助 DDD 建模不要让 AI 替你思考而是让它帮你起稿2.1 DDD 建模的本质是划边界很多初学者把 DDD 理解为Controller、Service、Repository 分三层这是最大的误解。分层只是脚手架DDD 的核心是领域建模也就是把业务规则转化为代码结构的过程。一个真正的领域模型要回答几个问题核心业务实体有哪些哪些对象是拥有独立生命周期的聚合根哪些对象只是属性组合的值对象业务状态迁移的规则是什么边界在哪里这些问题如果答不上来代码分层分得再漂亮也只是换了一种方式写贫血模型。传统做法里领域建模通常靠领域专家和架构师开工作坊用事件风暴一类的方式梳理业务流程。这个过程中最耗时间的不是画图而是把模糊的业务描述变成精确的模型边界。而这一步恰好是 AI 能发挥作用的场景。2.2 用 AI 生成领域模型的正确流程AI 的优势在于它读过大量领域建模的案例能够快速生成一份结构完整、覆盖面广的领域模型草案。问题在于很多人直接把 AI 生成的答案当成最终方案这是本末倒置。正确姿势是把 AI 当成一个快速起稿的助手而不是做决策的架构师。我一般会按这个流程走交代业务背景。把系统要解决的业务问题、参与角色、核心流程、关键规则用自然语言描述清楚越具体越好。AI 的输出质量高度依赖你输入的上下文质量。让 AI 识别限界上下文。先不要急着让它写代码而是让它把整个业务划分为若干个限界上下文比如订单、支付、库存、用户、营销。让 AI 提取实体、值对象和聚合根。对每个上下文列出候选的实体、值对象、聚合根并给出理由。定义领域事件和业务规则。比如订单已创建支付已完成库存不足以及状态迁移的约束。人工评审并交给领域专家确认。这是最关键的一步AI 生成的是草案最终判断必须由懂业务的人来做。举个例子一段简化版的提示词可以长这样我正在为一个 B2C 电商系统做 DDD 领域建模。 核心流程包括用户浏览商品、下单、支付、库存扣减、订单发货。 订单不能同时支付两次库存不足时不允许下单支付后自动触发出库单。 请帮我在订单限界上下文里识别实体、值对象、聚合根并说明实体间的关联关系。这样得到的输出通常比你自己从零开始写要全面得多。2.3 一个订单系统的建模示例假设 AI 给出了这样一份订单上下文的草案聚合根Order实体OrderItem、Payment、Shipment值对象CustomerInfo、Address、Money领域事件OrderCreated、PaymentConfirmed、OrderShipped这个结构基本合理。但需要人工确认的点在于Customer 到底是独立的聚合根还是 Order 里的值对象如果客户信息在订单里只需要快照式保存那一份 CustomerInfo 值对象就够了如果订单需要实时关联客户状态那就需要独立的 Customer 聚合根和仓储。这类决策AI 无法替你真正判断需要结合业务对一致性的要求来决定。代码层面聚合根的核心职责是维护自己的业务规则。一个简化示例public class Order : AggregateRoot { public Guid Id { get; private set; } public string OrderNo { get; private set; } public CustomerInfo Customer { get; private set; } private ListOrderItem _items new(); public IReadOnlyCollectionOrderItem Items _items; public PaymentStatus PaymentStatus { get; private set; } public void AddItem(ProductInfo product, int quantity) { if (product null) throw new DomainException(商品信息不能为空); if (quantity 0) throw new DomainException(数量必须大于 0); _items.Add(new OrderItem(product.Id, product.Price, quantity)); } public void ConfirmPaid() { if (PaymentStatus ! PaymentStatus.Pending) throw new DomainException(当前订单状态不允许支付确认); PaymentStatus PaymentStatus.Paid; AddDomainEvent(new PaymentConfirmed(Id)); } }注意看业务规则不能重复支付被封装在聚合根内部而不是散落在 Application Service 里。这就是 DDD 真正有价值的地方规则跟着模型走而不是跟着流程走。2.4 AI 辅助建模的三个边界第一AI 不了解你的真实业务约束。比如财务合规要求、发票开具规则、退款时效这些通常是公司内部的硬性约束外部 AI 不可能知道。它生成的模型再漂亮也只能作为骨架。第二AI 容易生成看起来合理但不完整的模型。它倾向于提供一个结构工整、术语正确的泛化答案但你的业务可能有一些特殊分支比如预售订单赠品订单售后改价这些特殊场景如果不在提示词里显式说明AI 就会忽略。第三AI 不能替代领域专家评审。我的建议是用 AI 生成初稿之后一定要组织一次小范围的业务评审会把聚合根、业务规则、状态迁移图一条条过一遍。宁可在这个阶段多花两天也不要等代码写了一万行再回来改模型。3. Redis 高性能缓存加一个中间件不等于缓存就做好3.1 先确认 Redis 在你的架构里承担什么角色很多团队引入 Redis 之后做的第一件事就是把所有查询结果都塞进去然后发现缓存命中率不高、数据不一致、内存涨得飞快。问题不是 Redis 不好而是没有想清楚它在架构里的角色。在企业级 .NET 系统里Redis 常见角色有四个缓存层缓存热点查询结果减少数据库压力这是最常见也最容易做错的角色。分布式会话存储多实例部署时Session 不再存在单机内存里而是统一放到 Redis。分布式锁解决多实例之间的资源竞争。轻量级消息或队列利用 List、Stream 结构做简单的任务队列。不同角色对应不同的数据类型和用法不要指望一套代码通吃。3.2 .NET 接入 Redis 的最小链路在 ASP.NET Core 里官方推荐的方式是基于IDistributedCache抽象来做缓存它在Microsoft.Extensions.Caching.StackExchangeRedis包里提供了 Redis 实现。好处是业务代码只依赖抽象接口未来如果替换缓存中间件业务代码不需要大改。一个常见的最小配置{ Redis: { Connection: localhost:6379,passwordyourpass,defaultDatabase0,abortConnectfalse } }builder.Services.AddStackExchangeRedisCache(options { options.Configuration builder.Configuration[Redis:Connection]; options.InstanceName appname:; });这里几个参数值得解释一下password生产环境必填Redis 默认是不需要密码的但这个配置通常应该在云环境或自建环境中开启。defaultDatabase用不同的 database 隔离不同业务模块但生产环境更推荐用 key 前缀隔离因为以后做集群迁移时多 database 会有很多限制。abortConnectfalse应用启动时即使 Redis 暂时不可用也不要崩溃连接池会自动重连。这对提升可用性很重要。3.3 缓存策略什么东西值得缓存不是所有数据都值得进缓存。判断标准有三个读多写少。数据的热度比较集中比如前 20% 的商品贡献了 80% 的流量。对实时性要求不苛刻允许秒级甚至分钟级的延迟。最常见的缓存模式是 Cache-Aside也叫旁路缓存。核心逻辑就是读的时候先查缓存缓存没有就查数据库查到后回填缓存写的时候先更新数据库再删除缓存。这个模式的优点是实现简单缺点是首次查询会有缓存穿透窗口需要靠合理的初始化和预热来解决。一段简化示例public async TaskOrderDto GetOrderAsync(Guid orderId) { string key $order:v1:{orderId}; var cached await _cache.GetStringAsync(key); if (!string.IsNullOrEmpty(cached)) return JsonSerializer.DeserializeOrderDto(cached); var order await _orderRepository.GetByIdAsync(orderId); if (order null) return null; var dto OrderDto.FromEntity(order); await _cache.SetStringAsync(key, JsonSerializer.Serialize(dto), new DistributedCacheEntryOptions { AbsoluteExpirationRelativeToNow TimeSpan.FromMinutes(10) }); return dto; }注意 key 的设计order:v1:{orderId}其中v1是版本号。一旦 DTO 结构升级直接改版本号就能让所有旧缓存自然失效不需要手动清理这是一个很容易被忽略但非常有用的习惯。3.4 缓存穿透、击穿、雪崩最常见也最容易被低估这三个问题是缓存系统里几乎必考的考点但在真实项目里往往不是一次出现而是组合出现。缓存穿透是指查询一个不存在的数据每次都会直接打到数据库。解决方案有两个一是缓存空值给不存在的 key 也设置一个很短的 TTL二是用布隆过滤器在缓存之前做一层判断但布隆过滤器有误判率实现复杂度相对高。我更建议先做空值缓存简单直接。缓存击穿是指某个热点 key 在同一瞬间过期大量请求同时打到数据库。解决方案通常是用锁来保证只有一个请求去重建缓存其他请求等待。实现方式有分布式锁也可以用一个进程内的锁来降低复杂度。更激进的做法是热点 key 不设过期时间后台任务定期刷新但这只适合确定的热点。缓存雪崩是指大量 key 集中在同一时间过期导致数据库瞬间压力飙升。最常见的方案是给 TTL 加一个随机值比如原本 10 分钟过期的缓存实际设置在 8 到 12 分钟之间随机分布。这个操作成本极低但能把过期请求打散强烈建议默认就做。3.5 Redis 分布式锁能用但别当成万能事务在 .NET 多实例部署下如果一个资源需要被多个进程互斥操作就需要分布式锁。StackExchange.Redis 提供了原生支持string token Guid.NewGuid().ToString(); bool locked _redis.LockTake(lock:inventory:order, token, TimeSpan.FromSeconds(10)); try { if (!locked) return; // 获取锁失败重试或直接返回 // 执行库存扣减等互斥业务逻辑 } finally { _redis.LockRelease(lock:inventory:order, token); }这里要特别注意 token 的作用只有持有正确 token 的客户端才能释放锁避免误删别人刚获取的锁。这是很多自研锁最容易出错的地方。但分布式锁不是银弹。它解决的是互斥问题不是事务问题。如果锁内部的操作失败需要回滚Redis 锁本身帮不了你如果锁的过期时间设得太短业务还没执行完锁就自动释放了另一个请求就会进来如果设得太长一旦持有者宕机其他请求就要等很久。这些都是需要结合业务场景权衡的。我见过不少团队因为用了 Redis 锁就觉得高并发安全了结果库存超卖照样发生原因就是锁的过期时间没有考虑到业务方法的实际执行时间。正确做法是锁的过期时间要大于业务操作的最大耗时并且业务里最好有兜底校验不能把安全完全寄托在一把锁上。4. Nginx 负载均衡让流量均匀地打到集群上4.1 Nginx 在企业级部署里解决什么当 .NET 应用从单实例变成多实例之后第一个要解决的问题就是用户请求应该打到哪一台机器。Nginx 最常见的职责就是反向代理和负载均衡。它做的事情很简单维护一个后端服务器列表按照某种策略把进来的请求分发给不同的 .NET 实例。默认方式有轮询、加权轮询、最小连接数、IP Hash 等。更重要的Nginx 在高并发场景下本身很轻量可以集中处理 TLS 终止、静态文件、压缩、超时控制这些边缘工作让后端的 ASP.NET Core 专注于业务逻辑。4.2 最小负载均衡配置拆解一个典型的配置长这样upstream dotnet_backend { least_conn; server 10.0.0.11:8080 max_fails2 fail_timeout30s; server 10.0.0.12:8080 max_fails2 fail_timeout30s; keepalive 32; } server { listen 80; server_name api.example.com; client_max_body_size 20m; gzip on; location / { proxy_pass http://dotnet_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 5s; proxy_read_timeout 30s; } }几个关键点least_conn把请求发给当前连接数最少的后端适合后端请求处理时间差异较大的场景。如果所有接口响应都很快默认的轮询就够了。max_fails和fail_timeoutNginx 会在这段时间内如果发现某台后端连接失败超过次数就把它临时摘除这是最基础的被动健康检查。keepaliveNginx 和后端保持长连接避免每个请求都重新建立 TCP 连接这个对性能影响很明显。X-Forwarded-*必须把这些头传给后端否则 ASP.NET Core 获取不到客户端真实 IP 和协议类型。4.3 反向代理之外的几件小事很多人在配置了proxy_pass之后就觉得完事了其实还有几个容易被忽略的点。第一ASP.NET Core 需要处理转发头。默认情况下HttpContext.Connection.RemoteIpAddress拿到的是 Nginx 服务器的 IP而不是用户 IP。需要在 Program.cs 里启用转发头中间件app.UseForwardedHeaders(new ForwardedHeadersOptions { ForwardedHeaders ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto });第二健康检查要单独开一个端点。Nginx 的被动健康检查只能发现请求失败如果后端进程假死、但 TCP 端口还通着Nginx 是感知不到的。更可靠的做法是在 .NET 里加一个/health端点用AddHealthChecks()注册然后用脚本定期探活。这样 Nginx 或者云平台的负载均衡器才能及时移除不健康的实例。第三WebSocket 场景需要额外配置升级头。如果 .NET 应用里有 SignalR 或 WebSocket 服务Nginx 需要设置Upgrade和Connection头否则连接建立不起来。4.4 上生产前必须检查的配置项我建议在部署前把下面这几项都过一遍每一项都可能在线上出问题worker_processes一般设为 CPU 核心数即可。keepaliveNginx 到后端的连接复用是否开启。client_max_body_size如果应用支持上传文件这个不调大大文件会直接被 Nginx 拒掉。gzip对文本类响应开启压缩收益非常明显。proxy_read_timeout如果后端有慢接口这个超时时间不能设得太短否则正常的慢查询会被误杀。proxy_cache_path对不常变化的接口响应做缓存但要注意缓存的 key 设计避免把不同用户的私有数据串了。这些配置看起来都是小点但每一个都可能是线上故障的根源。5. 把这套架构串起来看一次完整请求5.1 系统分层总览当 DDD、Redis、Nginx 组合在一起时一套完整的企业级架构通常长这样层次组件主要职责接入层Nginx负载均衡、TLS 终止、静态资源、反向代理表现层ASP.NET Core API接收请求、参数校验、返回响应应用层Application Service协调用例、事务边界、调用领域服务领域层Domain Model聚合根、实体、值对象、领域事件、业务规则基础设施层Repository、Redis、EF Core数据持久化、缓存读写、外部服务调用很多团队对分层的理解是把代码文件放到不同文件夹但真正的分层价值在于依赖方向上层可以依赖下层下层不能反向依赖上层。领域层不引用 ASP.NET Core 的任何东西这是底线。5.2 从 Nginx 到 Redis 再到数据库的完整链路一次典型的查询请求在整套架构里的路径是这样的用户请求先打到 NginxNginx 根据负载均衡策略选择一台后端实例。ASP.NET Core 中间件处理转发头然后路由到对应 Controller。Controller 调用 Application Service不直接操作仓储。Application Service 先尝试从 Redis 缓存读取数据。缓存命中直接返回数据库零压力。缓存未命中Application Service 调用领域层的聚合根或仓储接口。仓储实现访问数据库查询结果回填 Redis并设置合理 TTL。响应逐层返回最终由 Nginx 返回给客户端。如果是写请求路径会有些不同Application Service 调用聚合根的业务方法领域事件被收集事务提交后相关缓存被删除或更新。这里的关键是先更新数据库再删除缓存顺序反了就会出现脏数据。5.3 常见故障排查顺序我在排查这类架构的问题时习惯按照从外层到内层的顺序逐层确认而不是一上来就看代码先看响应状态。是 502、504、500还是 200 但速度慢不同状态对应完全不同的排查方向。502 和 504 优先查 Nginx。502 Bad Gateway通常说明后端不可达先看 Nginx 配置的 upstream 和后端实例的端口是否正常504 Gateway Timeout说明后端处理超时优先查后端线程池、慢 SQL、外部依赖。请求超时或缓慢查 Redis 命中率。如果缓存命中率极低说明缓存策略有问题大量请求压到了数据库。数据不一致查缓存更新顺序。先确认是先删缓存再更新数据库还是先更新数据库再删缓存以及 TTL 设置的是否合理。登录态丢失查 Session 存储位置。如果 Session 放在单机内存多实例部署后就会出现用户请求落到不同机器导致登录态丢失需要把 Session 迁移到 Redis。这个排查链路不是万能的但它能帮你在故障发生时先缩小范围而不是在代码里瞎翻。6. 哪些人适合这套架构哪些人先别急着上6.1 适合这套架构的信号下面这几个信号如果同时出现说明这套架构值得投入业务规则复杂状态多、分支多代码里到处是if/else改一处漏三处。数据访问有明显的热点同一批数据在高峰期被反复查询。单实例已经扛不住流量或者需要多机房、多节点部署。团队有耐心在建模阶段投入时间而不是一上来就想写代码。这套架构的核心收益不是性能提升多少倍而是让系统在业务复杂和流量上涨的双重压力下仍然可以理解、可以演进、可以扩展。6.2 不适合的场景反过来如果只是做一个内部管理后台、一个原型验证、一个简单 CRUD 业务业务规则不复杂并发量也很低那这套架构大概率是过度设计。DDD 的建模成本、Redis 的运维成本、Nginx 的部署成本都会变成团队的负担。还有一些团队虽然规模不小但领域建模的条件不成熟比如没有真正的领域专家参与业务规则掌握在老员工脑子里但没人写下来。这种情况下强行上 DDD只会得到一个看起来分层很漂亮、实际业务规则还是一团浆糊的系统。可以先从澄清业务规则开始而不是从引入 DDD 开始。6.3 沉淀下来的一套执行框架如果决定要落地我建议按照下面这个顺序推进第一步先建模不碰缓存不碰 Nginx。集中精力把核心限界上下文和聚合根设计清楚用 AI 辅助快速起稿人工评审确认边界。这一阶段的目标是让业务规则在一个清晰的结构里被表达出来。第二步再分层把依赖关系理清。按照领域层、应用层、基础设施层、表现层组织代码确保依赖方向单向。先把一个核心用例跑通比如创建订单。第三步后加速给热点数据加 Redis。有了清晰的仓储接口之后缓存是自然而然的增强。先只缓存读多写少、热度集中的数据设置合理 TTL观察命中率再扩大范围。第四步再扩容用 Nginx 承接多实例。当系统需要多实例部署时再引入 Nginx。配置最小负载均衡、转发头、健康检查把单实例变成集群。这个框架的核心思路是先让系统变得能理解再让它变得更快最后让它变得能扩展。顺序反了就容易在混乱的业务模型上叠缓存、叠负载均衡最后变成一套更复杂的乱摊子。最后说一点个人体会。企业级架构的难点从来不是技术选型而是知道你的系统现在处于什么阶段、需要哪一层能力。DDD、Redis、Nginx 都是被验证过无数次的成熟工具但它们是否适合你取决于你的业务复杂度、流量规模和团队状态。先把最小闭环跑通再逐步叠加复杂度这个原则放在任何架构改造里都成立。