spring-boot-service 访问存储库中的 SELECT 和更新顺序错误

wrong sequence of SELECT's and UPDATES in spring-boot-service accessing repository

我有所有简单的 classes,但是 spring-boot-service 和存储库有问题。 就像我有一个测试 class 有以下测试和必要的方法 execut():

@Test
public void deposit() throws Exception {
    long balance = accountService.getBalance(accountNr, pin);
    execute(() -> accountService.deposit(accountNr, amount), INVOCATIONS);
    long newBalance = accountService.getBalance(accountNr, pin);
    assertEquals(balance + INVOCATIONS * amount, newBalance);
}

public static void execute(Task task, int times) throws InterruptedException 
{
        ExecutorService executorService = Executors.newCachedThreadPool();
        for (int i = 0; i < times; i++) {
            executorService.submit(() -> {
                try {
                    task.run();
                } catch (Exception ex) {
                    throw new RuntimeException(ex);
                }
            });
        }
        executorService.shutdown();
        executorService.awaitTermination(1, TimeUnit.HOURS);
    }

然后是一个非常简单的实体,有四个属性:

@Entity
public class Account {

    @Id
    @GeneratedValue
    private Integer nr;

    @Version
    private Integer version;
    private String pin;
    private long balance;
...

该服务有一个方法,首先搜索一个帐户,修改一个值,并尝试将其存储在数据库中:

public void deposit(int accountNr, long amount) throws InvalidCredentials, InvalidTransaction {
    Account account = accountRepository.getAccountByNr(accountNr);
    account.deposit(amount);
    accountRepository.saveAndFlush(account);
}

当我现在执行测试时,SELECT 和 UPDATES 混淆了,以至于最后数据库中没有正确的值。

然后我用@Transactional(isolation = Isolation.READ_COMMITTED, propagation = Propagation.REQUIRES_NEW) 提供了服务方法,这也没有帮助。

有人知道吗?

spring-banner 后的日志输出为:

 INFO 20320 --- [           main] o.e.b.a.AccountServiceConcurrentIT       : Started AccountServiceConcurrentIT in 9.063 seconds (JVM running for 11.324)
DEBUG 20320 --- [           main] org.hibernate.SQL : select nextval ('hibernate_sequence')
DEBUG 20320 --- [           main] org.hibernate.SQL : insert into account (balance, pin, version, nr) values (?, ?, ?, ?)
DEBUG 20320 --- [           main] org.hibernate.SQL : select account0_.balance as col_0_0_ from account account0_ where account0_.nr=? and account0_.pin=?
DEBUG 20320 --- [pool-1-thread-8] org.hibernate.SQL : select account0_.nr as nr1_0_, account0_.balance as balance2_0_, account0_.pin as pin3_0_, account0_.version as version4_0_ from account account0_ where account0_.nr=?
DEBUG 20320 --- [pool-1-thread-2] org.hibernate.SQL : select account0_.nr as nr1_0_, account0_.balance as balance2_0_, account0_.pin as pin3_0_, account0_.version as version4_0_ from account account0_ where account0_.nr=?
DEBUG 20320 --- [pool-1-thread-6] org.hibernate.SQL : select account0_.nr as nr1_0_, account0_.balance as balance2_0_, account0_.pin as pin3_0_, account0_.version as version4_0_ from account account0_ where account0_.nr=?
DEBUG 20320 --- [pool-1-thread-9] org.hibernate.SQL : select account0_.nr as nr1_0_, account0_.balance as balance2_0_, account0_.pin as pin3_0_, account0_.version as version4_0_ from account account0_ where account0_.nr=?
DEBUG 20320 --- [pool-1-thread-2] org.hibernate.SQL : update account set balance=?, pin=?, version=? where nr=?
DEBUG 20320 --- [pool-1-thread-6] org.hibernate.SQL : update account set balance=?, pin=?, version=? where nr=?
DEBUG 20320 --- [pool-1-thread-8] org.hibernate.SQL : update account set balance=?, pin=?, version=? where nr=?
DEBUG 20320 --- [pool-1-thread-4] org.hibernate.SQL : select account0_.nr as nr1_0_, account0_.balance as balance2_0_, account0_.pin as pin3_0_, account0_.version as version4_0_ from account account0_ where account0_.nr=?
DEBUG 20320 --- [pool-1-thread-4] org.hibernate.SQL : update account set balance=?, pin=?, version=? where nr=?
DEBUG 20320 --- [pool-1-thread-9] org.hibernate.SQL : update account set balance=?, pin=?, version=? where nr=?
DEBUG 20320 --- [pool-1-thread-3] org.hibernate.SQL : select account0_.nr as nr1_0_, account0_.balance as balance2_0_, account0_.pin as pin3_0_, account0_.version as version4_0_ from account account0_ where account0_.nr=?
DEBUG 20320 --- [pool-1-thread-5] org.hibernate.SQL : select account0_.nr as nr1_0_, account0_.balance as balance2_0_, account0_.pin as pin3_0_, account0_.version as version4_0_ from account account0_ where account0_.nr=?
DEBUG 20320 --- [ool-1-thread-10] org.hibernate.SQL : select account0_.nr as nr1_0_, account0_.balance as balance2_0_, account0_.pin as pin3_0_, account0_.version as version4_0_ from account account0_ where account0_.nr=?
DEBUG 20320 --- [pool-1-thread-1] org.hibernate.SQL : select account0_.nr as nr1_0_, account0_.balance as balance2_0_, account0_.pin as pin3_0_, account0_.version as version4_0_ from account account0_ where account0_.nr=?
DEBUG 20320 --- [pool-1-thread-7] org.hibernate.SQL : select account0_.nr as nr1_0_, account0_.balance as balance2_0_, account0_.pin as pin3_0_, account0_.version as version4_0_ from account account0_ where account0_.nr=?
DEBUG 20320 --- [pool-1-thread-7] org.hibernate.SQL : update account set balance=?, pin=?, version=? where nr=?
DEBUG 20320 --- [ool-1-thread-10] org.hibernate.SQL : update account set balance=?, pin=?, version=? where nr=?
DEBUG 20320 --- [pool-1-thread-5] org.hibernate.SQL : update account set balance=?, pin=?, version=? where nr=?
DEBUG 20320 --- [pool-1-thread-3] org.hibernate.SQL : update account set balance=?, pin=?, version=? where nr=?
DEBUG 20320 --- [pool-1-thread-1] org.hibernate.SQL : update account set balance=?, pin=?, version=? where nr=?
DEBUG 20320 --- [           main] org.hibernate.SQL : select account0_.balance as col_0_0_ from account account0_ where account0_.nr=? and account0_.pin=?

java.lang.AssertionError: 
Expected :10000
Actual   :1000

您正在使用 ExecutorService 启动 10 个线程。

这 10 个线程是独立 运行ning 的。这意味着线程 运行 首先没有可预测的顺序。正如您在日志输出中看到的那样:

[pool-1-thread-8]
[pool-1-thread-2]
[pool-1-thread-6]
[pool-1-thread-9]
[pool-1-thread-2]
[pool-1-thread-6]
[pool-1-thread-8]
[pool-1-thread-4]
[pool-1-thread-4]
[pool-1-thread-9]
[pool-1-thread-3]
[pool-1-thread-5]
[ool-1-thread-10]
[pool-1-thread-1]
[pool-1-thread-7]
[pool-1-thread-7]
[ool-1-thread-10]
[pool-1-thread-5]
[pool-1-thread-3]
[pool-1-thread-1]

为避免覆盖您的帐户余额,您必须使用悲观锁定。

这可以在存储库上使用 @Lock 注释实现:

@Lock(LockModeType.PESSIMISTIC_WRITE)
Account account = accountRepository.getAccountByNr(accountNr);

您必须确保存款方法对 运行 同一交易中的所有代码具有交易性:

@Transactional
public void deposit(int accountNr, long amount) throws InvalidCredentials, InvalidTransaction {
    Account account = accountRepository.getAccountByNr(accountNr);
    account.deposit(amount);
    accountRepository.saveAndFlush(account);
}

所以每次调用getAccountByNr都会锁住记录,事务结束会释放锁。