使用 Tx 传播丢失 Hibernate 更新 REQUIRES_NEW

Lost updates on Hibernate using Tx propagation REQUIRES_NEW

我需要一些帮助来管理我刚刚发现造成的混乱。

我想在我的实体中实施软锁定机制。我不想使用 Hibernate 的锁定功能,因为我的进程是由 long-运行 和复合事务组成的。简而言之:我需要在方法运行时将实体标记为 Locked,因此并发调用将阻止 运行.

所以我将定义为时间戳(将来用于检测锁超时)列

@Column(name="LOCK_TIME")
private Date lockTime;

到目前为止一切顺利。现在...

@Transactional
public void doSomething(Long entityId){

    Object lockInfo = lockManager.acquireLock(entityId);  //@Transactional(REQUIRES_NEW)
    if (lockInfo == null) // e.g. lock not acquired
        throw new ObjectLockedException();

    try{
        Entity e = entityDao.findById(entityId);

        ....
        entityDao.update(e);
    } finally {
        lockManager.unlock(lockInfo); //@Transactional(REQUIRES_NEW)
    }
}

和 LockManager 实现

@Override
public LockInfo lock(final Class<? extends Lockable> clazz, final Serializable id) throws NotFoundException
{
    try
    {
        return hibernateTemplate.execute(new HibernateCallback<LockInfo>()
        {

            @Override
            public LockInfo doInHibernate(Session session) throws HibernateException, SQLException
            {
                Lockable object = (Lockable) session.load(clazz, id);
                if (object == null)
                    throw new HibernateException(new NotFoundException(clazz, id));

                if (object.getLockTime() != null)
                    return null;

                object.setLockTime(new Date());
                session.update(object);

                return new LockInfo(clazz, id, object.getLockTime());
            }
        });
    }
    catch (HibernateException ex)
    {
        if (ex.getCause() instanceof NotFoundException)
            throw (NotFoundException) ex.getCause();
        throw ex;
    }
}

@Override
public void unlock(final LockInfo lock) throws NotFoundException, InvalidOperationException, IllegalArgumentException, ObjectNotLockedException
{
    try
    {
        hibernateTemplate.execute(new HibernateCallback<Void>()
        {

            @Override
            public Void doInHibernate(Session session) throws HibernateException, SQLException
            {
                Lockable object = (Lockable) session.load(lock.getClazz(), lock.getId());
                if (object == null)
                    throw new HibernateException(new NotFoundException(lock.getClazz(), lock.getId()));

                if (object.getLockTime() == null || !lock.getLockDate()
                                                         .equals(object.getLockTime()))
                    throw new HibernateException(new ObjectNotLockedException(lock.getId()));

                object.setLockTime(null);
                session.update(object);

                return null;
            }
        });
    }
    catch (HibernateException ex)
    {
        if (ex.getCause() instanceof NotFoundException)
            throw (NotFoundException) ex.getCause();
        throw ex;
    }

}

稍加调试就发现了我所犯的错误:对象在方法执行后仍然处于锁定状态。

想了想,画了一个状态序列:

| Statement                         | value of e |     DB state |
|-----------------------------------|:----------:|-------------:|
| lockManager.acquireLock(entityId) |            |  lock = null |
| e = entityDao.findById(entityId)  |   locked   | lock != null |
| lockManager.unlock                |   locked   |  lock = null |
| commit doSomething                | locked     | lock != null |

基本上即使 entityDao.update(e) 在实体解锁之前(我不会显示 lock()unlock() 方法,因为它们很简单),实际更新仅 在方法结束后发生。由于变量 e 拥有自己的锁定信息,而不是与数据库同步的信息,Hibernate 将其用作更新的一部分。在普通的 SQL 中,这永远不会发生,因为你不会触摸。

我发现我的锁系统设计得很糟糕:我想问你如何根据以下要求改进我的设计:

  1. 锁必须尽快生效
  2. 必须不惜一切代价在方法结束时移除锁(finally 子句),即使主事务回滚

我正在考虑为锁(列 entityClassentityId)使用单独的 table,但我想知道我的设计模式(锁列)是否可以改编

在发布问题之前,我花了一天时间重新检查我的设计,并最终自己找到了解决方案。为了以防万一,我想分享给后人。

我之前的分析(新事务被方法的事务覆盖)是正确的,并且基于这样一个事实,即方法的事务最终在解锁事务之后提交,而 tx 序列应该是:lock,process , 解锁.

两个简单的代码修改使事情发生了:

  1. 使用 @Column(updatable=false)
  2. 使 lockTime 字段不可变
  3. 使用 HQL 更新锁具

这是最后的LockManagerclass

@Override
public LockInfo lock(final Class<? extends Lockable> clazz, final Serializable id) throws NotFoundException
{
    try
    {
        return hibernateTemplate.execute(new HibernateCallback<LockInfo>()
        {

            @Override
            public LockInfo doInHibernate(Session session) throws HibernateException, SQLException
            {
                Date lockTime = new Date();
                Query q = session.createQuery(String.format("update %1s item set item.lockTime = :lock where item.id = :id and item.lockTime is null", clazz.getSimpleName()))
                                 .setDate("lock", lockTime)
                                 .setParameter("id", id);

                return q.executeUpdate() > 0 ? new LockInfo(clazz, id, lockTime) : null;
            }
        });
    }
    catch (HibernateException ex)
    {
        if (ex.getCause() instanceof NotFoundException)
            throw (NotFoundException) ex.getCause();
        throw ex;
    }
}

@Override
public void unlock(final LockInfo lock) throws NotFoundException, InvalidOperationException, IllegalArgumentException, ObjectNotLockedException
{
    try
    {
        hibernateTemplate.execute(new HibernateCallback<Void>()
        {

            @Override
            public Void doInHibernate(Session session) throws HibernateException, SQLException
            {
                Query q = session.createQuery(String.format("update %1s item set item.lockTime = null where item.id = :id and item.lockTime = :lock", lock.getClazz()
                                                                                                                                                          .getSimpleName()))
                                 .setDate("lock", lock.getLockDate())
                                 .setParameter("id", lock.getId());

                if (q.executeUpdate() == 1)
                    return null;
                else
                    throw new HibernateException(new ObjectNotLockedException(lock.getId()));
            }
        });
    }
    catch (HibernateException ex)
    {
        if (ex.getCause() instanceof NotFoundException)
            throw (NotFoundException) ex.getCause();
        throw ex;
    }

}

理由

Hibernate 自动将标记为 updatable=false 的列排除在通过 Session 更新之外。但是,如果您 运行 在不可修改的字段上使用普通的旧 HQL 语句,则不会注意到