Skip to content
Go back

什么是单体架构?三种类型用 C# 讲清楚

“单体”这个词在架构讨论里经常出现,但用法常常互相矛盾且模糊。先把定义说清楚:单体是一个可部署单元,包含所有应用逻辑——一份代码库、一个部署产物、一个运行进程。其他一切——好的、坏的、微妙的——都由此派生。

单体的精确定义

拆开来看,单体有三个核心特征:

最后一点是区分单体和分布式系统的决定性特征。当你的 OrderService 通过方法调用而非 HTTP 请求调用 InventoryService 时,你在单体领域。调用是同步的、快速的、发生在同一内存空间内。没有序列化、没有网络延迟、没有部分失败场景。

单体的定义本身不涉及质量。单体可以结构优雅,也可以是一团乱麻——架构类型不决定这个,你的设计决定决定它。

三种单体类型

类型一:单进程单体

这是大多数开发者听到”单体”时脑子里浮现的画面。经典分层架构:Controller 调 Service,Service 调 Repository,Repository 调数据库。一切在一个进程里,层之间直接调用。

// Controller 层
[ApiController]
[Route("api/customers")]
public sealed class CustomerController : ControllerBase
{
    private readonly ICustomerService _customerService;

    public CustomerController(ICustomerService customerService)
    {
        _customerService = customerService;
    }

    [HttpGet("{id}")]
    public async Task<IActionResult> GetById(int id)
    {
        var customer = await _customerService.GetByIdAsync(id);
        return customer is null ? NotFound() : Ok(customer);
    }
}

// Service 层
public sealed class CustomerService : ICustomerService
{
    private readonly ICustomerRepository _repository;

    public CustomerService(ICustomerRepository repository)
    {
        _repository = repository;
    }

    public async Task<Customer?> GetByIdAsync(int id) =>
        await _repository.GetByIdAsync(id);
}

// Repository 层
public sealed class CustomerRepository : ICustomerRepository
{
    private readonly AppDbContext _dbContext;

    public CustomerRepository(AppDbContext dbContext)
    {
        _dbContext = dbContext;
    }

    public async Task<Customer?> GetByIdAsync(int id) =>
        await _dbContext.Customers.FindAsync(id);
}

这种模式在中小规模下工作良好。层是逻辑分离的、接口可测试、代码易导航。问题是随着应用增长,Service 之间开始互相依赖、共享工具类被到处引用、分层模型变成横切关注点的迷宫。

类型二:模块化单体

模块化单体仍然是单体——一个进程、一次部署——但它在功能模块之间强制执行硬边界。每个模块是一个自包含单元,有自己的公开 API 和内部实现。其他模块只通过公开 API 交互,绝不触碰内部实现。

在 .NET 中通常实现为每个模块一个类库项目,全部被 Web 宿主消费。关键执行机制是项目引用:宿主引用模块程序集,但模块之间不直接引用。

// Customers 模块——唯一公开入口
public static class CustomerModule
{
    public static IServiceCollection AddCustomersModule(
        this IServiceCollection services,
        IConfiguration configuration)
    {
        services.AddScoped<ICustomerService, CustomerService>();
        services.AddScoped<ICustomerRepository, CustomerRepository>();
        services.AddDbContext<CustomerDbContext>(options =>
            options.UseSqlServer(
                configuration.GetConnectionString("Customers")));
        return services;
    }
}

// Orders 模块
public static class OrderModule
{
    public static IServiceCollection AddOrdersModule(
        this IServiceCollection services,
        IConfiguration configuration)
    {
        services.AddScoped<IOrderService, OrderService>();
        services.AddScoped<IOrderRepository, OrderRepository>();
        services.AddDbContext<OrderDbContext>(options =>
            options.UseSqlServer(
                configuration.GetConnectionString("Orders")));
        return services;
    }
}

// Program.cs——宿主组合模块
var builder = WebApplication.CreateBuilder(args);
builder.Services
    .AddCustomersModule(builder.Configuration)
    .AddOrdersModule(builder.Configuration);
var app = builder.Build();
app.Run();

每个模块声明它暴露了什么,宿主不知道模块的内部类型——只知道公开服务接口。模块化单体的最大实际好处是:如果需要迁移到微服务,每个模块已经是候选服务——有自己的数据上下文、服务边界和公开 API。提取模块只需要单独部署,不需要解依赖关系。

类型三:分布式单体(反模式)

分布式单体是两头不靠的反模式:分布式系统的运维复杂度 + 单体的紧耦合。有网络延迟、部署协调头疼、部分失败场景——但仍然不能独立部署组件,因为它们通过共享数据或代码耦合在一起。

// Shared.Infrastructure——两个服务都依赖这个共享 DbContext
public class SharedAppDbContext : DbContext
{
    public DbSet<Customer> Customers { get; set; } = null!;
    public DbSet<Order> Orders { get; set; } = null!;
    public DbSet<OrderLine> OrderLines { get; set; } = null!;
}

// "独立服务" #1:OrderService——部署为独立容器
public sealed class OrderService
{
    private readonly SharedAppDbContext _dbContext;

    public OrderService(SharedAppDbContext dbContext)
    {
        _dbContext = dbContext;
    }

    public async Task<Order?> GetOrderWithCustomerAsync(int orderId)
    {
        return await _dbContext.Orders
            .Include(o => o.Customer)
            .FirstOrDefaultAsync(o => o.Id == orderId);
    }
}

问题明显:两个”独立服务”共享 SharedAppDbContext,后者同时拥有 CustomersOrders 表。改一个服务的 schema 需要更新共享库并同时重新部署两个服务。你在服务之间加了网络跳转,却没有消除使独立部署成为不可能的那个耦合。

分布式单体的明显信号:服务共享数据库 schema、共享库包含实体类型和 DTO、部署一个服务要求同时部署另一个、集成测试需要同时启动多个服务才能测一个功能。

三种类型对比

维度单进程单体模块化单体分布式单体
部署单个产物,简单单个产物,简单多个产物,复杂协调
耦合中等——层之间自由调用低——强制模块边界高——跨网络共享数据和代码
团队规模受限——共享代码冲突良好——团队拥有模块差——每次变更都要协调
调试简单——单进程单日志简单——单进程结构化模块困难——需要分布式追踪
迁移路径提取前需要解耦工作模块已准备好提取迁移前必须先修耦合
运维复杂度

关键结论:模块化单体几乎总是比单进程单体更好的中间形态,而分布式单体必须避免。如果你要背负分布式服务的运维成本,你需要真正的服务独立性来证明它值得。

现代 .NET 中的单体

ASP.NET Core 是良好结构化单体的坚实基础。框架不推你向单体或微服务——它给你工具来正确组织任何一种。

对模块化单体,IServiceCollection 扩展方法模式是地道的 .NET 写法,随模块增加而良好扩展。Minimal API 也让每个模块拥有自己的端点注册变得特别容易:

public static class CustomerEndpoints
{
    public static IEndpointRouteBuilder MapCustomerEndpoints(
        this IEndpointRouteBuilder app)
    {
        app.MapGet("/api/customers/{id}",
            async (int id, ICustomerService svc) =>
        {
            var customer = await svc.GetByIdAsync(id);
            return customer is null
                ? Results.NotFound()
                : Results.Ok(customer);
        });
        return app;
    }
}

每个模块拥有自己的端点注册、服务注册和数据访问,宿主项目只负责组合。这让宿主保持薄,模块保持自包含。

单体什么时候是正确的选择

单体不是妥协,不是通向微服务的半步。它经常是系统的正确最终架构。

团队规模:2 到 10 人的团队用良好结构化的单体比用分布式系统行动更快。管理多个服务的协调开销,要等到有足够多的人、独立部署真的减少集成冲突时才划算。

领域清晰度:微服务要求你在构建之前就清楚服务边界。边界划错代价高昂——你会掉进分布式单体反模式。模块化单体让你通过先构建系统再内部重构来发现正确边界。单体里移动一个模块边界是一个下午的重构量。合并两个配置错误的微服务是数周的基础设施工作量。

项目阶段:早期产品方向不断变化。单体更优雅地吸收变化。微服务放大每次领域变更的成本。

多数应用诚实的默认选择是模块化单体。它不是垫脚石——它是合法的目标架构,正确构建时能处理显著规模。

结语

理解什么是单体——并区分三种类型——是任何做系统架构的 C# 开发者的基础知识。单进程单体是经典起点;模块化单体在保持单次部署的同时强制执行真正的内部边界;分布式单体是需要主动避免的反模式。

问题不是”单体还是微服务”,而是”什么结构适合我的团队规模、领域清晰度和产品阶段”。对大多数 .NET 应用,答案是一个构建良好的模块化单体。

参考


Tags


Previous

Agent Skills for .NET 正式发布:可复用的 Agent 领域技能包

Next

从零搭建你的第一个 MCP 服务端(C# 快速上手)