我有这个交易:
em.getTransaction().begin();
{
final Payment payment = em.find(Payment.class, id);
if (payment.status != Status.INIT)
throw new IllegalStateException("Cannot set to PAID, is not INIT but " + status);
payment.status = Status.PAID;
}
em.getTransaction().commit();
log.info("Payment " + id + " was paid");
但是,正如您在此处所看到的,交易不会阻止竞争条件:
[11:10:18.265] INFO [PaymentServlet] [MSP] Status COMPLETED
[11:10:18.265] INFO [PaymentServlet] Payment c76f9e75-99d7-4721-a8ac-e3a638dd8317 was paid
[11:10:18.267] INFO [PaymentServlet] [MSP] Status COMPLETED
[11:10:18.267] INFO [PaymentServlet] Payment c76f9e75-99d7-4721-a8ac-e3a638dd8317 was paid
付款设置为PAID
两次。我没有抛出异常,也没有回滚或任何东西。
我做错了什么?
答案 0 :(得分:7)
您需要使用乐观锁定。乐观锁定是很少发生冲突的更新,因此在发生偶然事务时回滚它是可以接受的。悲观锁定会导致数据库在对象使用时保持锁定,有效地对所有内容进行单线程处理并可能导致性能问题。有关更详细的说明,请参阅http://en.wikibooks.org/wiki/Java_Persistence/Locking#JPA_2.0_Locking。
要解决此问题,您应该向Payment添加一个字段(传统声明是私有Long版本)并为其提供JPA @Version注释。如果要手动管理架构,请确保右表中存在相应的列。然后,JPA将使用此字段检查冲突的更新,并在存在冲突时回滚事务。
更新:有关悲观锁定的更多信息:https://blogs.oracle.com/carolmcdonald/entry/jpa_2_0_concurrency_and简而言之,可以配置JPA来锁定对象,但这样做是非常罕见的。换句话说,如果您是对JDBC进行手工编码查询,则必须在每次选择结束时写入“for update”以引起悲观锁定;默认情况下不会锁定读取,因为它会使数据库和数据库用户哭泣。
答案 1 :(得分:1)
您没有说明您正在使用的数据库,或者什么事务隔离级别。如果使用符合SQL标准的SERIALIZABLE
事务,则不会看到此错误。 9.1之前的PostgreSQL版本,MS SQL Server的某些配置以及所有版本的Oracle在您要求时都不会提供真正的可序列化事务,因此必须在此类环境中使用显式锁定。大多数数据库产品默认为READ COMMITTED
事务隔离级别,因此您可能需要明确请求SERIALIZABLE
个事务。
完全披露,我与Dan R.K.合作。麻省理工学院的港口将真正的可序列化交易添加到PostgreSQL版本9.1,以便威斯康星州法院软件可以干净地处理这些问题。有关差异的示例,请参阅this Wiki page。