使用 EntityManager 对实体设置悲观锁

Set pessimistic lock on entity with EntityManager

考虑以下情况: 我们收到来自更新我们实体的网络服务的请求。有时我们可能会(几乎)同时收到两个请求。由于并发更新,我们遇到过实体看起来完全错误的情况。这个想法是悲观地锁定实体,这样每当第一个请求到来时它立即锁定实体并且第二个请求不能触及它(乐观锁定对我们来说是别无选择)。我写了一个集成测试来检查这个行为。

我得到了如下所示的集成测试:

protected static TestRemoteFacade testFacade;

@BeforeClass
public static void setup() {
    testFacade = BeanLocator.lookupRemote(TestRemoteFacade.class, TestRemoteFacade.REMOTE_JNDI_NAME, TestRemoteFacade.NAMESPACE);
}

@Test
public void testPessimisticLock() throws Exception {
    testFacade.readPessimisticTwice();
}

调用 bean

@Stateless
@Clustered
@SecurityDomain("myDomain")
@RolesAllowed({ Roles.ACCESS })
public class TestFacadeBean extends FacadeBean implements TestRemoteFacade {

    @EJB
    private FiolaProduktLocalFacade produkt;

    @Override
    public void readPessimisticTwice() {
        produkt.readPessimisticTwice();
    }
}

produkt 本身就是一个 bean

@Stateless
@Clustered
@SecurityDomain("myDomain")
@RolesAllowed({ Roles.ACCESS })
public class ProduktFacadeBean implements ProduktLocalFacade {

    @Override
    public void readPessimisticTwice() {
        EntityManager entityManager = MyService.getCrudService().getEntityManager();
        System.out.println("Before first try.");
        entityManager.find(MyEntity.class, 1, LockModeType.PESSIMISTIC_WRITE);
        System.out.println("Before second try.");
        entityManager.find(MyEntity.class, 1, LockModeType.PESSIMISTIC_WRITE);
        System.out.println("After second try.");
    }
}

public class MyService {

    public static CrudServiceLocalFacade getCrudService() {
        return CrudServiceLookup.getCrudService();
    }

}

public final class CrudServiceLookup {

    private static CrudServiceLocalFacade crudService;

    private CrudServiceLookup(){
    }

    public static CrudServiceLocalFacade getCrudService() {
        if (crudService == null)
            crudService = BeanLocator.lookup(CrudServiceLocalFacade.class, CrudServiceLocalFacade.LOCAL_JNDI_NAME);
        return crudService;
    }

    public static void setCrudService(CrudServiceLocalFacade crudService) {
        CrudServiceLookup.crudService = crudService;
    }

}

@Stateless
@Local(CrudServiceLocalFacade.class)
@TransactionAttribute(TransactionAttributeType.MANDATORY)
@Interceptors(OracleDataBaseInterceptor.class)
public class CrudServiceFacadeBean implements CrudServiceLocalFacade {

    private EntityManager em;

    @Override
    @PersistenceContext(unitName = "persistence_unit")
    public void setEntityManager(EntityManager entityManager) {
        em = entityManager;
    }

    @Override
    public EntityManager getEntityManager() {
        return em;
    }

}

现在出现的问题是:如果我在System.out.println("Before second try.");下断点启动一次集成测试,然后第二次启动集成测试,后一次仍然可以读取MyEntity。值得注意的是它们是 不同的 个实例(我在调试模式下对 instanceId 进行了观察)。这表明 entityManager 没有共享他的休眠上下文。

我做了以下观察:

我如何悲观地读取一个对象,这意味着:每当我从数据库加载一个实体时,我希望它立即被锁定(即使没有修改)。

仅当另一个线程已持有锁时,锁才会失败。您可以在数据库中的单行上使用两个 FOR UPDATE 锁,因此这不是特定于 JPA 的东西。

你描述的两种方式,即。

  • em.find(MyEntity.class, 1, LockModeType.PESSIMISTIC_WRITE)
  • em.lock(entity, LockModeType.PESSIMISTIC_WRITE)

锁定数据库中的相关行,但仅针对 entityManager 生命周期,即。对于封闭事务,一旦您到达事务结束,锁将自动释放

@Transactional()
public void doSomething() {
  em.lock(entity, LockModeType.PESSIMISTIC_WRITE); // entity is locked
  // any other thread trying to update the entity until this method finishes will raise an error
}
...
object.doSomething();
object.doSomethingElse(); // lock is already released here

您是否尝试过在您的应用程序服务器中设置隔离级别?

无论您之后尝试做什么,要锁定一行 (read/write),您需要将隔离级别设置为 TRANSACTION_SERIALIZABLE。