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都会锁住记录,事务结束会释放锁。
我有所有简单的 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都会锁住记录,事务结束会释放锁。