这是重现异常的示例项目。
此示例说明了当许多并发事务正在修改帐户余额时的问题。Account 可以绑定多个 Card 实体。交易与订单相关,并且在时间上是最后的。每个线程执行如下:
- 客户端请求“/order/{hashId}”以获取第一个可用的 Order by given card hash id
- 客户端为给定订单开始新的交易 - '/tx/{orderId}/start'
- 客户完成 tx - '/tx/{txId}/stop/{amount}' 从账户余额中减去 tx 金额。
实体锁定
Account 和 Order 实体使用@javax.persistence.Version 进行版本控制。在最后一步 Account 实体被悲观写锁锁定:
@Override
public Account getLockedAccount(Integer id) {
final Account account = findOne(id);
em.lock(account, LockModeType.PESSIMISTIC_WRITE);
return account;
}
测试
要测试并发访问,请使用JMeter脚本 src/main/resources/StressTest.jmx。注意:由于使用JSON Path extractor ,必须将额外的库安装到 JMeter 主页才能运行脚本。在普通笔记本电脑上使用这些特定设置,您可以获得大约 10% 的 TxEnd 请求错误:
{
"timestamp":1425407408204,
"status":500,
"error":"Internal Server Error",
"exception":"org.springframework.orm.ObjectOptimisticLockingFailureException",
"message":"Object of class [sample.data.jpa.domain.Account] with identifier [1]: optimistic locking failed; nested exception is org.hibernate.StaleObjectStateException: Row was updated or deleted by another transaction (or unsaved-value mapping was incorrect) : [sample.data.jpa.domain.Account#1]",
"path":"/tx/1443/stop/46.4"
}
问题
尽管使用了悲观写锁,我仍然得到乐观锁定异常。是否有任何其他方法可以确保帐户的完整性,而无需为所有更新或同步方法创建任务执行队列?
UPD:任务执行器的工作被放置在另一个分支中。SpringThreadPoolTaskExecutor
结合事务性任务解决了这个问题。