Hacker News new | ask | show | jobs
by lmm 2024 days ago
> The problem we now have is called write skew. Our reads from the inventory view can be out of date by the time the checkout event is processed. If two users try to buy the same item at nearly the same time, they will both succeed, and we won’t have enough inventory for them both.

And you'll have exactly the same problem if you're using a traditional ACID database: the user saw the item as being available, clicked buy, but it was unavailable by the they went to get it. Using an ACID database doesn't gain you anything; you might as well just use Kafka for everything.

2 comments

The A in ACID literally stands for atomicity. If you're using an ACID database that can't guarantee that an item is available for purchase at the time it's purchased, you're using a bad database.

The user having stale data in their browser and finding an item has already been purchased is very different from the database, within a transaction, allowing an item which has already been purchased to be purchased again.

> The user having stale data in their browser and finding an item has already been purchased is very different from the database, within a transaction, allowing an item which has already been purchased to be purchased again.

This is like "the operation was a success, but the patient died". What matters is the user-facing behaviour of your whole system; a transaction that can't actually cover the parts the user cares about is pointless.

You're misunderstanding the problem. If I press "buy now" and it says "success", I don't want to receive an email later saying "oops, we didn't actually have any in stock, we'll send you a refund" because another order was sitting in a queue. That's the failure mode here. The staleness of what's in the browser is an orthogonal concern unrelated to the database or use of Kafka.
It's very much related. You have one item in stock. Two users see the item as available in their browsers and click "buy now". That's the problem that you actually have to solve, and database transactions don't help you solve it: whether you're using a transactional datastore or not you have to do pretty much the same thing when the user clicks "buy now": issue an attempt to buy it, wait for that attempt to be confirmed/denied, and handle both cases.

And once you've solved that problem you don't need or want ACID transactions because they don't actually do anything for you. Order confirmation emails have the same problem as the browser: you can't (or at least shouldn't) actually hold a database-level transaction open while you connect to an email server, so you have to do something like recording a queue of email confirmations that are ready to be sent - exactly the same thing you do when using Kafka.

You're still missing the point. You're saying:

> issue an attempt to buy it, wait for that attempt to be confirmed/denied, and handle both cases.

Only one user should receive a success. The article describes a setup where both users receive a success because of write skew. This is the very problem that transactions avoid: locking the row storing the number of items in stock so that it cannot be decremented below zero.

You can still use Kafka. What the article is saying is that you can't just check the DB to see if items are in stock and write your order to a queue. Because there might already be an order for the same item in the queue which makes your new order invalid.

Literally the whole point of ACID databases is that multiple clients can operate on the same set of data and avoid putting it into a bad state. If I'm a bank storing an account balance of $20 and you withdraw $20 and I withdraw $20, only one of us should get $20. This is the same problem.

> Only one user should receive a success. The article describes a setup where both users receive a success because of write skew.

It describes a setup where neither user bothers to check whether they had a success because the authors are affecting not to understand how to use Kafka.

> What the article is saying is that you can't just check the DB to see if items are in stock and write your order to a queue.

You can't do that with a database either. You have to actually handle the case where it fails. It's not actually any easier than doing it properly with Kafka.

> Literally the whole point of ACID databases is that multiple clients can operate on the same set of data and avoid putting it into a bad state. If I'm a bank storing an account balance of $20 and you withdraw $20 and I withdraw $20, only one of us should get $20.

Banks don't actually use ACID transactions for that, because they don't work in the real world; the bank needs to have a record of both your attempts to withdraw $20, not for one of you to see an error. What a bank ends up implementing is much the same as what you implement when using Kafka.

The ACID system can guarantee nobody is billed for an item that you can't deliver. If you want a more user-friendly guarantee, you can reserve it when it's added to the cart.
> If you want a more user-friendly guarantee, you can reserve it when it's added to the cart.

If you open an ACID transaction when the user adds something to the cart and don't close it until they check out, you'll find your database gets locked up pretty quickly. So you can't actually use the ACID transactions to implement the behaviour you want - you have to implement some kind of reserve/commit semantics in userspace, whether you're using an ACID database or not.

You don't need to hold the transaction open the entire time, you just need the inventory count to be correct.

You track the inventory and reservations. Taking a reservation checks that inventory is available. With row-level locking, only that inventory item is locked. If that fails, it can search for timed out reservations, update the inventory and try again.

If it succeeds, it decrements the inventory then adds a reservation. At that point, the transaction can close, and in the common case, you only held locks long enough to update a row in inventory and add a row to reservations.

You still have to handle the case where your transaction fails though. So you actually end up writing the same thing you'd do if you didn't have transactions: you issue an attempt to reserve, wait for the response to that attempt (whether that's transaction commit succeeding/failing or a queue processor processing), and handle both possible results. The transaction support doesn't actually help you because it's at the wrong level to be useful.