java - 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。
解决方案
问题很可能是您的数据库事务和 java 锁之间存在竞争。
另一个问题是您只锁定了两个帐户中的一个,但双方都需要防止并发访问。这将引入死锁的可能性。
DB/java 锁竞争的场景是:
- HTTP 请求到达您的控制器,
- @Transaction 启动数据库事务
- 你得到一个 Java 锁
- 您执行该操作,尚未将任何内容刷新到 DB
- 您释放了 java 锁,但您的控制器方法尚未返回,因此 JPA 事务未刷新到数据库
- 另一个请求进来,打开一个事务,并“看到世界”,也就是说,没有刷新(例如步骤“0”)
- 无论现在发生什么,您都有两笔交易,根据您的需要,其中一笔交易对世界的看法是“错误的”。
现在想象一下,如果除此之外,您的程序有多个实例(例如故障转移、负载共享),那么您的 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 锁定)并解决死锁问题。
推荐阅读
- regex - 将 Unicode 字符与正则表达式匹配
- javascript - 标签为空时如何隐藏?
- mysql - SQL 索引存在,但 drop 命令告诉它不存在
- api - APIs for implementing video playback in embedded Linux (buildroot)
- android - Is there any use of Android:debuggable=true when you don't have any source code?
- c# - 如何从嵌套的 JSON 对象中检索特定属性
- laravel - Laravel - 按特定顺序获取数据……
- python - Python Pandas 中的转置
- google-cloud-platform - 加载区域“europe-west1-a”时出错:googleapi:错误 404:找不到资源“projects/google-project-id/zones/europe-west1-a”,notFound
- java - 如何在java中将我的数组数据添加到我的列表中