如何在 Spring 中处理具有并发访问的事务
How to handle transactions with concurrent access in Spring
我有一种方法的服务:
@Service
public class DefaultTestService implements TestService {
private static final Logger LOGGER = Logger.getLogger(DefaultTestService.class);
@Autowired
private TestRepository testRepository;
@Transactional(readOnly = false, isolation = Isolation.SERIALIZABLE)
@Override
public void incrementAndGet(Long testModelId) {
LOGGER.debug("Transaction is active: " + TransactionSynchronizationManager.isActualTransactionActive());
final TestModel tm = testRepository.findOne(testModelId);
if (tm != null) {
LOGGER.debug("Updated " + testModelId + " from value: " + tm.getValue());
tm.setValue(tm.getValue() + 1);
testRepository.save(tm);
} else {
LOGGER.debug("Saved with id: " + testModelId);
final TestModel ntm = new TestModel();
ntm.setId(testModelId);
testRepository.save(ntm);
}
}
}
而且我 运行 Gatling 有 2 个并行调用配置 testModelId = 1L
参数。
由于这些调用,我收到错误消息:
org.postgresql.util.PSQLException: ERROR: duplicate key value violates unique constraint "test_model_pkey"
从日志中可以看出,有两次调用同时进入了这个方法,并且每次都打印了日志
"Saved with id: 1"
"Saved with id: 1"
我假设在此方法上添加事务注释会阻止行 testRepository.findOne(testModelId)
中的一个调用,直到其他调用完成其执行,但正如我从日志中看到的那样,它以不同的方式工作。
所以我的问题是,在出现并发访问的情况下,事务是如何工作的?我该如何处理这种并发访问的情况?
事务意味着在事务边界内执行的持久对象的所有修改将:
- 在事务结束时提交(即 所有 修改都保存在数据库中)
- 在事务结束时回滚(即 none 的修改保留在数据库中)
就这些了。
在这种情况下交易如何运作?
2 个线程之一到达事务末尾并成功提交。另一个线程到达事务末尾,由于违反约束而未能提交,因此第二个事务终止于 "rollback" 状态。
为什么 findOne
在第二笔交易中没有被阻止?
仅仅是因为,尽管 SERIALIZABLE 事务级别,但没有要锁定的行。 findOne
return 两个事务都没有结果,也没有任何东西被锁定(当然,如果第一个事务在第二个事务执行之前提交 findOne
:这是另一回事)。
如何在您的特定情况下处理并发事务(即插入新行时违反 PK 约束)?
最常见的策略是让数据库将 id 分配给新行 - 在序列的帮助下 -
(作为实验,您可以尝试将隔离级别设置为 READ_UNCOMMITED,以便第二个事务可以从第一个事务读取未提交的更改。我不确定您是否注意到任何差异,因为如果 findOne
在第二笔交易中,在第一笔交易 testRepository.save(ntm);
之前执行,它仍然 return 没有结果)
一般如何处理并发修改引起的事务回滚?
这真的取决于您的用例。基本上你可以选择:
- 捕获异常并"retry"操作。
- 向调用者抛出异常(可能向用户显示一条温和的错误消息)。
请注意,如果事务以回滚状态终止:事务期间修改的持久对象图不会恢复到其原始状态。
请注意,使用隔离级别 SERIALIZABLE 会导致巨大的性能问题,通常仅用于关键和偶尔的事务。
我在并行调用中遇到了类似的问题,我使用 ReentrantLock 解决了它。
这是一个使用您的代码的示例:
@Service
public class DefaultTestService {
private final ReentrantLock lock = new ReentrantLock();
@Autowired
private TestRepository testRepository;
@Transactional(readOnly = false, isolation = Isolation.SERIALIZABLE)
public void incrementAndGet(Long testModelId) {
lock.lock();
try {
final TestModel tm = testRepository.findOne(testModelId);
if (tm != null) {
LOGGER.debug("Updated " + testModelId + " from value: " + tm.getValue());
tm.setValue(tm.getValue() + 1);
testRepository.save(tm);
} else {
LOGGER.debug("Saved with id: " + testModelId);
final TestModel ntm = new TestModel();
ntm.setId(testModelId);
testRepository.save(ntm);
}
} finally {
lock.unlock();
}
}
}
我有一种方法的服务:
@Service
public class DefaultTestService implements TestService {
private static final Logger LOGGER = Logger.getLogger(DefaultTestService.class);
@Autowired
private TestRepository testRepository;
@Transactional(readOnly = false, isolation = Isolation.SERIALIZABLE)
@Override
public void incrementAndGet(Long testModelId) {
LOGGER.debug("Transaction is active: " + TransactionSynchronizationManager.isActualTransactionActive());
final TestModel tm = testRepository.findOne(testModelId);
if (tm != null) {
LOGGER.debug("Updated " + testModelId + " from value: " + tm.getValue());
tm.setValue(tm.getValue() + 1);
testRepository.save(tm);
} else {
LOGGER.debug("Saved with id: " + testModelId);
final TestModel ntm = new TestModel();
ntm.setId(testModelId);
testRepository.save(ntm);
}
}
}
而且我 运行 Gatling 有 2 个并行调用配置 testModelId = 1L
参数。
由于这些调用,我收到错误消息:
org.postgresql.util.PSQLException: ERROR: duplicate key value violates unique constraint "test_model_pkey"
从日志中可以看出,有两次调用同时进入了这个方法,并且每次都打印了日志
"Saved with id: 1"
"Saved with id: 1"
我假设在此方法上添加事务注释会阻止行 testRepository.findOne(testModelId)
中的一个调用,直到其他调用完成其执行,但正如我从日志中看到的那样,它以不同的方式工作。
所以我的问题是,在出现并发访问的情况下,事务是如何工作的?我该如何处理这种并发访问的情况?
事务意味着在事务边界内执行的持久对象的所有修改将:
- 在事务结束时提交(即 所有 修改都保存在数据库中)
- 在事务结束时回滚(即 none 的修改保留在数据库中) 就这些了。
在这种情况下交易如何运作?
2 个线程之一到达事务末尾并成功提交。另一个线程到达事务末尾,由于违反约束而未能提交,因此第二个事务终止于 "rollback" 状态。
为什么 findOne
在第二笔交易中没有被阻止?
仅仅是因为,尽管 SERIALIZABLE 事务级别,但没有要锁定的行。 findOne
return 两个事务都没有结果,也没有任何东西被锁定(当然,如果第一个事务在第二个事务执行之前提交 findOne
:这是另一回事)。
如何在您的特定情况下处理并发事务(即插入新行时违反 PK 约束)?
最常见的策略是让数据库将 id 分配给新行 - 在序列的帮助下 -
(作为实验,您可以尝试将隔离级别设置为 READ_UNCOMMITED,以便第二个事务可以从第一个事务读取未提交的更改。我不确定您是否注意到任何差异,因为如果 findOne
在第二笔交易中,在第一笔交易 testRepository.save(ntm);
之前执行,它仍然 return 没有结果)
一般如何处理并发修改引起的事务回滚?
这真的取决于您的用例。基本上你可以选择:
- 捕获异常并"retry"操作。
- 向调用者抛出异常(可能向用户显示一条温和的错误消息)。
请注意,如果事务以回滚状态终止:事务期间修改的持久对象图不会恢复到其原始状态。
请注意,使用隔离级别 SERIALIZABLE 会导致巨大的性能问题,通常仅用于关键和偶尔的事务。
我在并行调用中遇到了类似的问题,我使用 ReentrantLock 解决了它。
这是一个使用您的代码的示例:
@Service
public class DefaultTestService {
private final ReentrantLock lock = new ReentrantLock();
@Autowired
private TestRepository testRepository;
@Transactional(readOnly = false, isolation = Isolation.SERIALIZABLE)
public void incrementAndGet(Long testModelId) {
lock.lock();
try {
final TestModel tm = testRepository.findOne(testModelId);
if (tm != null) {
LOGGER.debug("Updated " + testModelId + " from value: " + tm.getValue());
tm.setValue(tm.getValue() + 1);
testRepository.save(tm);
} else {
LOGGER.debug("Saved with id: " + testModelId);
final TestModel ntm = new TestModel();
ntm.setId(testModelId);
testRepository.save(ntm);
}
} finally {
lock.unlock();
}
}
}