你好,周围也有类似的问题,但是找不到可以帮助我的答案。 在春季启动应用程序中,我具有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。
答案 0 :(得分:3)
问题很可能是您在数据库事务和Java锁之间存在竞争。
另一个问题是,您只能锁定两个帐户中的一个,但是需要保护双方不被并发访问。这将引入死锁的可能性。
DB / java锁定竞赛的场景是:
现在想象一下,如果最重要的是,您的程序具有多个实例(例如,故障转移,负载共享),那么您的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
服务方法调用保存在存储库中。
此外,建议也向存储库中添加非锁定选择方法,以便您可以在不需要锁定(即出于非资金转移目的)时使用该方法。
我有服务类可以在帐户之间转移资金, 使用细粒度锁定。如果线程属于线程,它将锁定 同一帐户。
使用这种方法,如果帐户相同,锁定将自动发生。