Skip to content

第十八章:效能優化 (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 Release

18.7 實戰練習

練習 1:實作快取層

  1. BookAppService 的所有查詢方法加入 Redis 快取。
  2. 實作快取失效策略 (更新/刪除時移除快取)。
  3. 監控快取命中率。

練習 2:解決 N+1 問題

  1. 在現有專案中找出所有的 N+1 查詢。
  2. 使用 Include 或投影重構。
  3. 使用 SQL Profiler 驗證查詢次數減少。

練習 3:效能基準測試

  1. 使用 BenchmarkDotNet 比較三種查詢方式的效能。
  2. 分析記憶體使用情況。

18.8 總結

效能優化是一個持續的過程。

  • 快取 是最簡單有效的優化手段。
  • 查詢優化 解決了大部分的效能瓶頸。
  • 監控 讓您能及早發現問題。

在下一章,我們將探討 安全性 (Security),學習如何保護 ABP 應用程式免受攻擊。


參考資源

Released under the MIT License.