Search code examples
mysqlspringjpaspring-data-jpapessimistic-locking

Spring Data JPA - Pessimistic Locking Not Working


Using: Spring Boot 2.3.3, MySQL 5.7(currently via TestContainers), JUnit 5

I have a JpaRepository inside a Spring MVC application that has a method set to be @Lock(LockModeType.PESSIMISTIC_WRITE) and, while I do see the SELECT ... FOR UPDATE coming up in the resulting SQL, it doesn't seem to do much of anything.

I'll put the code below, but, if I try to spin up multiple threads that make the same call, each thread is able to read the same initial value in question and nothing ever seems to block/wait. And my understanding is that any "additionally" called methods that are also @Transactional (from the org.springframework.transaction namespace) are made part of the original transaction.

I can't figure out what I'm doing wrong. Any assistance would be appreciated, even if it means pointing out that my understanding/expectations are flawed.

Repository

public interface AccountDao extends JpaRepository<Account, Long> {
  @Lock(LockModeType.PESSIMISTIC_WRITE)
  public Optional<Account> findById(Long id);
}

Services

Account Service

@Service
public class AccountServiceImpl implements AccountService {
  @Autowired
  private FeeService feeService;

  @Override
  @Transactional // have also tried this with REQUIRES_NEW, but the same results occur
  public void doTransfer(Long senderId, Long recipientId, TransferDto dto) {
    // do some unrelated stuff

    this.feeService.processFees(recipientId);
  }
}

Fee Service

@Service
public class FeeServiceImpl implements FeeService {
  @Autowired
  private AccountDao accountDao;

  @Override
  @Transactional // have also tried removing this
  public void processFees(Long recipientId) {
    // this next line is actually done through another service with a @Transactional annotation, but even without that annotation it still doesn't work
    Account systemAccount = this.accountDao.findById(recipientId);

    System.out.println("System account value: " + systemAccount.getFunds());

    systemAccount.addToFunds(5);

    System.out.println("Saving system account value: " + systemAccount.getFunds());
  }
}

Test

public class TheTest {
  // starts a @SpringBootTest with ```webEnvironment = WebEnvironment.RANDOM_PORT``` so it should start up a dedicated servlet container

  // also auto configures a WebTestClient

  @Test
  @Transactional
  public void testLocking() {
    // inserts a bunch of records to have some users and accounts to test with and does so via JPA, hence the need for @Transactional

    // code here to init an ExecutorService and a synchronized list

    // code here to create a series of threads via the ExecutorService that uses different user IDs as the sender, but the same ID for the recipient, hence the need for pessimistic locking
  }
}

I can put in the testing code if necessary, but, I'm not sure what other details are necessary.

The resulting output (especially from the System.out.println calls in FeeServiceImpl) shows that the same "system account" value is read in across all threads, and the saved value is, therefore, also always the same.

When the application starts up, that value is 0, and all threads read that 0, with no apparent locking or waiting. I can see multiple transactions starting up and committing (I increased the logging level on Hibernate's TransactionImpl), but, it doesn't seem to matter.

Hopefully I'm overlooking or doing something dumb, but, I can't quite figure out what it is.

Thank you!


Solution

  • Of course, it was something buried that I wasn't expecting.

    It turns out my tables had been created using MyISAM instead of InnoDB, oddly, since that hasn't been the default for table creation in MySQL in a long time.

    So, here's what I did:

    1. I thought I was using MySQL 8.0. Turns out TestContainers defaults (to 5.7.22 in my case) when using a JDBC connection string that doesn't specifically name the version. So I fixed that.

    2. This still didn't fix things as MyISAM was still being used. It turns out this was because I had a legacy dialect setting in my configuration. Updating that to something like MySQL57Dialect corrected that.

    This actually also explains the "weird" behaviour I was seeing in my JUnit tests, as values were popping into the DB right away and not rolling back, etc.

    I hope this helps someone else in the future!