Khi làm việc với cơ sở dữ liệu SQL Server trên nền tảng .NET, đặc biệt là trong các hệ thống có tải cao, hiện tượng Deadlock thường xuất hiện khi nhiều giao dịch cạnh tranh truy cập cùng một tài nguyên. Để cải thiện khả năng đọc dữ liệu mà không gây ảnh hưởng đến tính nhất quán ở mức độ cực kỳ cao, kỹ thuật sử dụng trạng thái READ UNCOMMITTED thông qua câu lệnh WITH (NOLOCK) được áp dụng rộng rãi. Cách tiếp cận này giúp giảm thiểu việc chờ khóa, tuy nhiên nó đi kèm với rủi ro về dữ liệu bẩn.
Có một cách linh hoạt để áp dụng NOLOCK vào các truy vấn LINQ mà không cần sửa đổi từng câu lệnh SQL thủ công là thông qua cơ chế Interceptor. Kỹ thuật này cho phép chúng ta chèn WITH (NOLOCK) vào cú pháp lệnh SQL cuối cùng trước khi gửi lên server.
Cấu Trúc Bộ Chặn Lệnh
Phần lõi của giải pháp là tạo một lớp kế thừa từ DbCommandInterceptor. Lớp này sẽ kiểm tra trạng thái toàn cục để quyết định thay đổi nội dung lệnh SQL.
using System.Data.Common;
using System.Text.RegularExpressions;
using Microsoft.EntityFrameworkCore.Storage.Internal;
using Microsoft.EntityFrameworkCore.Diagnostics;
namespace Core.Database.Interceptors
{
public class ReadOnlyQueryInterceptor : DbCommandInterceptor
{
// Cờ kiểm soát chế độ không khóa
private static bool _forceReadOnly = false;
// Biểu thức chính quy để nhận diện các bảng đã alias nhưng chưa có NOLOCK
private static readonly Regex TablePattern = new Regex(
@"(?<tableAlias>(FROM|JOIN) \[[^\]]+\] AS \[[^\]]+\])(?! WITH \(NOLOCK\))",
RegexOptions.Compiled | RegexOptions.IgnoreCase);
/// <summary>
/// Phương thức thực thi đồng bộ
/// </summary>
public override InterceptionResult<object> ScalarExecuting(
DbCommand command,
CommandEventData eventData,
InterceptionResult<object> result)
{
InjectNoLockHint(command);
return base.ScalarExecuting(command, eventData, result);
}
/// <summary>
/// Phương thức thực thi không đồng bộ
/// </summary>
public override ValueTask ScalarExecutingAsync(
DbCommand command,
CommandEventData eventData,
InterceptionResult<object> result,
CancellationToken cancellationToken = default)
{
InjectNoLockHint(command);
return base.ScalarExecutingAsync(command, eventData, result, cancellationToken);
}
/// <summary>
/// Xử lý kết quả trả về dạng Reader
/// </summary>
public override InterceptionResult<DbDataReader> ReaderExecuting(
DbCommand command,
CommandEventData eventData,
InterceptionResult<DbDataReader> result)
{
InjectNoLockHint(command);
return result;
}
public override ValueTask ReaderExecutingAsync(
DbCommand command,
CommandEventData eventData,
InterceptionResult<DbDataReader> result,
CancellationToken cancellationToken = default)
{
InjectNoLockHint(command);
return base.ReaderExecutingAsync(command, eventData, result, cancellationToken);
}
private void InjectNoLockHint(DbCommand command)
{
if (_forceReadOnly && !string.IsNullOrEmpty(command.CommandText))
{
command.CommandText = TablePattern.Replace(
command.CommandText,
"${tableAlias} WITH (NOLOCK)"
);
}
}
public void EnableNoLock(bool state)
{
_forceReadOnly = state;
}
}
}
Thiết Lập Tiện Ích Mở Rộng
Thay vì gọi các phương thức tĩnh trực tiếp, nên sử dụng hàm mở rộng để quản lý vòng đời kích hoạt trạng thái NOLOCK. Điều này đảm bảo cờ sẽ luôn được reset về false sau khi truy vấn hoàn tất, kể cả khi xảy ra lỗi.
using System.Linq;
using System.Threading.Tasks;
using Core.Database.Interceptors;
namespace Core.Extensions
{
public static class QueryExecutionExtensions
{
/// <summary>
/// Thực thi truy vấn với quyền đọc không khóa
/// </summary>
public static TResult ExecuteWithNoLock<T, TResult>(
this IQueryable<T> source,
Func<IQueryable<T>, TResult> action)
{
var interceptor = new ReadOnlyQueryInterceptor();
interceptor.EnableNoLock(true);
try
{
return action(source);
}
finally
{
interceptor.EnableNoLock(false);
}
}
}
}
Đăng Ký Vào Container Dependency Injection
Trong file khởi tạo ứng dụng (Startup.cs hoặc Program.cs), đăng ký bộ chặn vào cấu hình DbContext.
using Microsoft.EntityFrameworkCore;
using Core.Database.Interceptors;
using Microsoft.Extensions.DependencyInjection;
// Cấu hình Services
services.AddScoped<ReadOnlyQueryInterceptor>();
services.AddDbContext<MyDataContext>(options =>
{
options.UseSqlServer(connectionString)
.AddInterceptors(new ReadOnlyQueryInterceptor());
});
Ví Dụ Áp Dụng Trong Logic Nghiệp Vụ
Sau khi cài đặt xong, bạn có thể áp dụng vào bất kỳ truy vấn nào cần tối ưu hiệu suất đọc nhanh mà chấp nhận rủi ro dữ liệu cũ.
var userId = 12345;
var result = await _context.Users
.Where(u => u.Id == userId)
.Select(u => new UserDto
{
Username = u.Username,
Email = u.Email,
Status = u.Status
})
.ExecuteWithNoLock(q => q.ToListAsync());