javaspring-bootconcurrency

Does Transactional on READ_COMMITTED prevent two requests from overwriting each others changes?


Lets say I have an entity lottery ticket. It has the winning_ticket set to true and the winner set to null. I have some code draw_ticket(Player player, Ticket ticket). It should throw an exception if winner is already set, otherwise set winner to the player. It should also call winner.balance+=100 then save the entity.

Here's the pseudocode

@Transactional(rollbackOn=Exception.class)
public void drawTicket(UUID playerId, UUID ticketId){
    var player=playerRepository.findById(playerId);
    var ticket=playerRepository.findById(ticketId);

    if(ticket.winner==null)
        ticket.winner=player
    else
        throw new TicketAlreadyRedeemedException()

    if(ticket.winningTicket==true)
        player.balance+=100
        
    playerRepository.save(player)
    ticketRepository.save(ticket)
}

Two requests happen simultaneously

The function is protected by Transactional and READ_COMMITTED

Will only one player be reliably credited? Is marking the function as transactional enough to prevent race conditions?

It seems obvious to me that READ_COMMITTED should protect me and reliably prevent race conditions, but chatGPT claims it doesn't and that transactions literally only undo changes if an exception happens.


Solution

  • It seems obvious to me that READ_COMMITTED should protect me and reliably prevent race conditions

    Why would it? What's your assumption here?

    You didn't specify what database, but let's assume postgres:

    Read Committed is the default isolation level in PostgreSQL. When a transaction runs on this isolation level, a SELECT query sees only data committed before the query began and never sees either uncommitted data or changes committed during query execution by concurrent transactions. (However, the SELECT does see the effects of previous updates executed within this same transaction, even though they are not yet committed.) Notice that two successive SELECTs can see different data, even though they are within a single transaction, when other transactions commit changes during execution of the first SELECT.
    If a target row found by a query while executing an UPDATE statement (or DELETE or SELECT FOR UPDATE) has already been updated by a concurrent uncommitted transaction then the second transaction that tries to update this row will wait for the other transaction to commit or rollback. In the case of rollback, the waiting transaction can proceed to change the row. In the case of commit (and if the row still exists; i.e. was not deleted by the other transaction), the query will be re-executed for this row to check that the new row version still satisfies the query search condition. If the new row version satisfies the query search condition then the row will be updated (or deleted or marked for update). Note that the starting point for the update will be the new row version; moreover, after the update the doubly-updated row is visible to subsequent SELECTs in the current transaction. Thus, the current transaction is able to see the effects of the other transaction for this specific row.

    https://www.postgresql.org/docs/7.2/xact-read-committed.html

    In your case "the query will be re-executed for this row to check that the new row version still satisfies the query search condition" is still true.

    What you can do: