thảo luận [Câu hỏi phỏng vấn Interm Node JS] Xử lý collision

  • Người tạo chủ đề Người tạo chủ đề ChuotGreen
  • Ngày bắt đầu Ngày bắt đầu

ChuotGreen

Junior Member
Như tiêu đề, nay em có đi phỏng vấn thực tập công ty thì họ có hỏi 1 câu hỏi mà em ko biết. Câu hỏi của họ là: Giả sử chúng ta bán hàng (ví dụ như vé xe phim), khi mà có 2 người dùng cùng thực hiện cùng một thao tác đặt hàng (giả sử như họ click cùng 1 mốc thời gian chính xác đến tuyệt đối) mà chỉ còn duy nhất 1 mặt hàng thì bạn sẽ xử lý trường hợp này như thế nào?
Mọi người có ai biết thì cho em xin đc chỉ giáo ạ.
 
Cho 2 cái giao dịch đó là không hợp lệ rồi xử lý những giao dịch khác tiếp được không?
 
Như tiêu đề, nay em có đi phỏng vấn thực tập công ty thì họ có hỏi 1 câu hỏi mà em ko biết. Câu hỏi của họ là: Giả sử chúng ta bán hàng (ví dụ như vé xe phim), khi mà có 2 người dùng cùng thực hiện cùng một thao tác đặt hàng (giả sử như họ click cùng 1 mốc thời gian chính xác đến tuyệt đối) mà chỉ còn duy nhất 1 mặt hàng thì bạn sẽ xử lý trường hợp này như thế nào?
Mọi người có ai biết thì cho em xin đc chỉ giáo ạ.

Bác intern công ty nào đó mà họ hỏi cách giải quyết vấn đề rồi

Gửi từ Xiaomi 220333QAG bằng vozFApp
 
2 request đặt hàng kể cả cùng lúc tuyệt đối thì nó sẽ đến server trên 2 process khác nhau, nên vẫn có chênh lệch thời gian. Nếu làm đúng thì đặt table constraint kiểu available_stock >= 0 thì chỉ có 1 transaction là thành công, transaction còn lại sẽ bị fail => 1 trong 2 ông bị xui thôi :D.
 
Như tiêu đề, nay em có đi phỏng vấn thực tập công ty thì họ có hỏi 1 câu hỏi mà em ko biết. Câu hỏi của họ là: Giả sử chúng ta bán hàng (ví dụ như vé xe phim), khi mà có 2 người dùng cùng thực hiện cùng một thao tác đặt hàng (giả sử như họ click cùng 1 mốc thời gian chính xác đến tuyệt đối) mà chỉ còn duy nhất 1 mặt hàng thì bạn sẽ xử lý trường hợp này như thế nào?
Mọi người có ai biết thì cho em xin đc chỉ giáo ạ.
locking
 
Thật ra đây là một câu hỏi khá khó và phức tạp tùy thuộc vào ngữ cảnh bài toán. Tuy nhiên với trường hợp là phỏng vấn intern bạn có thể đưa ra vài giải pháp đơn giản như sau:

1. Atomic update:
Mã:
UPDATE product SET stock = stock-1 WHERE product_id = ? AND stock > 0
Với câu query như thế này thì luôn đảm bảo sẽ không có collision ở level DB. Sau câu update bạn có thể check giá trị updatedCount trả về để verify xem việc update có thành công hay không.
Có thể nó đây là cách tốt nhất trong trường hợp đơn giản, bất lợi là nếu phức tạp hơn chút thì cách này không làm được.

2. Transaction:
Cách này thì đơn giản bạn có thể sử dụng transaction trong DB để lock table / row liên quan lại, xử lý logic như bình thường sau đó confirm transaction, nếu fail thì rollback. Đại loại như vầy (mã giả):
Mã:
db.beginTransaction()
try {
    product = db.query("select * from product where id = ?", product_id)
    if (product.stock <= 0) throw Exception()
    db.query("update product set stock = ? where id = ?", product.stock - 1, product.id)
    db.commitTransaction()
}
catch() {
    db.rollback()
}

Lợi thế là dùng transaction đáp ứng được hầu như mọi logic xử lý dù phức tạp đến đâu. Bất lợi là performance kém đến rất kém (vì lock), và rất nhiều trường hợp biên phức tạp do đặc thù của bài toán locking.

3. Stored procedure:
Tương tự như cách trên nhưng bạn viết hẳn luôn một cái stored procedure trong DB xử lý hết các logic. Sau đó trong code chỉ việc gọi đến cái SP này là xong:
Mã:
// Mình chả bh viết SP nên không biết viết T_T
BEGIN TRANSACTION
SET product = SELECT....
UPDATE product SET product = product.stock - 1 WHERE...
END TRANSACTION

Cách này thì y chang như cách transaction, chỉ là level của code nằm ở chỗ khác nhau thôi. Lợi thế là performance có thể tốt hơn 1 chút xíu vì giảm chi phí network. Bất lợi là code nằm ở trong DB thì khó hiểu, khó bảo trì, khó implement các logic phức tạp và hao tốn tài nguyên của process DB.

Với các ngữ cảnh nâng cao hơn (Non transactional, hệ thống phân tán,...) thì đây sẽ là một bài toán khó và các cách trên sẽ không hiệu quả.
 
Thật ra đây là một câu hỏi khá khó và phức tạp tùy thuộc vào ngữ cảnh bài toán. Tuy nhiên với trường hợp là phỏng vấn intern bạn có thể đưa ra vài giải pháp đơn giản như sau:

1. Atomic update:
Mã:
UPDATE product SET stock = stock-1 WHERE product_id = ? AND stock > 0
Với câu query như thế này thì luôn đảm bảo sẽ không có collision ở level DB. Sau câu update bạn có thể check giá trị updatedCount trả về để verify xem việc update có thành công hay không.
Có thể nó đây là cách tốt nhất trong trường hợp đơn giản, bất lợi là nếu phức tạp hơn chút thì cách này không làm được.

2. Transaction:
Cách này thì đơn giản bạn có thể sử dụng transaction trong DB để lock table / row liên quan lại, xử lý logic như bình thường sau đó confirm transaction, nếu fail thì rollback. Đại loại như vầy (mã giả):
Mã:
db.beginTransaction()
try {
    product = db.query("select * from product where id = ?", product_id)
    if (product.stock <= 0) throw Exception()
    db.query("update product set stock = ? where id = ?", product.stock - 1, product.id)
    db.commitTransaction()
}
catch() {
    db.rollback()
}

Lợi thế là dùng transaction đáp ứng được hầu như mọi logic xử lý dù phức tạp đến đâu. Bất lợi là performance kém đến rất kém (vì lock), và rất nhiều trường hợp biên phức tạp do đặc thù của bài toán locking.

3. Stored procedure:
Tương tự như cách trên nhưng bạn viết hẳn luôn một cái stored procedure trong DB xử lý hết các logic. Sau đó trong code chỉ việc gọi đến cái SP này là xong:
Mã:
// Mình chả bh viết SP nên không biết viết T_T
BEGIN TRANSACTION
SET product = SELECT....
UPDATE product SET product = product.stock - 1 WHERE...
END TRANSACTION

Cách này thì y chang như cách transaction, chỉ là level của code nằm ở chỗ khác nhau thôi. Lợi thế là performance có thể tốt hơn 1 chút xíu vì giảm chi phí network. Bất lợi là code nằm ở trong DB thì khó hiểu, khó bảo trì, khó implement các logic phức tạp và hao tốn tài nguyên của process DB.

Với các ngữ cảnh nâng cao hơn (Non transactional, hệ thống phân tán,...) thì đây sẽ là một bài toán khó và các cách trên sẽ không hiệu quả.
Bác đỉnh quá. Không biết khi nào mới đc như bác 🤣 Em cảm ơn bác nhiều ạ
 
Cái này người hỏi muốn check tư duy bạn thôi, nhưng hỏi chưa đủ.
Để phân tích đầy đủ theo thực tế thì: 2 người cùng click thì kiểu gì cũng phải chênh nhau 0.00000x giây nên về mặt thực tế không có 2 request chính xác cũng 1 thời điểm được. Giả sử nếu được thì nữa thì xét về cấp độ phần cứng, card mạng mỗi lần chỉ process 1 gói tin tại 1 thời điểm, nên sẽ có request vào trước và 1 request vào sau, không có việc 2 request vào đồng thời.

Thường với câu hỏi này, nếu hỏi rõ thì sẽ là "nếu logic xử lí đặt hàng process lâu (tầm 1,2s), thì việc request tới sau tranh giành update tài nguyên của request tới trước phải xử lí ntn". Câu trả lời là áp dụng cơ chế lock, request tới sau phải chờ request tới trước thực hiện xong thì nó mới được chạm vào resource đó.

Giờ tiếp theo là solution, lock thế nào, lock cái gì & ở tầng nào.
- Lock ở tầng Application: một số ngôn ngữ bản chất đa luồng như C# Java đều hỗ trợ keyword lock, nodejs thì việc ăn race condition cũng ít hơn do bản chất đơn luồng & chạy non-blocking, nhưng vẫn có khả năng dính chấu, nên cần thiết vẫn phải lock resource (bằng các thể loại thư viện như async-lock, mutex). Lock xong rồi thì thằng tới sau phải đợi thằng tới trước chạy xong, ai đặt vé sau 0,00001s thì bị dính lỗi hết vé => xong chuyện.

- Lock ở tầng SQL, khi và chỉ khi bạn đang dùng SQL Database, không dùng ORM & thích chạy code SQL thuần, thì đơn giản bạn chỉ cần bọc đoạn code thực thi logic đặt vé trong 1 transaction (
BEGIN TRANSACTION
--- (execute logic)
COMMIT TRANSACTION
), vì bản chất transaction sẽ tự động lock table/ row khi nó thực thi query/ DML trên đó nên request tới sau gọi sql sẽ đợi sql của request trước chạy xong mới chạy.

Cơ bản nó là vậy đấy =))
 
Thật ra đây là một câu hỏi khá khó và phức tạp tùy thuộc vào ngữ cảnh bài toán. Tuy nhiên với trường hợp là phỏng vấn intern bạn có thể đưa ra vài giải pháp đơn giản như sau:

1. Atomic update:
Mã:
UPDATE product SET stock = stock-1 WHERE product_id = ? AND stock > 0
Với câu query như thế này thì luôn đảm bảo sẽ không có collision ở level DB. Sau câu update bạn có thể check giá trị updatedCount trả về để verify xem việc update có thành công hay không.
Có thể nó đây là cách tốt nhất trong trường hợp đơn giản, bất lợi là nếu phức tạp hơn chút thì cách này không làm được.

2. Transaction:
Cách này thì đơn giản bạn có thể sử dụng transaction trong DB để lock table / row liên quan lại, xử lý logic như bình thường sau đó confirm transaction, nếu fail thì rollback. Đại loại như vầy (mã giả):
Mã:
db.beginTransaction()
try {
    product = db.query("select * from product where id = ?", product_id)
    if (product.stock <= 0) throw Exception()
    db.query("update product set stock = ? where id = ?", product.stock - 1, product.id)
    db.commitTransaction()
}
catch() {
    db.rollback()
}

Lợi thế là dùng transaction đáp ứng được hầu như mọi logic xử lý dù phức tạp đến đâu. Bất lợi là performance kém đến rất kém (vì lock), và rất nhiều trường hợp biên phức tạp do đặc thù của bài toán locking.

3. Stored procedure:
Tương tự như cách trên nhưng bạn viết hẳn luôn một cái stored procedure trong DB xử lý hết các logic. Sau đó trong code chỉ việc gọi đến cái SP này là xong:
Mã:
// Mình chả bh viết SP nên không biết viết T_T
BEGIN TRANSACTION
SET product = SELECT....
UPDATE product SET product = product.stock - 1 WHERE...
END TRANSACTION

Cách này thì y chang như cách transaction, chỉ là level của code nằm ở chỗ khác nhau thôi. Lợi thế là performance có thể tốt hơn 1 chút xíu vì giảm chi phí network. Bất lợi là code nằm ở trong DB thì khó hiểu, khó bảo trì, khó implement các logic phức tạp và hao tốn tài nguyên của process DB.

Với các ngữ cảnh nâng cao hơn (Non transactional, hệ thống phân tán,...) thì đây sẽ là một bài toán khó và các cách trên sẽ không hiệu quả.
Bác có thể chia sẻ môt số cách để thực hiện với hệ thống phân tán không ạ.
:/ em cũng đang gặp vấn đề này dùng lock nhưng không giải quyết được vấn đề performance.
 
Bác có thể chia sẻ môt số cách để thực hiện với hệ thống phân tán không ạ.
:/ em cũng đang gặp vấn đề này dùng lock nhưng không giải quyết được vấn đề performance.

Hệ thống phân tán thì dùng event ordering ( lamport clock, vector/matrix clock)
Tuy nhiên ko rõ lắm bài toán cụ thể của bạn là gì
 
Bác có thể chia sẻ môt số cách để thực hiện với hệ thống phân tán không ạ.
:/ em cũng đang gặp vấn đề này dùng lock nhưng không giải quyết được vấn đề performance.
Với hệ thống phân tán thì sẽ không có 1 giải pháp nào là tối ưu mà sẽ tùy vào từng trường hợp mà ta chọn / phối hợp các giải pháp sao cho tối ưu nhất. Vài ý tưởng đơn giản có thể để trả lời phỏng vấn:

1. Atomic update:
Nói chung luôn dùng atomic update nếu có thể, đây luôn là một giải pháp đơn giản, tối ưu và tiết kiệm. Ta có thể setup một sử dụng một cụm Redis cho việc lưu trữ / xử lý stock của đơn hàng. Thêm một lua script để xử lý update:

Mã:
let val = tonumber(redis.call(get, KEYS[1]))
if (val <= 0) return -1
return redis.call("incr", KEYS[1], -1)

Dù là LUA script thì redis vẫn rất rất nhanh so với các DB khác, Redis là single-threaded nên luôn đảm bảo không bao giờ có xung đột (kể cả trong hệ thống clustering). Bất lợi là việc tách logic xử lý stock ra một DB riêng sẽ kéo theo nhiều khó khăn khác về technical, redis cũng có rủi ro trong việc mất mát dữ liệu (dù rất rất hiếm).

2. Distributed lock:
Vẫn là lock, nhưng trong hệ thống phân tán thì cao cấp hơn. Implement bằng Redis luôn cho dễ.

Lợi điểm là khả thi với hầu như mọi tình huống, khi mà không thể dùng atomic update thì có thể switch ngay qua phương án này.
Bất lợi là performance kém. Nhiều trường hợp biên cần xử lý cho bài toán global locking. Vì tự xử lý nên dễ gặp deadlock.

3. Queueing:
Việc xử lý các tác vụ collision sẽ được thực hiện bằng worker và xử lý single threaded nhằm tránh đụng độ. Ta có thể sử dụng các MQ đơn giản như RabbitMQ hoặc Redis luôn. Để scalable ta có thể hash task theo collision key và scale số lượng worker tương ứng

Lợi điểm là có thể implement các logic phức tạp, scalable, ít điều kiện biên.
Bất lợi là luồng xử lý bị băm nhỏ ra thành các giai đoạn, khó quản lý toàn luồn (có thể dùng các tool khác để quản lý flow - nhưng nằm ngoài phạm vi câu hỏi). Ngoài ra đôi khi hệ thống không sẵn sàng để phân tách như vậy, chi phí thực hiện lớn.
 
Hệ thống phân tán thì dùng event ordering ( lamport clock, vector/matrix clock)
Tuy nhiên ko rõ lắm bài toán cụ thể của bạn là gì
Em.có 1 bảng để lưu tổng amount các transaction của 1 cửa hàng. Có cả amount <0.
Đúng ra thì chả cần tạo bảng để tính tổng lúc tạo transaction làm gì ( lúc nào đó cần thì query tổng theo một điều kiện nào đó ) nhưng vì lý do nghiệp vụ nên cần phải có thêm bảng đó bác ạ.
Cám ơn bác nhé.
Với hệ thống phân tán thì sẽ không có 1 giải pháp nào là tối ưu mà sẽ tùy vào từng trường hợp mà ta chọn / phối hợp các giải pháp sao cho tối ưu nhất. Vài ý tưởng đơn giản có thể để trả lời phỏng vấn:

1. Atomic update:
Nói chung luôn dùng atomic update nếu có thể, đây luôn là một giải pháp đơn giản, tối ưu và tiết kiệm. Ta có thể setup một sử dụng một cụm Redis cho việc lưu trữ / xử lý stock của đơn hàng. Thêm một lua script để xử lý update:

Mã:
let val = tonumber(redis.call(get, KEYS[1]))
if (val <= 0) return -1
return redis.call("incr", KEYS[1], -1)

Dù là LUA script thì redis vẫn rất rất nhanh so với các DB khác, Redis là single-threaded nên luôn đảm bảo không bao giờ có xung đột (kể cả trong hệ thống clustering). Bất lợi là việc tách logic xử lý stock ra một DB riêng sẽ kéo theo nhiều khó khăn khác về technical, redis cũng có rủi ro trong việc mất mát dữ liệu (dù rất rất hiếm).

2. Distributed lock:
Vẫn là lock, nhưng trong hệ thống phân tán thì cao cấp hơn. Implement bằng Redis luôn cho dễ.

Lợi điểm là khả thi với hầu như mọi tình huống, khi mà không thể dùng atomic update thì có thể switch ngay qua phương án này.
Bất lợi là performance kém. Nhiều trường hợp biên cần xử lý cho bài toán global locking. Vì tự xử lý nên dễ gặp deadlock.

3. Queueing:
Việc xử lý các tác vụ collision sẽ được thực hiện bằng worker và xử lý single threaded nhằm tránh đụng độ. Ta có thể sử dụng các MQ đơn giản như RabbitMQ hoặc Redis luôn. Để scalable ta có thể hash task theo collision key và scale số lượng worker tương ứng

Lợi điểm là có thể implement các logic phức tạp, scalable, ít điều kiện biên.
Bất lợi là luồng xử lý bị băm nhỏ ra thành các giai đoạn, khó quản lý toàn luồn (có thể dùng các tool khác để quản lý flow - nhưng nằm ngoài phạm vi câu hỏi). Ngoài ra đôi khi hệ thống không sẵn sàng để phân tách như vậy, chi phí thực hiện lớn.
Cám ơn bác :v
Về phần bất lợi và có lợi của phương pháp thứ 3. Đấy là bác tự suy ra hay là có bài viết nào về vấn đề đó không nhỉ.
 
Sửa lần cuối:
Em.có 1 bảng để lưu tổng amount các transaction của 1 cửa hàng. Có cả amount <0.
Đúng ra thì chả cần tạo bảng để tính tổng lúc tạo transaction làm gì ( lúc nào đó cần thì query tổng theo một điều kiện nào đó ) nhưng vì lý do nghiệp vụ nên cần phải có thêm bảng đó bác ạ.
Cám ơn bác nhé.

Cám ơn bác :v
Về phần bất lợi và có lợi của phương pháp thứ 3. Đấy là bác tự suy ra hay là có bài viết nào về vấn đề đó không nhỉ.
Kinh nghiệm thôi bác.
 

Thống kê chủ đề

Ngày tạo
ChuotGreen,
Người trả lời cuối
Niels Henrik Abel,
Trả lời
24
Lượt xem
5.116
Quay lại
Lên đầu trang