使用 EntityManager 在实体上设置悲观锁定

Posted

技术标签:

【中文标题】使用 EntityManager 在实体上设置悲观锁定【英文标题】:Set pessimistic lock on entity with EntityManager 【发布时间】:2018-02-05 09:02:36 【问题描述】:

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

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

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 没有共享他的休眠上下文。

我做了以下观察:

每当我在entity 上调用setter 并将其保存到数据库时,都会获得锁。但这不是我需要的。我需要锁而没有修改实体。 我也试过entityManager.lock(entity, LockModeType.PESSIMISTIC_WRITE)的方法,但行为是一样的。 我在 DBVisualizer 中找到了事务设置。目前它设置为 TRANSACTION_NONE。我也尝试了所有其他方法(TRANSACTION_READ_UNCOMMITTED、TRANSACTION_READ_COMMITTED、TRANSACTION_REPEATABLE_READ、TRANSACTION_SERIALIZABLE),但均未成功。 让第一个线程读取实体,然后第二个线程读取相同的实体。让第一个踏板修改实体,然后第二个修改它。然后让双方都保存实体,最后保存实体的人获胜,不会抛出异常。

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

【问题讨论】:

我不知道它在 Java、Hibernate、JPA 中是如何应用的,但是——如果您的软件打开游标,该游标使用 CURSOR STABILITY 或 REPEATABLE READ 隔离来获取行(又名实体) (SET ISOLATION 语句)和 FOR UPDATE 模式(光标属性),则该行将在获取时被其他人锁定以防更新。在您进行更改之前,您不会阻止其他人阅读该行,但您可以确保它不会在您背后改变。但是,这是在相对较低的级别上工作的(我认为是 ESQL/C;您认为是 JDBC 或 ODBC)。我不知道它是如何向上转换调用链的。 @JonathanLeffler:我将事务级别更改为 TRANSACTION_READ_UNCOMMITED、TRANSACTION_READ_COMMITED、TRANSACTION_REPEATABLE_READ 和 TRANSACTION_SERIALIZABLE。他们都没有工作。我在第二次阅读时没有得到异常(未经修改)。 @Chris311 你应该发布更完整的代码来测试它。是否使用两个不同的实体管理器读取实体?否则,每当您再次尝试加载同一实体时,实体管理器只会返回它的本地托管实例,它不再需要访问数据库。 以下内容可能对***.com/questions/32336481/…有帮助 EntityManager 不是线程安全的,具有单例范围的 EM 看起来确实有点设计味道(除非您的应用是单线程的) 【参考方案1】:

你描述的两种方式,即。

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

【讨论】:

这正是我面临的问题。我得到了相同的实体管理器实例并且我在同一个事务中。我更新了我的问题以使这一点更清楚。【参考方案2】:

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

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

【讨论】:

【参考方案3】:

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

【讨论】:

根本没有回答我的问题。 @Chris311 你做对了! entityManager.lock(entity, LockModeType.PESSIMISTIC_WRITE);就够了 不,它不是这样工作的。我可以毫无例外地调用 find 方法两次。此外,我可以使用 DBVisualizer 读取数据库中的实体... 1.您从一个线程进行后续调用吗?如果是 - 没关系。 2.你不能锁定实体的读取,只能从修改 @Chris311 您是否尝试从 dbvisualizer 更改实体,同时将其锁定在您的应用程序中?

以上是关于使用 EntityManager 在实体上设置悲观锁定的主要内容,如果未能解决你的问题,请参考以下文章

在 Spring 上实例化 EntityManager

EntityManager 是不是持有对分离实体的引用?

从 EntityManager 获取所有映射的实体

注入 EntityManager 与。实体管理器工厂

EntityManager方法简介

如何使用 Doctrine QueryBuilder 或 EntityManager 通过多对多相关实体查找实体