Công nghệ - 15/04/2025 09:43:56
Bạn hãy tưởng tượng một website bán hàng đang có một chiếc PS5 phiên bản giới hạn, đang giảm giá $100 nhưng chỉ còn một chiếc trong kho. Hai khách hàng, Nam và Hằng, cùng nhìn thấy thấy InStock=1, đồng thời nhấn nút "Mua" - Place Order. Sẽ xảy ra tình huống gì ở đây?
Trong hệ thống thương mại điện tử, việc xử lý hai khách hàng cùng nhấn nút "Mua" cho sản phẩm cuối cùng là một bài toán concurrency điển hình. Nếu không kiểm soát tốt, cả hai có thể nhận thông báo "Mua thành công", dẫn đến bán quá mức (overselling) và lỗi dữ liệu.
Trong bài viết này, tôi sẽ trình bày một giải pháp sử dụng pessimistic locking với SemaphoreSlim trong C#, đồng thời tái cấu trúc code để tuân thủ nguyên tắc SOLID bằng cách tách logic quản lý kho sang một InventoryService. Tôi cũng sẽ cung cấp các unit test để đảm bảo tính đúng đắn của giải pháp.
Trong bài viết này, tôi sẽ trình bày một giải pháp sử dụng pessimistic locking với SemaphoreSlim trong C#, đồng thời cấu trúc code để tuân thủ nguyên tắc SOLID bằng cách tách logic quản lý kho sang một InventoryService - chúng ta chưa quan tâm tới service này vội nhé.
1. Khóa giao dịch với SemaphoreSlim: Đảm bảo chỉ một giao dịch được xử lý cho mỗi sản phẩm tại một thời điểm.
2. Tách logic kho sang InventoryService:
3. Sử dụng Dependency Injection: Đảm bảo các service có thể thay thế nhau, tuân thủ Dependency Inversion Principle (DIP).
Giả sử chúng ta có 1 class Product đơn giản chỉ gồm: Id, Name và Stock
public class Product
{
public int Id { get; set; }
public string Name { get; set; }
public int Stock { get; set; }
}
Đồng thời có interface IInventoryService để chứa các nghiệp vụ của inventory.
public interface IInventoryService
{
Task<Product?> GetProductAsync(int productId);
Task<bool> UpdateStockAsync(int productId, int quantityToReduce);
}
1 interface IPurchaseService và implementation của nó được xây dựng dưới đây
public interface IPurchaseService
{
Task<bool> TryPurchaseAsync(int productId);
}
public class PurchaseService : IPurchaseService
{
private readonly IInventoryService _inventoryService;
private static readonly ConcurrentDictionary<int, SemaphoreSlim> _productLocks = new();
public PurchaseService(IInventoryService inventoryService)
{
_inventoryService = inventoryService;
}
public async Task<bool> TryPurchaseAsync(int productId)
{
var semaphore = _productLocks.GetOrAdd(productId, _ => new SemaphoreSlim(1, 1));
try
{
if (!await semaphore.WaitAsync(TimeSpan.FromSeconds(5)))
{
return false; // Timeout
}
return await _inventoryService.UpdateStockAsync(productId, 1);
}
catch (Exception ex)
{
// Log lỗi (giả sử có logger)
Console.WriteLine($"Purchase error: {ex.Message}");
return false;
}
finally
{
semaphore.Release();
if (semaphore.CurrentCount == 1)
{
_productLocks.TryRemove(productId, out _);
}
}
}
}
Tình huống
Kết quả: Nam mua thành công, Hằng nhận thông báo hết hàng => Hằng gọi điện cho Nam, 2 bạn cùng về một nhà chơi game cùng nhau :)
Để chắc chắn, chúng ta cùng viết nhanh một cái UnitTest cho trường hợp này, vì Test cực kỳ quan trọng mà :)
using Microsoft.EntityFrameworkCore;
using Moq;
using System;
using System.Threading.Tasks;
using Xunit;
public class PurchaseServiceConcurrencyTests
{
private readonly Mock<IInventoryService> _inventoryServiceMock;
private readonly IPurchaseService _purchaseService;
public PurchaseServiceConcurrencyTests()
{
_inventoryServiceMock = new Mock<IInventoryService>();
_purchaseService = new PurchaseService(_inventoryServiceMock.Object);
}
[Fact]
public async Task TryPurchaseAsync_TwoUsersConcurrent_OnlyOneSucceeds()
{
// Arrange
int productId = 1;
bool firstPurchaseResult = false;
bool secondPurchaseResult = false;
// Giả lập InventoryService: chỉ cho phép một lần UpdateStockAsync thành công
_inventoryServiceMock
.SetupSequence(s => s.UpdateStockAsync(productId, 1))
.ReturnsAsync(true) // Lần đầu thành công
.ReturnsAsync(false); // Lần hai thất bại (hết hàng)
// Act
// Chạy hai giao dịch đồng thời
var task1 = Task.Run(async () => firstPurchaseResult = await _purchaseService.TryPurchaseAsync(productId));
var task2 = Task.Run(async () => secondPurchaseResult = await _purchaseService.TryPurchaseAsync(productId));
// Đợi cả hai hoàn thành
await Task.WhenAll(task1, task2);
// Assert
// Chỉ một trong hai giao dịch thành công
Assert.True(firstPurchaseResult != secondPurchaseResult, "Exactly one purchase should succeed");
Assert.Equal(2, _inventoryServiceMock.Invocations.Count); // UpdateStockAsync được gọi đúng 2 lần
}
}
Kịch bản giả lập:
Tình huống: Hai người dùng (giả lập bằng hai task) gọi TryPurchaseAsync cho sản phẩm ID=1 gần như đồng thời. Sản phẩm chỉ có 1 trong kho.
Mong đợi:
Cách SemaphoreSlim đảm bảo concurrency
Phương án hoạt động được, nhưng phương án nào cũng có ưu và nhược, chúng ta thử phân tích nhé
Ưu điểm
Nhược điểm
Tối ưu hiệu suất: Sử dụng hàng đợi (queue) với RabbitMQ hoặc Azure Service Bus để xếp hàng các giao dịch thay vì khóa trực tiếp. Điều này cho phép xử lý thứ tự mà không cần giữ khóa lâu. Thay vì SemaphoreSlim, gửi yêu cầu mua vào một queue và xử lý từng yêu cầu một cách tuần tự.
Tăng thời gian timeout hoặc retry: Cho phép retry giao dịch nếu timeout xảy ra, hoặc điều chỉnh timeout dựa trên tải hệ thống (ví dụ, 10 giây thay vì 5 giây) - có thể sử dụng thư viện Polly cho phương án này.
Quản lý bộ nhớ: Thêm cơ chế tự động xóa semaphore không sử dụng sau một khoảng thời gian (ví dụ, 1 giờ). Sử dụng Timer hoặc một background service để kiểm tra và xóa các semaphore không hoạt động.
Hỗ trợ hệ thống phân tán: hay SemaphoreSlim bằng khóa phân tán với Redis hoặc ZooKeeper để hỗ trợ nhiều server.
Sử dụng khóa SemaphoreSlim trong C# là một phương án xử lý đảm bảo chỉ một người dùng mua được sản phẩm cuối cùng trong tình huống hai client tranh chấp, nhấn "Mua" cùng lúc. Bằng cách khóa theo sản phẩm, chúng ta loại bỏ nguy cơ overselling và giữ được tính toàn vẹn dữ liệu.
Giải pháp này đặc biệt phù hợp cho các hệ thống thương mại điện tử vừa và nhỏ, nơi các sản phẩm hot thường xuyên gây ra tranh giành. Dù có một số nhược điểm về hiệu suất và hỗ trợ phân tán, chúng có thể được khắc phục bằng các công cụ như Redis, queue, hoặc retry mechanism.
Tôi tin rằng cách tiếp cận này cân bằng giữa độ tin cậy, tính đơn giản, và khả năng mở rộng.
Nếu bạn có những phương án khác hay bạn đang xây dựng một hệ thống tương tự, hãy chia sẻ phương án và cùng trao đổi nhé!
Happy coding.
#DotNet #CSharp #SemaphoreSlim #Concurrency #EcommerceDev #CleanCode #AsyncProgramming DevBlog #CodeSharing #TechLeadership #SystemDesign #SoftwareArchitecture #BackendDevelopment #DeveloperLife
#wecommit100xshare #1percentbetter
/Son Do - ngoài search cũng thích e-commerce :)
Công nghệ - 19/08/2025 21:13:07
Tìm hiểu cách xây dựng hệ thống phát hiện ngôn ngữ ký hiệu theo thời gian thực bằng AI, sử dụng DETR để tăng cường khả năng tiếp cận và đổi mới. Kết nối lời nói và cử chỉ.
Công nghệ - 18/08/2025 13:38:25
Tối ưu hóa các hệ thống RAG bằng cách tận dụng siêu dữ liệu để truy xuất thông tin chính xác và nhanh chóng hơn, giải quyết các thách thức về dữ liệu dư thừa hoặc lỗi thời với công cụ LangExtract nguồn mở. Khám phá cách LangExtract sử dụng các mô hình ngôn ngữ tiên tiến để trích xuất và cấu trúc siêu dữ liệu, tạo ra một quy trình truy xuất hợp lý và hiệu quả.
Công nghệ - 01/08/2025 07:00:00
Gỡ lỗi LLM rất quan trọng vì quy trình làm việc của chúng phức tạp và liên quan đến nhiều phần như chuỗi, lời nhắc, API, công cụ, trình truy xuất, v.v.
Công nghệ - 19/06/2025 03:05:09
Code xong chạy được là chưa đủ – phải biết khi nào nó "chết" nữa chứ 😅
Bạn đang triển khai ứng dụng trên Kubernetes, Docker hay môi trường production nào? Và bạn từng "toát mồ hôi" vì service chết mà không ai báo?
Công nghệ - 16/07/2025 13:41:17
Công nghệ - 27/06/2025 03:15:44
Công nghệ - 11/12/2025 15:05:29
[Góc chuyện nghề] bán account game để đi học nghệ - bạn dám không?
Làm nghề 20 năm, gặp nhiều sinh viên, nhưng chiều qua tôi khá bất ngờ với một cậu em tên Quang. Em Quang muốn theo nghề BA và mong muốn lương 20 triệu sau khi làm việc 1.5 năm tới 2 năm trong nghề.
Công nghệ - 22/09/2025 08:59:20
Dừng ngay việc dùng DateTime.Now trong APIs, đó là ổ lỗi tiềm ẩn trong hệ thống của bạn
⏱️ Tôi từng nghĩ DateTime.Now là một thứ vô hại, đơn giản và tiện lợi, cho đến khi gặp những vấn đề về múi giờ. Những lỗi "tưởng chừng nhỏ" này lại chính là nguồn cơn của sự thất vọng và tốn kém thời gian cho nhiều đội ngũ phát triển.
Công nghệ - 14/03/2025 04:30:32