使用Tx传播REQUIRES_NEW丢失了Hibernate的更新

时间:2015-03-13 13:32:20

标签: java hibernate transactions

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

我想在我的实体中实现一个软锁定机制。我不想使用Hibernate的锁定功能,因为我的进程是由长时间运行的复合事务组成的。用简短的话来说:我需要在方法运行时将实体标记为Locked,因此并发调用将阻止运行。

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

@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子句)不惜一切代价删除锁
  3. 我正在考虑使用单独的锁表(entityClassentityId列),但我想知道我的设计模式(锁定列)是否可以调整

1 个答案:

答案 0 :(得分:0)

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

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

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

  1. 使用lockTime
  2. 使@Column(updatable=false)字段不可变
  3. 使用HQL更新锁
  4. 这是最后的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
                {
                    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语句,则不会注意到