Search code examples
javamicroservicesspring-cloudreplicationrace-condition

Micro services - race condition between multiple service replica


I am learning micro services - Spring Cloud now. During the study, I am thinking how to deal with the race condition in the following cases.

Let's say I have a "ProductOrder" service, which has 2 replicas. Now there are two order requests coming and the load balancer assigns the api request one for each replica. Note that they are sharing the same db (so I am not talking about the data synchronization. Or maybe I am?). So how can the service make sure the storage still have enough products to meet the ordering requests.

I know key word "synchronized" in java is for multi threading. However, the replicas are not multi-threading but multi process. Am I correct? Any ideas?


Solution

  • The database will have a table like Product(id, amount). Placing order will need to updat e the record for that product.

    And the implementation for that will look something like this. First the application will read the product amount from the DB:

    SELECT id, amount
    FROM Product
    WHERE id = {some_id}
    FOR UPDATE;
    

    Then application code will check if there is a requested amount. And then it will update the value in the DB:

    UPDATE Product
    SET amount = {new_amount}
    WHERE id = {some_id}
    

    Note that:

    1. all these should happen in one DB transaction
    2. the first query uses FOR UPDATE clause to prevent other concurrent transactions from modification of this product while our transaction is running. Think of it as a synchronized but not on JVM level but on DB level.
    3. if another user will try to buy the same product the transaction for that user (lets call it T2) will try to do the same and the first step is to query the record with lock. That will block transaction T2 until the record is unblocked and that will happen when the transaction that currently holds the lock either commits or rolls back. At that point T2 will proceed and it will see up-to-date value for the product if it was changed by T1.

    This approach uses pessimistic lock, that is we lock the record we want to update for the whole duration of the transaction.

    There is another approach - optimistic.

    Optimistic lock requires that you have a version column in the Product table. Then the first query does not lock the record in the DB but we get the version of the record:

    SELECT id, amount, version
    FROM Product
    WHERE id = {some_id}
    

    The check in application works the same way as in pessimistic scenario. But the final update is different:

    UPDATE Product
    SET amount = {new_amount}
    WHERE id = {some_id}
       and version = {version_we_read_initially}
    

    Update operation always returns the number of updated records. If we got 0 it means that the record was changed while our transaction was run. It means that the amount we saw initially (in the first query) may have been changed. In order to proceed we need to retry the whole operation from step 1, that is read the amount and version for the product again, check that there is the required amount and try to update the amount again.

    Of cause there should be some limit on how many times we are going to retry. On busy system for the product that is in demand this may become a problem.