Let’s assume that you are running two instances of a Tickets Service sharing a MongoDB database.
We receive two updates on the same ticket from a client that will be handled concurrently by the Tickets Service instances thanks to load balancing. For some reason, the request made after the other one updates the ticket before the other could even reach the database thus breaking the original order of the requests. So, the request that should have been handled before is discarded (for example) when adopting an Optimistic Concurrency Control versioning system and creates an inconsistency that could potentially be dangerous for some kind data (like account balances).
How do you solve this kind of problem and guarantee the correct ordering and consistency?
2
Answers
The almost only way to somewhat guarantee this I can think of is to guarantee that user can uses only single client instance (for example via blocking login which will validate there are no other active sessions) at a time and make that client instance assign the ordering to the send requests (i.e. atomically increment and use some counter client side which will be then used server side for correct ordering), which obviously not something you usually want in some accounting system (for example if you are writing some banking system you usually do not want to prevent user from using ATM while using mobile bank client).
If we are talking about using such communication channels like HTTP, TCP/IP, etc. you can’t actually guarantee the ordering even if you have a single service instance because the order can be scrambled long before the requests hit your server (due to transport specifics or even client machine CPU scheduling, in theory), not to mention that single instance still usually processes requests in parallel (though here could be nuances) and is susceptible in general to the same issues but on smaller scale (and can use some other tools for synchronization).
Actually you kind of don’t. You must respect business rules like you can’t withdraw more than there is on account balance + allowed overdraft. If user sends withdraw request which will overdraft the balance over the limit before the deposit request is processed or even acknowledged then it is kind of client problem and the withdraw attempt should be retried.
What (I would argue) you actually want/need is to guarantee that your two instances will not perform "unatomic"/non-synchronized updates on the same data. Usually it is handled via transactions on the database side with appropriate isolation levels. Another approach can be to test for optimistic concurrency violation approach, i.e. (for most relational databases) you can just use query looking something like the following:
And then check if the returned number of updated rows is equal to 1.
If you use the
linearizable
read concern with themajority
write concern, the threads performing queries will appear to be executing queries as if a single thread executed the operations. If you use thereplaceOne
query, it looks like it should be possible to implement optimistic concurrency control as in Guru’s answer: read a document withlinearizable
read concern (include a version in the document, e.g."version": 42
), construct a new document with an incremented version (e.g."version": 43
), and usereplaceOne
withmajority
write concern to ensure that the document will only be updated to this version if a majority of nodes still see the previous version. If the write fails because a majority of nodes have a later version, re-read the document.The Mongo docs note that this will generally not be as performant as other strategies, especially in the case where you have to attempt things multiple times. If a fairly high level of concurrency is expected, it may be worth having your microservice instances form a stateful cluster among themselves and allocate responsibility for operating on a particular ticket or account amongst them. Since one instance is responsible for a given ticket/account/etc., local pessimistic concurrency control can be used to effectively make the operations (including the "modify" part of "read-modify-write") against Mongo on a given document be executed by a single thread: this also can let you safely cache values in the instance’s memory (turning "read-modify-write-read-modify-write…" into "read-modify-write-modify-write…"): depending on how frequently the modifications are being attempted, this may be a major win or a lot of extra complexity. It’s especially common in actor style approaches where there’s tooling to simplify a lot of this (e.g. Service Weaver for golang, Akka.Net, Akka on the JVM, Orleans, or Erlang/Elixir… disclaimer: my employer maintains one of those projects).