Spring Boot服务中的线程

时间:2018-10-23 11:05:04

标签: java multithreading spring-boot spring-data-jpa spring-transactions

你好,周围也有类似的问题,但是找不到可以帮助我的答案。 在春季启动应用程序中,我具有Service类来在帐户之间转移资金,在这里我使用了细粒度的锁定。如果线程属于同一帐户,则线程将锁定。

@Service
public class AccountServiceImpl implements AccountService {

static final HashMap<Long, ReentrantLock> locks = new HashMap<Long, ReentrantLock>();

    private ReentrantLock getLock(Long id) {
        synchronized (locks) {
            ReentrantLock lock = locks.get(id);
            if (lock == null) {
                lock = new ReentrantLock();
                locks.put(id, lock);
            }
            return lock;
        }
      }
    }

    @Override
    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public List<Transaction> transferMoney(Long sourceAccountId, Long targetAccountId, Currency currency, BigDecimal amount) {

        Lock lock = getLock(sourceAccountId);
        try {
            lock.lock();
       Balance balance = getBalance(sourceAccountId, currency);
                System.out.println("BALANCE BEFORE TRANSFER " + balance.getBalance()); 
        //createTransactions here using transactionService.create(transactions)
        accountRepository.refresh(accountRepository.findOne(sourceAccountId))  
        balance = getBalance(sourceAccountId, currency);
        System.out.println("BALANCE AFTER TRANSFER " + balance.getBalance()); 
        return transactions;
       }finally{
           lock.unlock()
        }


}

除了我使用apache jmeter发送多个并行请求外,它通常都能按预期工作。如果我发送多个请求以从具有1000个余额控制台的帐户转移100美元,则输出如下:

  

转移前的余额1000
转移后的余额900
  转移前的余额1000
转移后的余额900
  转移之前的余额900
转移之后的余额800
  转移前的余额800
转移后的余额700
  转移前的余额700
转移后的余额600
  转移前的余额700
转移后的余额600

因此它通常可以正常工作,但是在某些时候它并没有更新余额。到目前为止,我已经尝试了所有内容,传播和隔离。在线程删除锁之前,手动创建事务并提交事务。似乎没有任何作用。在我使用

之前
  

传播。REQUIRES_NEW

控制台输出始终为

  

转移前的余额1000
转移后的余额900

现在它有时有时不起作用。它不一致。

“获取余额”方法使用以下方法刷新帐户:

  

accountRepository.refresh()

和transactionService.createTransactions也用Propagation注释。REQUIRES_NEW 那么谁能告诉我为什么这不起作用?至少以正确的方式指导我。

谢谢


编辑: 如果不清楚,则从数据库读取足够的数据。使用spring jpa。

2 个答案:

答案 0 :(得分:3)

问题很可能是您在数据库事务和Java锁之间存在竞争。

另一个问题是,您只能锁定两个帐户中的一个,但是需要保护双方不被并发访问。这将引入死锁的可能性。

DB / java锁定竞赛的场景是:

  1. HTTP请求命中您的控制器
  2. @Transaction启动数据库事务
  3. 您将获得Java锁
  4. 您执行了该操作,但尚未刷新任何内容到数据库
  5. 您释放了Java锁,但您的控制方法尚未返回,因此JPA事务未刷新到数据库
  6. 另一个请求进入,打开一个事务,并“照看世界”,即没有刷新(例如,步骤“ 0”)
  7. 无论现在发生什么,您都有两笔交易,其中一笔交易根据您的需求是“错误的”。

现在想象一下,如果最重要的是,您的程序具有多个实例(例如,故障转移,负载共享),那么您的Java锁定甚至将无法正常工作,真是一场噩梦:-)。

(在这种复杂程度下)“简单”的方法是对实体进行“ SELECT FOR UPDATE”,这可以防止SELECTS相互缠绕。您甚至不需要Java锁,SQL引擎将以本机方式提供锁定(第二个选择在提交第一个事务之前不会返回)。

但是您仍然会有死锁的风险(如果有两个请求,一个是帐户A到B,另一个是从B到C,这就打开了B被锁定在两个请求中的可能性交易,则您必须抓紧并重审此案,希望在该时间点解决冲突。

您可以在https://www.baeldung.com/jpa-pessimistic-locking处进行阅读,以了解如何执行SELECT FOR UPDATE,这基本上需要像这样加载实体:

entityManager.find(Account.class, accountId, LockModeType.PESSIMISTIC_WRITE);

另一种可能是反转Java锁和@transactionnal。这样,您永远不会在独占模式之前访问数据库。但这将留下问题,即多个JVM中的程序的多个实例无法共享锁。如果不是您的情况,那么这可能会更简单。

在任何一种情况下,您都仍然必须同时锁定两侧(DB或Java锁定)并解决死锁。

答案 1 :(得分:1)

正如GPI回答和其他人评论的那样,摆脱Java锁定,因为从长远来看,好处会抵消损失。通过使用-org.springframework.data.jpa.repository.Lock批注,已经为spring-data-jpa解决了此问题。

只需用-@Lock(LockModeType.PESSIMISTIC_WRITE)注释用于选择帐户数据(帐户实体)以进行汇款的存储库方法。这将锁定选定的数据,直到交易完成。我猜想,相同的存储库将用于从 以及帐户中检索。

将转账金额同时应用于两个帐户,并使用@Transactional服务方法调用保存在存储库中。

此外,建议也向存储库中添加非锁定选择方法,以便您可以在不需要锁定(即出于非资金转移目的)时使用该方法。

  

我有服务类可以在帐户之间转移资金,   使用细粒度锁定。如果线程属于线程,它将锁定   同一帐户。

使用这种方法,如果帐户相同,锁定将自动发生。