首页 > 解决方案 > Spring Boot 服务中的线程

问题描述

您好,周围有类似的问题,但找不到对我有帮助的答案。在 Spring Boot 应用程序中,我有服务类在帐户之间转移资金,我使用细粒度锁定。如果线程属于同一个帐户,它们将被锁定。

@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余额800
余额700余额700
之前700余额在
转移600余额600
余额600之前转移700
余额700余额转移后600余额600

所以它大部分工作正常,但有时它没有得到更新的余额。到目前为止,我尝试了一切,传播和隔离。在线程移除锁之前手动创建事务并提交它们。似乎没有任何效果。在我使用之前

传播.REQUIRES_NEW

控制台输出总是

转账前余额 1000
转账后余额 900

现在它有时工作有时不工作。它不一致。

Get Balance 方法使用以下方法刷新帐户:

accountRepository.refresh()

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

谢谢你


编辑: 如果从数据库中读取的数据不够清晰。使用弹簧 jpa。

标签: javamultithreadingspring-bootspring-data-jpaspring-transactions

解决方案


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

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

DB/java 锁竞争的场景是:

  1. HTTP 请求到达您的控制器,
  2. @Transaction 启动数据库事务
  3. 你得到一个 Java 锁
  4. 您执行该操作,尚未将任何内容刷新到 DB
  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 锁定)并解决死锁问题。


推荐阅读