spring 引导服务中的线程
Threads in spring boot service
您好,周围有类似的问题,但找不到对我有帮助的答案。
在 spring 引导应用程序中,我有服务 class 在帐户之间转账,我在其中使用细粒度锁定。如果线程属于同一个帐户,它们将被锁定。
@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 美元,控制台输出如下所示:
BALANCE BEFORE TRANSFER 1000
BALANCE AFTER TRANSFER 900
BALANCE BEFORE TRANSFER 1000
BALANCE AFTER TRANSFER 900
BALANCE BEFORE TRANSFER 900
BALANCE AFTER TRANSFER 800
BALANCE BEFORE TRANSFER 800
BALANCE AFTER TRANSFER 700
BALANCE BEFORE TRANSFER 700
BALANCE AFTER TRANSFER 600
BALANCE BEFORE TRANSFER 700
BALANCE AFTER TRANSFER 600
所以它大部分工作正常,但在某些时候它没有得到更新的余额。到目前为止,我尝试了一切,传播和隔离。在线程移除锁之前手动创建事务并提交它们。似乎没有任何效果。在我使用
之前
Propagation.REQUIRES_NEW
控制台输出总是
BALANCE BEFORE TRANSFER 1000
BALANCE AFTER TRANSFER 900
现在它有时工作有时不工作。它不一致。
获取余额方法刷新帐户使用:
accountRepository.refresh()
和 transactionService.createTransactions 也被注释为 Propagation.REQUIRES_NEW
那么谁能告诉我为什么这不起作用?至少以正确的方式引导我。
谢谢
编辑:
如果从数据库中读取的数据不够清晰。使用 spring jpa.
问题很可能是您的数据库事务与 java 锁之间存在竞争。
另一个问题是您只锁定了两个帐户之一,但双方都需要防止并发访问。这将引入死锁的可能性。
DB/java 锁竞争的场景是:
- HTTP 请求到达您的控制器,
- @Transaction 启动一个数据库事务
- 你得到一把Java锁
- 您执行操作,尚未向数据库刷新任何内容
- 您释放了 java 锁,但您的控制器方法尚未 returned,因此 JPA 事务未刷新到 DB
- 另一个请求进来,打开一个事务,并且"sees the world"原样,也就是说,没有刷新任何东西(例如步骤“0”)
- 无论现在发生什么,您都有两个事务,其中一个根据您的需要具有 "wrong" 世界观。
现在想象一下,如果除此之外,您的程序有多个实例(例如故障转移、负载共享),您的 java 锁甚至都无法工作,这是一场噩梦:-)。
执行此操作的 "simple" 方法(在这种复杂程度下)是 "SELECT FOR UPDATE" 您的实体,这将防止 SELECTS 的缠绕。您甚至不需要 java 锁,SQL 引擎会在本地提供锁定(第二个 select 在第一个事务提交之前不会 return)。
但是你还是有死锁的风险(如果有两个请求,一个是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 锁定)并解决死锁问题。
正如 GPI 的回答和其他人的评论,摆脱 Java 锁定,因为在长期 运行 中收益会抵消损失。 spring-data-jpa 已经通过使用 - org.springframework.data.jpa.repository.Lock
注释解决了这个问题。
只需使用 - @Lock(LockModeType.PESSIMISTIC_WRITE)
注释用于 select 帐户数据(帐户实体)以用于汇款目的的存储库方法。这将锁定 selected 数据,直到交易完成。我想,相同的存储库将用于检索 from 以及 to 帐户。
将转帐金额应用于两个帐户并在您的 @Transactional
服务方法中调用存储库保存。
此外,建议也向存储库添加非锁定 select 方法,这样您就可以在不需要锁定时使用它,即用于非汇款目的。
i have Service class to transfer money between accounts, where im
using fine-grained locking. Threads gonna lock if they belong to the
same account.
使用这种方法,如果帐户相同,将自动锁定。
您好,周围有类似的问题,但找不到对我有帮助的答案。 在 spring 引导应用程序中,我有服务 class 在帐户之间转账,我在其中使用细粒度锁定。如果线程属于同一个帐户,它们将被锁定。
@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 美元,控制台输出如下所示:
BALANCE BEFORE TRANSFER 1000
BALANCE AFTER TRANSFER 900
BALANCE BEFORE TRANSFER 1000
BALANCE AFTER TRANSFER 900
BALANCE BEFORE TRANSFER 900
BALANCE AFTER TRANSFER 800
BALANCE BEFORE TRANSFER 800
BALANCE AFTER TRANSFER 700
BALANCE BEFORE TRANSFER 700
BALANCE AFTER TRANSFER 600
BALANCE BEFORE TRANSFER 700
BALANCE AFTER TRANSFER 600
所以它大部分工作正常,但在某些时候它没有得到更新的余额。到目前为止,我尝试了一切,传播和隔离。在线程移除锁之前手动创建事务并提交它们。似乎没有任何效果。在我使用
之前Propagation.REQUIRES_NEW
控制台输出总是
BALANCE BEFORE TRANSFER 1000
BALANCE AFTER TRANSFER 900
现在它有时工作有时不工作。它不一致。
获取余额方法刷新帐户使用:
accountRepository.refresh()
和 transactionService.createTransactions 也被注释为 Propagation.REQUIRES_NEW
那么谁能告诉我为什么这不起作用?至少以正确的方式引导我。
谢谢
编辑: 如果从数据库中读取的数据不够清晰。使用 spring jpa.
问题很可能是您的数据库事务与 java 锁之间存在竞争。
另一个问题是您只锁定了两个帐户之一,但双方都需要防止并发访问。这将引入死锁的可能性。
DB/java 锁竞争的场景是:
- HTTP 请求到达您的控制器,
- @Transaction 启动一个数据库事务
- 你得到一把Java锁
- 您执行操作,尚未向数据库刷新任何内容
- 您释放了 java 锁,但您的控制器方法尚未 returned,因此 JPA 事务未刷新到 DB
- 另一个请求进来,打开一个事务,并且"sees the world"原样,也就是说,没有刷新任何东西(例如步骤“0”)
- 无论现在发生什么,您都有两个事务,其中一个根据您的需要具有 "wrong" 世界观。
现在想象一下,如果除此之外,您的程序有多个实例(例如故障转移、负载共享),您的 java 锁甚至都无法工作,这是一场噩梦:-)。
执行此操作的 "simple" 方法(在这种复杂程度下)是 "SELECT FOR UPDATE" 您的实体,这将防止 SELECTS 的缠绕。您甚至不需要 java 锁,SQL 引擎会在本地提供锁定(第二个 select 在第一个事务提交之前不会 return)。
但是你还是有死锁的风险(如果有两个请求,一个是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 锁定)并解决死锁问题。
正如 GPI 的回答和其他人的评论,摆脱 Java 锁定,因为在长期 运行 中收益会抵消损失。 spring-data-jpa 已经通过使用 - org.springframework.data.jpa.repository.Lock
注释解决了这个问题。
只需使用 - @Lock(LockModeType.PESSIMISTIC_WRITE)
注释用于 select 帐户数据(帐户实体)以用于汇款目的的存储库方法。这将锁定 selected 数据,直到交易完成。我想,相同的存储库将用于检索 from 以及 to 帐户。
将转帐金额应用于两个帐户并在您的 @Transactional
服务方法中调用存储库保存。
此外,建议也向存储库添加非锁定 select 方法,这样您就可以在不需要锁定时使用它,即用于非汇款目的。
i have Service class to transfer money between accounts, where im using fine-grained locking. Threads gonna lock if they belong to the same account.
使用这种方法,如果帐户相同,将自动锁定。