使用 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 中,这永远不会发生,因为你不会触摸。
我发现我的锁系统设计得很糟糕:我想问你如何根据以下要求改进我的设计:
- 锁必须尽快生效
- 必须不惜一切代价在方法结束时移除锁(
finally
子句),即使主事务回滚
我正在考虑为锁(列 entityClass
和 entityId
)使用单独的 table,但我想知道我的设计模式(锁列)是否可以改编
在发布问题之前,我花了一天时间重新检查我的设计,并最终自己找到了解决方案。为了以防万一,我想分享给后人。
我之前的分析(新事务被方法的事务覆盖)是正确的,并且基于这样一个事实,即方法的事务最终在解锁事务之后提交,而 tx 序列应该是:lock,process , 解锁.
两个简单的代码修改使事情发生了:
- 使用
@Column(updatable=false)
使 lockTime
字段不可变
- 使用 HQL 更新锁具
这是最后的LockManager
class
@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 语句,则不会注意到
我需要一些帮助来管理我刚刚发现造成的混乱。
我想在我的实体中实施软锁定机制。我不想使用 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 中,这永远不会发生,因为你不会触摸。
我发现我的锁系统设计得很糟糕:我想问你如何根据以下要求改进我的设计:
- 锁必须尽快生效
- 必须不惜一切代价在方法结束时移除锁(
finally
子句),即使主事务回滚
我正在考虑为锁(列 entityClass
和 entityId
)使用单独的 table,但我想知道我的设计模式(锁列)是否可以改编
在发布问题之前,我花了一天时间重新检查我的设计,并最终自己找到了解决方案。为了以防万一,我想分享给后人。
我之前的分析(新事务被方法的事务覆盖)是正确的,并且基于这样一个事实,即方法的事务最终在解锁事务之后提交,而 tx 序列应该是:lock,process , 解锁.
两个简单的代码修改使事情发生了:
- 使用
@Column(updatable=false)
使 - 使用 HQL 更新锁具
lockTime
字段不可变
这是最后的LockManager
class
@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 语句,则不会注意到