I have a service with one method:
@Service public class DefaultTestService implements TestService { private static final Logger LOGGER = Logger.getLogger(DefaultTestService.class); @Autowired private TestRepository testRepository; @Transactional(readOnly = false, isolation = Isolation.SERIALIZABLE) @Override public void incrementAndGet(Long testModelId) { LOGGER.debug("Transaction is active: " + TransactionSynchronizationManager.isActualTransactionActive()); final TestModel tm = testRepository.findOne(testModelId); if (tm != null) { LOGGER.debug("Updated " + testModelId + " from value: " + tm.getValue()); tm.setValue(tm.getValue() + 1); testRepository.save(tm); } else { LOGGER.debug("Saved with id: " + testModelId); final TestModel ntm = new TestModel(); ntm.setId(testModelId); testRepository.save(ntm); } } }
And I’m running Gatling with 2 parallel calls config with testModelId = 1L
argument.
As a result of these calls I’m getting error:
org.postgresql.util.PSQLException: ERROR: duplicate key value violates unique constraint "test_model_pkey"
What I can see from logs is that two calls have entered this method at once and each printed log
"Saved with id: 1" "Saved with id: 1"
I assumed that adding transaction annotation on this method would block one of the calls on the line testRepository.findOne(testModelId)
until other call finishes its execution, but as I can see from logs it’s working in a different way.
So my question is how transaction works in this case when concurrent access appears? And how can I handle this case with concurrent access?
Advertisement
Answer
A transaction means that all modifications of persistent objects performed within the boundary of the transaction will either :
- be commited at the end of the transaction (i.e. all modification are persisted in the DB)
- be rollbacked at the end of the transaction (i.e. none of the modifications are persisted in the DB) That’s all.
How transaction works in this case ?
One of the 2 threads reach the end of the transaction and commit successfully. The other thread reach the end of the transaction and fail to commit due to constraint violation, so the second transaction terminate in “rollback” state.
Why findOne
isn’t blocked in the second transaction ?
Simply because, despite the SERIALIZABLE transaction level, there is no row to lock. findOne
returns no results in both transactions and nothing get locked (of course if first transaction is commited before that the second transaction execute findOne
: it’s another story).
How to handle concurrent transaction in your particular case (i.e. constraint violation on PK while inserting new rows) ?
The most common strategy is to let the database assign the id to new rows -with the help of a sequence-
(As an experiment, you can try to set the isolation level to READ_UNCOMMITED so that second transaction may read uncommited changes from first transaction. I’m not sure you notice any difference because if findOne
in second transaction is executed before testRepository.save(ntm);
from the first transaction it will still return no results)
How to handle transaction rollback due to concurrent modification in general ?
It really depends on your use case. Basically you have the choice between:
- catching the exception and “retry” the operation.
- throwing the exception to caller (probably to display a gentle error message to the user).
Be aware that if the transaction terminate in a rollback state : the graph of persistent objects modified during the transaction is not reverted to it’s original state.
Please note that using isolation level SERIALIZABLE can cause huge performance issues and is generally used only for critical and occasional transaction.