第十四章:微服務架構設計 - 習題解答
本文件提供第十四章實戰練習的完整解答,涵蓋微服務方案建立、跨服務通訊和 API Gateway 配置。
練習 1:建立微服務方案
題目
- 使用 ABP CLI 建立微服務方案(注意:社群版沒有微服務範本,需手動建立多個
app專案並配置)。- 提示:建立
ProductService與OrderService兩個獨立專案。
- 提示:建立
- 配置 RabbitMQ 作為分散式事件匯流排。
解答
步驟 1:建立 ProductService
bash
# 建立 ProductService
abp new ProductService -t app --database-provider ef --ui none
cd ProductService步驟 2:建立 OrderService
bash
# 建立 OrderService
abp new OrderService -t app --database-provider ef --ui none
cd OrderService步驟 3:配置 RabbitMQ(ProductService)
安裝 NuGet 套件:
bash
cd src/ProductService.HttpApi.Host
dotnet add package Volo.Abp.EventBus.RabbitMQ配置 Module:
csharp
// ProductServiceHttpApiHostModule.cs
using Volo.Abp.EventBus.RabbitMQ;
[DependsOn(
typeof(AbpEventBusRabbitMqModule),
// ... 其他依賴
)]
public class ProductServiceHttpApiHostModule : AbpModule
{
public override void ConfigureServices(ServiceConfigurationContext context)
{
var configuration = context.Services.GetConfiguration();
// 配置 RabbitMQ
Configure<AbpRabbitMqEventBusOptions>(options =>
{
options.ClientName = "ProductService";
options.ExchangeName = "BookStore";
});
// 配置連線
Configure<AbpRabbitMqOptions>(options =>
{
options.Connections.Default.HostName = configuration["RabbitMQ:HostName"];
options.Connections.Default.Port = Convert.ToInt32(configuration["RabbitMQ:Port"]);
options.Connections.Default.UserName = configuration["RabbitMQ:UserName"];
options.Connections.Default.Password = configuration["RabbitMQ:Password"];
});
}
}appsettings.json:
json
{
"RabbitMQ": {
"HostName": "localhost",
"Port": 5672,
"UserName": "guest",
"Password": "guest"
}
}步驟 4:配置 RabbitMQ(OrderService)
重複步驟 3,但將 ClientName 改為 "OrderService"。
步驟 5:定義共享的事件 DTO
建立一個共享專案或在各自的 Domain.Shared 中定義:
csharp
// ProductService.Domain.Shared/Events/ProductStockChangedEto.cs
using System;
using Volo.Abp.EventBus;
namespace ProductService.Events
{
[EventName("ProductService.ProductStockChanged")]
public class ProductStockChangedEto
{
public Guid ProductId { get; set; }
public int NewStock { get; set; }
public int ChangedAmount { get; set; }
}
}步驟 6:發布事件(ProductService)
csharp
// ProductService.Application/Products/ProductAppService.cs
using Volo.Abp.EventBus.Distributed;
public class ProductAppService : ApplicationService
{
private readonly IDistributedEventBus _distributedEventBus;
private readonly IRepository<Product, Guid> _productRepository;
public ProductAppService(
IDistributedEventBus distributedEventBus,
IRepository<Product, Guid> productRepository)
{
_distributedEventBus = distributedEventBus;
_productRepository = productRepository;
}
public async Task DeductStockAsync(Guid productId, int quantity)
{
var product = await _productRepository.GetAsync(productId);
if (product.Stock < quantity)
{
throw new BusinessException("ProductService:InsufficientStock");
}
product.Stock -= quantity;
await _productRepository.UpdateAsync(product);
// 發布事件
await _distributedEventBus.PublishAsync(new ProductStockChangedEto
{
ProductId = productId,
NewStock = product.Stock,
ChangedAmount = -quantity
});
}
}步驟 7:訂閱事件(OrderService)
csharp
// OrderService.Application/EventHandlers/ProductStockChangedHandler.cs
using ProductService.Events;
using Volo.Abp.DependencyInjection;
using Volo.Abp.EventBus.Distributed;
namespace OrderService.EventHandlers
{
public class ProductStockChangedHandler :
IDistributedEventHandler<ProductStockChangedEto>,
ITransientDependency
{
private readonly ILogger<ProductStockChangedHandler> _logger;
public ProductStockChangedHandler(ILogger<ProductStockChangedHandler> logger)
{
_logger = logger;
}
public async Task HandleEventAsync(ProductStockChangedEto eventData)
{
_logger.LogInformation(
"Product {ProductId} stock changed to {NewStock}",
eventData.ProductId,
eventData.NewStock);
// 可以在這裡更新本地的產品快取或執行其他邏輯
await Task.CompletedTask;
}
}
}步驟 8:啟動 RabbitMQ
使用 Docker 快速啟動:
bash
docker run -d --name rabbitmq \
-p 5672:5672 \
-p 15672:15672 \
rabbitmq:3-management訪問管理介面:http://localhost:15672(帳號/密碼:guest/guest)
練習 2:實作跨服務通訊
題目
- 在
ProductService中建立商品。 - 在
OrderService中建立訂單時,透過HttpApi.Client同步檢查商品是否存在。 - 訂單建立後,發布事件通知
ProductService扣減庫存。
解答
步驟 1:在 ProductService 中實作商品 API
csharp
// ProductService.Application.Contracts/Products/IProductAppService.cs
public interface IProductAppService : IApplicationService
{
Task<ProductDto> GetAsync(Guid id);
Task<bool> CheckExistsAsync(Guid id);
Task DeductStockAsync(Guid productId, int quantity);
}csharp
// ProductService.Application/Products/ProductAppService.cs
public class ProductAppService : ApplicationService, IProductAppService
{
private readonly IRepository<Product, Guid> _productRepository;
public async Task<ProductDto> GetAsync(Guid id)
{
var product = await _productRepository.GetAsync(id);
return ObjectMapper.Map<Product, ProductDto>(product);
}
public async Task<bool> CheckExistsAsync(Guid id)
{
return await _productRepository.AnyAsync(p => p.Id == id);
}
public async Task DeductStockAsync(Guid productId, int quantity)
{
// 如練習 1 所示
}
}步驟 2:在 OrderService 中配置 HTTP Client
安裝 NuGet 套件:
bash
cd src/OrderService.HttpApi.Host
dotnet add package Volo.Abp.Http.Client
# 引用 ProductService.Application.Contracts
dotnet add reference ../../../ProductService/src/ProductService.Application.Contracts/ProductService.Application.Contracts.csproj配置 Module:
csharp
// OrderServiceHttpApiHostModule.cs
using Volo.Abp.Http.Client;
using ProductService;
[DependsOn(
typeof(AbpHttpClientModule),
typeof(ProductServiceApplicationContractsModule)
)]
public class OrderServiceHttpApiHostModule : AbpModule
{
public override void ConfigureServices(ServiceConfigurationContext context)
{
var configuration = context.Services.GetConfiguration();
// 配置 HTTP Client Proxy
context.Services.AddHttpClientProxies(
typeof(ProductServiceApplicationContractsModule).Assembly,
remoteServiceConfigurationName: "ProductService"
);
// 配置遠端服務地址
Configure<AbpRemoteServiceOptions>(options =>
{
options.RemoteServices.Configure("ProductService", config =>
{
config.BaseUrl = configuration["RemoteServices:ProductService:BaseUrl"];
});
});
}
}appsettings.json:
json
{
"RemoteServices": {
"ProductService": {
"BaseUrl": "http://localhost:5001"
}
}
}步驟 3:在 OrderService 中使用 HTTP Client
csharp
// OrderService.Application/Orders/OrderAppService.cs
using ProductService.Products;
using Volo.Abp.EventBus.Distributed;
public class OrderAppService : ApplicationService
{
private readonly IProductAppService _productAppService; // 遠端代理
private readonly IRepository<Order, Guid> _orderRepository;
private readonly IDistributedEventBus _distributedEventBus;
public OrderAppService(
IProductAppService productAppService,
IRepository<Order, Guid> orderRepository,
IDistributedEventBus distributedEventBus)
{
_productAppService = productAppService;
_orderRepository = orderRepository;
_distributedEventBus = distributedEventBus;
}
public async Task<OrderDto> CreateAsync(CreateOrderDto input)
{
// 1. 同步檢查商品是否存在
var productExists = await _productAppService.CheckExistsAsync(input.ProductId);
if (!productExists)
{
throw new BusinessException("OrderService:ProductNotFound");
}
// 2. 建立訂單
var order = new Order(
GuidGenerator.Create(),
input.ProductId,
input.Quantity,
OrderStatus.Pending
);
await _orderRepository.InsertAsync(order);
// 3. 發布事件通知 ProductService 扣減庫存
await _distributedEventBus.PublishAsync(new OrderCreatedEto
{
OrderId = order.Id,
ProductId = input.ProductId,
Quantity = input.Quantity
});
return ObjectMapper.Map<Order, OrderDto>(order);
}
}步驟 4:在 ProductService 中訂閱訂單事件
csharp
// ProductService.Application/EventHandlers/OrderCreatedHandler.cs
using OrderService.Events;
public class OrderCreatedHandler :
IDistributedEventHandler<OrderCreatedEto>,
ITransientDependency
{
private readonly IProductAppService _productAppService;
public OrderCreatedHandler(IProductAppService productAppService)
{
_productAppService = productAppService;
}
public async Task HandleEventAsync(OrderCreatedEto eventData)
{
try
{
await _productAppService.DeductStockAsync(
eventData.ProductId,
eventData.Quantity);
}
catch (BusinessException ex) when (ex.Code == "ProductService:InsufficientStock")
{
// 發布庫存不足事件,讓 OrderService 取消訂單
await _distributedEventBus.PublishAsync(new StockDeductionFailedEto
{
OrderId = eventData.OrderId,
Reason = "InsufficientStock"
});
}
}
}練習 3:配置 YARP Gateway
題目
- 建立一個新的 ASP.NET Core 空專案作為 Gateway。
- 安裝
Yarp.ReverseProxy。 - 配置路由將
/api/products轉發到ProductService,將/api/orders轉發到OrderService。
解答
步驟 1:建立 Gateway 專案
bash
dotnet new web -n ApiGateway
cd ApiGateway
dotnet add package Yarp.ReverseProxy步驟 2:配置 Program.cs
csharp
// Program.cs
using Yarp.ReverseProxy.Configuration;
var builder = WebApplication.CreateBuilder(args);
// 加入 YARP
builder.Services.AddReverseProxy()
.LoadFromConfig(builder.Configuration.GetSection("ReverseProxy"));
var app = builder.Build();
// 使用 YARP
app.MapReverseProxy();
app.Run();步驟 3:配置 appsettings.json
json
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"AllowedHosts": "*",
"ReverseProxy": {
"Routes": {
"product-route": {
"ClusterId": "product-cluster",
"Match": {
"Path": "/api/products/{**catch-all}"
},
"Transforms": [
{
"PathPattern": "/api/app/product/{**catch-all}"
}
]
},
"order-route": {
"ClusterId": "order-cluster",
"Match": {
"Path": "/api/orders/{**catch-all}"
},
"Transforms": [
{
"PathPattern": "/api/app/order/{**catch-all}"
}
]
}
},
"Clusters": {
"product-cluster": {
"Destinations": {
"destination1": {
"Address": "http://localhost:5001"
}
}
},
"order-cluster": {
"Destinations": {
"destination1": {
"Address": "http://localhost:5002"
}
}
}
}
}
}步驟 4:加入健康檢查
csharp
// Program.cs
builder.Services.AddReverseProxy()
.LoadFromConfig(builder.Configuration.GetSection("ReverseProxy"))
.AddHealthChecks();json
{
"ReverseProxy": {
"Clusters": {
"product-cluster": {
"HealthCheck": {
"Active": {
"Enabled": true,
"Interval": "00:00:10",
"Timeout": "00:00:05",
"Policy": "ConsecutiveFailures",
"Path": "/health"
}
},
"Destinations": {
"destination1": {
"Address": "http://localhost:5001",
"Health": "http://localhost:5001/health"
}
}
}
}
}
}步驟 5:加入認證與授權
csharp
// Program.cs
using Microsoft.AspNetCore.Authentication.JwtBearer;
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(options =>
{
options.Authority = "http://localhost:5000"; // IdentityServer
options.RequireHttpsMetadata = false;
options.Audience = "BookStore";
});
builder.Services.AddAuthorization();
// ...
app.UseAuthentication();
app.UseAuthorization();
app.MapReverseProxy();步驟 6:加入速率限制
bash
dotnet add package AspNetCoreRateLimitcsharp
// Program.cs
using AspNetCoreRateLimit;
builder.Services.AddMemoryCache();
builder.Services.Configure<IpRateLimitOptions>(builder.Configuration.GetSection("IpRateLimiting"));
builder.Services.AddInMemoryRateLimiting();
builder.Services.AddSingleton<IRateLimitConfiguration, RateLimitConfiguration>();
// ...
app.UseIpRateLimiting();
app.MapReverseProxy();json
{
"IpRateLimiting": {
"EnableEndpointRateLimiting": true,
"StackBlockedRequests": false,
"RealIpHeader": "X-Real-IP",
"ClientIdHeader": "X-ClientId",
"HttpStatusCode": 429,
"GeneralRules": [
{
"Endpoint": "*",
"Period": "1m",
"Limit": 100
}
]
}
}步驟 7:測試 Gateway
啟動所有服務:
bash
# Terminal 1: ProductService
cd ProductService/src/ProductService.HttpApi.Host
dotnet run --urls="http://localhost:5001"
# Terminal 2: OrderService
cd OrderService/src/OrderService.HttpApi.Host
dotnet run --urls="http://localhost:5002"
# Terminal 3: Gateway
cd ApiGateway
dotnet run --urls="http://localhost:5000"測試請求:
bash
# 透過 Gateway 存取 ProductService
curl http://localhost:5000/api/products
# 透過 Gateway 存取 OrderService
curl http://localhost:5000/api/orders總結
本章練習涵蓋了微服務架構的核心實作:
微服務方案建立:
- 建立多個獨立的 ABP 應用程式
- 配置 RabbitMQ 作為事件匯流排
- 實作事件發布與訂閱
跨服務通訊:
- 使用 HTTP Client Proxy 進行同步通訊
- 使用分散式事件進行非同步通訊
- 處理跨服務的錯誤與補償
API Gateway:
- 使用 YARP 建立反向代理
- 配置路由與轉換
- 加入健康檢查、認證和速率限制
最佳實踐:
- 優先使用非同步通訊減少耦合
- 實作 Saga 模式處理分散式交易
- 使用 API Gateway 統一入口
- 加入完整的可觀測性(日誌、追蹤、監控)
- 每個服務擁有自己的資料庫