带有休眠和触发器的乐观锁定 - “奇怪”行为

Posted

技术标签:

【中文标题】带有休眠和触发器的乐观锁定 - “奇怪”行为【英文标题】:Optimistic locking with hibernate and trigger - "Strange" behaviour 【发布时间】:2015-11-19 13:33:43 【问题描述】:

我想使用 hibernate 中的乐观锁定功能。为此,我为我的表配置了如下映射:

<hibernate-mapping package="org.example.dao.entity">
    <class name="org.example.dao.entity.EmployeeEntity" table="employee" dynamic-update="false">
        <id name="id" column="employee_id">
            <generator class="identity"/>
        </id>
        <version name="version" column="version" type="java.lang.Integer" generated="always"/>
        <property name="firstName" column="first_name" type="java.lang.String"/>
        <set name="projects" table="employee_to_project" inverse="true">
            <key column="employee_id"/>
            <many-to-many column="project_id" class="ProjectEntity"/>
        </set>
    </class>
</hibernate-mapping>

我使用 generate="always" 并在 db 中创建了以下触发器:

CREATE OR REPLACE FUNCTION public.tab_employee_update_version()
  RETURNS trigger
  LANGUAGE plpgsql
AS
$body$
BEGIN
  NEW.version = coalesce(OLD.version,0) + 1;
  RETURN NEW;
END;
$body$
/

然后我运行以下代码:

public static void main(String[] args) 
        ApplicationContext ctx = new ClassPathXmlApplicationContext("context.xml");

        final EmployeeService employeeService = ctx.getBean("employeeService", EmployeeService.class);

        // SELECT
        Employee employee = employeeService.getById(1L);

        // UPDATE
        employeeService.update(employee);    
    

并获得 HibernateOptimisticLockingFailureException,尽管员工记录没有被另一个事务同时更改:

Exception in thread "main" org.springframework.orm.hibernate4.HibernateOptimisticLockingFailureException: Object of class [org.example.dao.entity.EmployeeEntity] with identifier [1]: optimistic locking failed; nested exception is org.hibernate.StaleObjectStateException: Row was updated or deleted by another transaction (or unsaved-value mapping was incorrect) : [org.example.dao.entity.EmployeeEntity#1]
    at org.springframework.orm.hibernate4.SessionFactoryUtils.convertHibernateAccessException(SessionFactoryUtils.java:202)
    at org.springframework.orm.hibernate4.HibernateTransactionManager.convertHibernateAccessException(HibernateTransactionManager.java:730)
    at org.springframework.orm.hibernate4.HibernateTransactionManager.doCommit(HibernateTransactionManager.java:592)
    at org.springframework.transaction.support.AbstractPlatformTransactionManager.processCommit(AbstractPlatformTransactionManager.java:761)
    at org.springframework.transaction.support.AbstractPlatformTransactionManager.commit(AbstractPlatformTransactionManager.java:730)
    at org.springframework.transaction.interceptor.TransactionAspectSupport.commitTransactionAfterReturning(TransactionAspectSupport.java:485)
    at org.springframework.transaction.interceptor.TransactionAspectSupport.invokeWithinTransaction(TransactionAspectSupport.java:291)
    at org.springframework.transaction.interceptor.TransactionInterceptor.invoke(TransactionInterceptor.java:96)
    at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:179)
    at org.springframework.aop.framework.JdkDynamicAopProxy.invoke(JdkDynamicAopProxy.java:207)
    at com.sun.proxy.$Proxy15.update(Unknown Source)
    at org.example.AppMain.main(AppMain.java:25)
    at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:57)
    at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    at java.lang.reflect.Method.invoke(Method.java:606)
    at com.intellij.rt.execution.application.AppMain.main(AppMain.java:134)
Caused by: org.hibernate.StaleObjectStateException: Row was updated or deleted by another transaction (or unsaved-value mapping was incorrect) : [org.example.dao.entity.EmployeeEntity#1]
    at org.hibernate.persister.entity.AbstractEntityPersister.check(AbstractEntityPersister.java:2541)
    at org.hibernate.persister.entity.AbstractEntityPersister.update(AbstractEntityPersister.java:3285)
    at org.hibernate.persister.entity.AbstractEntityPersister.updateOrInsert(AbstractEntityPersister.java:3183)
    at org.hibernate.persister.entity.AbstractEntityPersister.update(AbstractEntityPersister.java:3525)
    at org.hibernate.action.internal.EntityUpdateAction.execute(EntityUpdateAction.java:159)
    at org.hibernate.engine.spi.ActionQueue.executeActions(ActionQueue.java:465)
    at org.hibernate.engine.spi.ActionQueue.executeActions(ActionQueue.java:351)
    at org.hibernate.event.internal.AbstractFlushingEventListener.performExecutions(AbstractFlushingEventListener.java:350)
    at org.hibernate.event.internal.DefaultFlushEventListener.onFlush(DefaultFlushEventListener.java:56)
    at org.hibernate.internal.SessionImpl.flush(SessionImpl.java:1258)
    at org.hibernate.internal.SessionImpl.managedFlush(SessionImpl.java:425)
    at org.hibernate.engine.transaction.internal.jdbc.JdbcTransaction.beforeTransactionCommit(JdbcTransaction.java:101)
    at org.hibernate.engine.transaction.spi.AbstractTransactionImpl.commit(AbstractTransactionImpl.java:177)
    at org.springframework.orm.hibernate4.HibernateTransactionManager.doCommit(HibernateTransactionManager.java:584)
    ... 14 more

事务边界在服务级别(EmployeeService 类用@Transactional 注释标记)。

我调试了代码发现,hibernate在更新的时候会自动增加版本:

Hibernate: select employeeen0_.employee_id as employee1_1_0_, employeeen0_.version as version2_1_0_, employeeen0_.age as age3_1_0_, employeeen0_.first_name as first_na4_1_0_, employeeen0_.last_name as last_nam5_1_0_ from employee employeeen0_ where employeeen0_.employee_id=?
10:36:46,258 TRACE main sql.BasicBinder:81 - binding parameter [1] as [BIGINT] - [1]
10:36:46,270 TRACE main sql.BasicExtractor:78 - extracted value ([version2_1_0_] : [INTEGER]) - [57]
10:36:46,270 TRACE main sql.BasicExtractor:78 - extracted value ([age3_1_0_] : [INTEGER]) - [0]
10:36:46,271 TRACE main sql.BasicExtractor:78 - extracted value ([first_na4_1_0_] : [VARCHAR]) - [Bogumil]
10:36:46,271 TRACE main sql.BasicExtractor:78 - extracted value ([last_nam5_1_0_] : [VARCHAR]) - [Bednarek]
10:36:46,280 TRACE main type.CollectionType:783 - Created collection wrapper: [org.example.dao.entity.EmployeeEntity.projects#1]

Hibernate: select projects0_.employee_id as employee1_1_0_, projects0_.project_id as project_2_2_0_, projectent1_.project_id as project_1_3_1_, projectent1_.name as name2_3_1_ from employee_to_project projects0_ inner join project projectent1_ on projects0_.project_id=projectent1_.project_id where projects0_.employee_id=?
10:36:46,394 TRACE main sql.BasicBinder:81 - binding parameter [1] as [BIGINT] - [1]
10:36:46,397 TRACE main sql.BasicExtractor:78 - extracted value ([project_1_3_1_] : [BIGINT]) - [1]
10:36:46,397 TRACE main sql.BasicExtractor:78 - extracted value ([name2_3_1_] : [VARCHAR]) - [Project1]
10:36:46,398 TRACE main sql.BasicExtractor:78 - extracted value ([employee1_1_0_] : [BIGINT]) - [1]
10:36:46,398 TRACE main sql.BasicExtractor:78 - extracted value ([project_2_2_0_] : [BIGINT]) - [1]
10:36:46,399 TRACE main sql.BasicExtractor:78 - extracted value ([project_1_3_1_] : [BIGINT]) - [2]
10:36:46,399 TRACE main sql.BasicExtractor:78 - extracted value ([name2_3_1_] : [VARCHAR]) - [Project2]
10:36:46,400 TRACE main sql.BasicExtractor:78 - extracted value ([employee1_1_0_] : [BIGINT]) - [1]
10:36:46,400 TRACE main sql.BasicExtractor:78 - extracted value ([project_2_2_0_] : [BIGINT]) - [2]

Hibernate: update employee set age=?, first_name=?, last_name=? where employee_id=? and version=?
10:36:46,426 TRACE main sql.BasicBinder:81 - binding parameter [1] as [INTEGER] - [0]
10:36:46,426 TRACE main sql.BasicBinder:81 - binding parameter [2] as [VARCHAR] - [Bogumil]
10:36:46,427 TRACE main sql.BasicBinder:81 - binding parameter [3] as [VARCHAR] - [Bednarek]
10:36:46,427 TRACE main sql.BasicBinder:81 - binding parameter [4] as [BIGINT] - [1]
10:36:46,427 TRACE main sql.BasicBinder:81 - binding parameter [5] as [INTEGER] - [58]

另外,我发现如果我在同一个事务中选择并更新一条记录,hibernate 不会增加版本并且一切正常。

还有一个观察。从映射文件中删除“set”元素后,一切正常:

<set name="projects" table="employee_to_project" inverse="true">
    <key column="employee_id"/>
    <many-to-many column="project_id" class="ProjectEntity"/>
</set>

谁能解释一下,为什么 hibernate 增加了版本,因此我得到了异常?

有关我的应用程序的更多详细信息:

springframework 版本:4.2.0.RELEASE 休眠版本:4.3.11.Final postgreSQL 版本:9.3

【问题讨论】:

发布堆栈跟踪。通常,堆栈跟踪中至少有一个原因解释了引发 HibernateOptimisticLockingFailureException 的原因。 我添加了堆栈跟踪。抛出异常的原因是因为hibernate在更新之前增加了版本。因此,hibernate 会尝试更新 db 中不存在的记录。 我真的不明白你为什么要创建那个版本更新触发器。你说 Hibernate 做到了,但在你发布的日志中没有证据。 db中version的值为5710:36:46,270 TRACE main sql.BasicExtractor:78 - extracted value ([version2_1_0_] : [INTEGER]) - [57])。然后,当 hibernate 进行更新时,它会将版本增加到 5810:36:46,427 TRACE main sql.BasicBinder:81 - binding parameter [5] as [INTEGER] - [58] 【参考方案1】:

从堆栈跟踪中,问题是“行已被另一个事务更新或删除(或未保存的值映射不正确)”。

如果问题与交易有关,则有三种可能:

您的代码尝试在同一行的两个不同点进行更新(但您已经检查过了,这不是问题) 在之前的测试中,您在同一行出现异常,代码未正确处理回滚,因此未关闭旧事务 在您尝试更新的行上没有关闭事务。也许您使用客户端(toad、sqlserver、mysql workbench、squirrel...)更新了该行并且从未提交数据。

【讨论】:

我写了一个简单的java spring应用程序来执行测试。我运行这个应用程序来执行测试,然后应用程序关闭。没有其他客户端在后台工作(没有 toad、workbench 等)。每次我运行测试时,都会抛出异常。 尝试直接在数据库上检查记录是否在您的应用程序未运行时被锁定。 不是。我运行 SELECT... FOR UPDATE 并且它成功了。

以上是关于带有休眠和触发器的乐观锁定 - “奇怪”行为的主要内容,如果未能解决你的问题,请参考以下文章

休眠乐观锁定..它是如何工作的?

如何在休眠中进行乐观锁定

何时明确排除乐观锁定(休眠)?

MySQL休眠异常

如果设备被锁定,CADislayLink 不会触发

反应阿波罗突变和乐观更新