第十八章:效能優化 (Performance Optimization)
18.1 引言:效能的重要性
效能不僅影響使用者體驗,也直接影響成本 (伺服器資源)。一個優化良好的應用程式可以用更少的資源服務更多的使用者。
1. 效能目標
- 回應時間:API 回應時間 < 200ms (P95)。
- 吞吐量:單台伺服器能處理 1000+ RPS (Requests Per Second)。
- 資源使用:CPU < 70%, Memory < 80%。
18.2 分散式快取 (Distributed Caching)
快取是效能優化的第一步。
1. 為什麼需要分散式快取?
- In-Memory Cache 只存在於單一進程中,無法在多台伺服器間共享。
- Redis 是一個高效能的 Key-Value 儲存,支援多種資料結構。
2. 整合 Redis
安裝套件:
bash
dotnet add package Microsoft.Extensions.Caching.StackExchangeRedis配置 Module:
csharp
public override void ConfigureServices(ServiceConfigurationContext context)
{
var configuration = context.Services.GetConfiguration();
context.Services.AddStackExchangeRedisCache(options =>
{
options.Configuration = configuration["Redis:Configuration"];
options.InstanceName = "BookStore:";
});
Configure<AbpDistributedCacheOptions>(options =>
{
options.KeyPrefix = "BookStore:";
options.GlobalCacheEntryOptions.SlidingExpiration = TimeSpan.FromMinutes(20);
});
}3. 使用快取
csharp
public class BookAppService : ApplicationService
{
private readonly IDistributedCache<BookDto> _cache;
private readonly IRepository<Book, Guid> _bookRepository;
public async Task<BookDto> GetAsync(Guid id)
{
// 嘗試從快取讀取
var cacheKey = $"Book:{id}";
var cached = await _cache.GetAsync(cacheKey);
if (cached != null)
{
Logger.LogDebug("Cache hit for book {BookId}", id);
return cached;
}
// 快取未命中,從資料庫讀取
var book = await _bookRepository.GetAsync(id);
var dto = ObjectMapper.Map<Book, BookDto>(book);
// 儲存到快取 (10 分鐘)
await _cache.SetAsync(
cacheKey,
dto,
new DistributedCacheEntryOptions
{
AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(10)
}
);
return dto;
}
public async Task UpdateAsync(Guid id, UpdateBookDto input)
{
var book = await _bookRepository.GetAsync(id);
ObjectMapper.Map(input, book);
await _bookRepository.UpdateAsync(book);
// 更新後移除快取
await _cache.RemoveAsync($"Book:{id}");
}
}18.3 查詢優化
1. N+1 問題
這是 ORM 中最常見的效能殺手。
問題範例:
csharp
// 錯誤:會產生 1 + N 次查詢
public async Task<List<OrderDto>> GetOrdersAsync()
{
var orders = await _orderRepository.GetListAsync(); // 查詢 1
foreach (var order in orders)
{
// 每個訂單都會觸發一次查詢 (查詢 N)
order.Customer = await _customerRepository.GetAsync(order.CustomerId);
}
return ObjectMapper.Map<List<Order>, List<OrderDto>>(orders);
}解決方案:使用 Include:
csharp
// 正確:只產生 1-2 次查詢
public async Task<List<OrderDto>> GetOrdersAsync()
{
var orders = await (await _orderRepository.GetQueryableAsync())
.Include(o => o.Customer)
.Include(o => o.Items)
.ThenInclude(i => i.Product)
.ToListAsync();
return ObjectMapper.Map<List<Order>, List<OrderDto>>(orders);
}2. 投影 (Projection)
只查詢需要的欄位,減少資料傳輸量。
csharp
public async Task<List<BookSummaryDto>> GetBookSummariesAsync()
{
return await (await _bookRepository.GetQueryableAsync())
.Select(b => new BookSummaryDto
{
Id = b.Id,
Name = b.Name,
AuthorName = b.Author.Name, // 自動 JOIN
Price = b.Price
})
.ToListAsync();
}3. AsNoTracking
對於唯讀查詢,停用 EF Core 的變更追蹤可以顯著提升效能。
csharp
public async Task<List<BookDto>> GetBooksForDisplayAsync()
{
return await (await _bookRepository.GetQueryableAsync())
.AsNoTracking() // 停用追蹤
.OrderBy(b => b.Name)
.Take(100)
.ProjectTo<BookDto>(_mapper.ConfigurationProvider)
.ToListAsync();
}18.4 資料庫優化
1. 索引 (Indexes)
為常用的查詢欄位建立索引。
csharp
protected override void OnModelCreating(ModelBuilder builder)
{
base.OnModelCreating(builder);
builder.Entity<Book>(b =>
{
b.HasIndex(x => x.Name); // 單欄索引
b.HasIndex(x => new { x.AuthorId, x.PublishDate }); // 複合索引
b.HasIndex(x => x.ISBN).IsUnique(); // 唯一索引
});
}2. 分頁
永遠不要一次載入所有資料。
csharp
public async Task<PagedResultDto<BookDto>> GetListAsync(GetBooksInput input)
{
var queryable = await _bookRepository.GetQueryableAsync();
// 先計算總數 (可以考慮快取)
var totalCount = await queryable.CountAsync();
// 分頁查詢
var books = await queryable
.OrderBy(input.Sorting ?? "name")
.Skip(input.SkipCount)
.Take(input.MaxResultCount)
.ToListAsync();
return new PagedResultDto<BookDto>(
totalCount,
ObjectMapper.Map<List<Book>, List<BookDto>>(books)
);
}18.5 效能監控
1. Application Insights
微軟官方的 APM (Application Performance Monitoring) 工具。
安裝:
bash
dotnet add package Microsoft.ApplicationInsights.AspNetCore配置:
csharp
builder.Services.AddApplicationInsightsTelemetry(options =>
{
options.ConnectionString = configuration["ApplicationInsights:ConnectionString"];
});2. 自訂追蹤
csharp
public class BookAppService : ApplicationService
{
private readonly TelemetryClient _telemetry;
public async Task<BookDto> GetAsync(Guid id)
{
using var operation = _telemetry.StartOperation<RequestTelemetry>("GetBook");
operation.Telemetry.Properties["BookId"] = id.ToString();
try
{
var book = await _bookRepository.GetAsync(id);
_telemetry.TrackMetric("BookRetrievalTime", operation.Telemetry.Duration.TotalMilliseconds);
return ObjectMapper.Map<Book, BookDto>(book);
}
catch (Exception ex)
{
_telemetry.TrackException(ex);
throw;
}
}
}18.6 效能測試
1. BenchmarkDotNet
用於微基準測試 (Micro-benchmarking)。
csharp
[MemoryDiagnoser]
[SimpleJob(RuntimeMoniker.Net90)]
public class BookQueryBenchmarks
{
private IRepository<Book, Guid> _repository;
[GlobalSetup]
public void Setup()
{
// 初始化測試環境
}
[Benchmark(Baseline = true)]
public async Task<List<Book>> WithoutInclude()
{
return await _repository.GetListAsync();
}
[Benchmark]
public async Task<List<Book>> WithInclude()
{
return await (await _repository.GetQueryableAsync())
.Include(b => b.Author)
.ToListAsync();
}
[Benchmark]
public async Task<List<BookDto>> WithProjection()
{
return await (await _repository.GetQueryableAsync())
.Select(b => new BookDto { Id = b.Id, Name = b.Name })
.ToListAsync();
}
}執行:
bash
dotnet run -c Release18.7 實戰練習
練習 1:實作快取層
- 為
BookAppService的所有查詢方法加入 Redis 快取。 - 實作快取失效策略 (更新/刪除時移除快取)。
- 監控快取命中率。
練習 2:解決 N+1 問題
- 在現有專案中找出所有的 N+1 查詢。
- 使用
Include或投影重構。 - 使用 SQL Profiler 驗證查詢次數減少。
練習 3:效能基準測試
- 使用 BenchmarkDotNet 比較三種查詢方式的效能。
- 分析記憶體使用情況。
18.8 總結
效能優化是一個持續的過程。
- 快取 是最簡單有效的優化手段。
- 查詢優化 解決了大部分的效能瓶頸。
- 監控 讓您能及早發現問題。
在下一章,我們將探討 安全性 (Security),學習如何保護 ABP 應用程式免受攻擊。
參考資源: