Hướng dẫn kết nối MySQL trong C# với EF Core: Connection Pooling và Transaction an toàn

MySQL tutorial - IT technology blog
MySQL tutorial - IT technology blog

Vấn đề: Tại sao kết nối MySQL trong .NET lại hay gặp lỗi hiệu năng?

Nhiều bạn khi mới bắt đầu làm việc với .NET và MySQL thường cài đặt thư viện MySql.Data (thư viện chính chủ của Oracle) theo thói quen. Tuy nhiên, qua thực tế triển khai các dự án lớn, mình nhận thấy thư viện này đôi khi gặp vấn đề về hiệu năng và không tương thích hoàn hảo với các tính năng bất đồng bộ (async/await) của .NET hiện đại.

Một vấn đề khác mà anh em hay gặp là không kiểm soát được số lượng kết nối gửi tới server. Khi traffic tăng đột biến, ứng dụng mở hàng trăm connection nhưng không đóng lại kịp, dẫn đến lỗi “Too many connections” trên MySQL Server. Kinh nghiệm xương máu của mình khi quản lý database production chạy MySQL 8.0 với khoảng 50GB data là: nếu không cấu hình Connection Pooling chuẩn, hệ thống sẽ sập ngay khi có đợt peak traffic.

Giải pháp tối ưu hiện nay là kết hợp MySqlConnector (một driver mã nguồn mở cực nhanh) cùng với Pomelo.EntityFrameworkCore.MySql để tận dụng sức mạnh của Entity Framework Core (EF Core).

Cài đặt môi trường và thư viện cần thiết

Thay vì dùng thư viện mặc định, chúng ta sẽ sử dụng bộ đôi thư viện được cộng đồng đánh giá cao nhất về độ ổn định và tốc độ. Mở Terminal trong project của bạn và chạy các lệnh sau:

# Cài đặt Provider cho EF Core
dotnet add package Pomelo.EntityFrameworkCore.MySql

# Cài đặt công cụ để chạy Migration
dotnet add package Microsoft.EntityFrameworkCore.Design

Lý do mình chọn Pomelo là vì nó hỗ trợ tốt các tính năng đặc thù của MySQL và MariaDB mà thư viện chính chủ của Microsoft hay Oracle đôi khi bỏ sót hoặc cập nhật chậm.

Cấu hình Connection Pooling chi tiết

Connection Pooling giúp tái sử dụng các kết nối đã mở, thay vì mỗi request lại tạo một kết nối mới (vốn rất tốn tài nguyên CPU và RAM). Trong file appsettings.json, bạn cần định nghĩa connection string kèm theo các tham số tối ưu:

{
  "ConnectionStrings": {
    "DefaultConnection": "Server=localhost;Port=3306;Database=my_app_db;User=root;Password=your_password;Pooling=true;MinPoolSize=5;MaxPoolSize=100;ConnectionTimeout=30;"
  }
}

Giải thích thông số:

  • Pooling=true: Kích hoạt tính năng pooling.
  • MinPoolSize=5: Luôn duy trì ít nhất 5 kết nối chờ sẵn.
  • MaxPoolSize=100: Giới hạn tối đa 100 kết nối để tránh làm treo MySQL Server.

Tiếp theo, đăng ký DbContext trong Program.cs:

var connectionString = builder.Configuration.GetConnectionString("DefaultConnection");
var serverVersion = new MySqlServerVersion(new Version(8, 0, 30)); // Thay đổi theo version MySQL của bạn

builder.Services.AddDbContext<AppDbContext>(
    options => options.UseMySql(connectionString, serverVersion)
        .LogTo(Console.WriteLine, LogLevel.Information) // Log SQL để debug
        .EnableSensitiveDataLogging()
        .EnableDetailedErrors()
);

Thao tác dữ liệu và Migration

Migration là cách tuyệt vời để quản lý version của database. Thay vì vào MySQL Workbench tạo bảng bằng tay, mình luôn khuyên anh em định nghĩa qua code (Code-First).

Tạo Model và DbContext

public class Product
{
    public int Id { get; set; }
    public string Name { get; set; } = null!;
    public decimal Price { get; set; }
}

public class AppDbContext : DbContext
{
    public AppDbContext(DbContextOptions<AppDbContext> options) : base(options) { }
    public DbSet<Product> Products => Set<Product>();
}

Chạy Migration

Thực hiện các lệnh sau để đồng bộ code vào database:

dotnet ef migrations add InitialCreate
dotnet ef database update

Kinh nghiệm của mình là luôn kiểm tra file Migration được sinh ra trước khi update. Đôi khi EF Core hiểu nhầm kiểu dữ liệu (ví dụ string thành longtext trong khi bạn chỉ cần varchar(255)), việc điều chỉnh kịp thời sẽ giúp database gọn nhẹ hơn.

Xử lý Transaction an toàn và tránh SQL Injection

Một trong những sai lầm chết người là cộng chuỗi SQL. EF Core mặc định đã chống SQL Injection khi bạn dùng LINQ. Tuy nhiên, khi cần dùng SQL thuần (Raw SQL), bạn phải cực kỳ cẩn thận.

Cách viết an toàn tránh SQL Injection

// SAI: Dễ bị SQL Injection
var category = "'electronics' OR 1=1";
var products = context.Products.FromSqlRaw($"SELECT * FROM Products WHERE Category = {category}").ToList();

// ĐÚNG: Sử dụng Parameterized Query
var products = context.Products
    .FromSqlInterpolated($"SELECT * FROM Products WHERE Category = {category}")
    .ToList();

Sử dụng Transaction để đảm bảo toàn vẹn dữ liệu

Khi bạn cần thực hiện nhiều thao tác (ví dụ: trừ tiền khách hàng và tạo đơn hàng), nếu một bước lỗi, toàn bộ phải rollback. Database của mình từng bị sai lệch tồn kho chỉ vì thiếu Transaction ở những đoạn code quan trọng.

using var transaction = await _context.Database.BeginTransactionAsync();

try
{
    var product = await _context.Products.FindAsync(1);
    product.Price -= 10;
    
    await _context.SaveChangesAsync();

    // Giả sử có một logic nghiệp vụ khác ở đây
    
    await transaction.CommitAsync();
    Console.WriteLine("Giao dịch thành công!");
}
catch (Exception)
{
    await transaction.RollbackAsync();
    Console.WriteLine("Giao dịch thất bại, đã rollback.");
}

Kiểm tra và Monitoring hiệu năng

Để biết hệ thống có đang chạy ổn hay không, mình thường kiểm tra số lượng connection đang thực tế kết nối vào MySQL bằng câu lệnh SQL:

SHOW STATUS WHERE `variable_name` = 'Threads_connected';

Nếu con số này liên tục chạm ngưỡng MaxPoolSize mà bạn đã cấu hình, đó là dấu hiệu cho thấy bạn đang bị “leak” connection (quên không dispose DbContext) hoặc query quá chậm làm nghẽn hàng đợi. Trong EF Core, việc sử dụng AddDbContext với cơ chế Dependency Injection mặc định đã xử lý việc dispose rất tốt, nhưng nếu bạn tự khởi tạo bằng từ khóa new, hãy chắc chắn bọc nó trong khối using.

Ngoài ra, hãy tận dụng tính năng logging của EF Core mà mình đã hướng dẫn ở phần cấu hình. Nhìn vào console, bạn sẽ thấy các câu lệnh SQL thực tế được gửi đi. Những optimization nhỏ như thêm .AsNoTracking() cho các câu lệnh chỉ đọc (read-only) cũng giúp giảm tải đáng kể cho bộ nhớ ứng dụng.

Việc làm chủ kết nối MySQL trong C# không chỉ đơn thuần là gọi hàm, mà là hiểu cách quản lý tài nguyên và bảo vệ dữ liệu. Những tinh chỉnh về Connection Pooling và Transaction ở trên là nền tảng để bạn xây dựng các ứng dụng .NET có khả năng scale tốt và bảo mật cao.

Share: