文档章节

Hibernate乐观锁引发的StaleObjectStateException分析

ksfzhaohui
 ksfzhaohui
发布于 2017/07/12 19:48
字数 2044
阅读 276
收藏 0
点赞 0
评论 0

前言
最近一个项目中使用了Hibernate的乐观锁,不巧的是出现了乐观锁最容易报的错:org.hibernate.StaleObjectStateException: Row was updated or deleted by another transaction
下面将通过一个模拟的实例重现问题,并做相应的分析。

乐观锁的作用
乐观锁的主要作用是为了解决事务并发带来的问题,相对于悲观锁而言,乐观锁机制采取了更加宽松的加锁机制;
悲观锁大多数情况下依靠数据库的锁机制实现,以保证操作最大程度的独占性,但随之而来的就是数据库性能的大量开销,特别是对长事务而言;
乐观锁机制在一定程度上解决了这个问题;乐观锁,大多是基于数据版本(Version)记录机制实现;

乐观锁原理:读取出数据时,将此版本号一同读出,之后更新时,对此版本号加一;
此时,将提交数据的版本数据与数据库表对应记录的当前版本信息进行比对,如果提交的数据版本号大于数据库表当前版本号,则予以更新,否则认为是过期数据;
当出现version过期时,会抛出如下错误:

public StaleObjectStateException(String persistentClass, Serializable identifier) {
    super("Row was updated or deleted by another transaction (or unsaved-value mapping was incorrect)");
    this.entityName = persistentClass;
    this.identifier = identifier;
}

本地重现
1.MYSQL建表SQL如下

CREATE TABLE `test_bean` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT,
  `version` int(11) NOT NULL,
  `status` int(11) DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8;
 
INSERT INTO `test_bean` VALUES ('1', '1', '1');

其中的version字段,就是用来做乐观锁处理的

2.实体TestBean以及TestBean.hbm.xml

public class TestBean {
 
    private long id;
    private int version;
    private int status;
}

TestBean.hbm.xml如下:

<hibernate-mapping package="zh.maven.hibernate_version">
    <class name="TestBean" table="test_bean" optimistic-lock="version">
        <id name="id" column="id">
            <generator class="native" />
        </id>
        <version name="version" column="version" type="integer"></version>
        <property name="status" />
    </class>
</hibernate-mapping>

3.主要代码
其中包括FirstService调用SecondService实现查询和更新,SecondService调用SecondDao实现查询和更新,代码如下:

public class FirstService {
 
    private SecondService secondService;
 
    public void modifyProcess() {
        TestBean testBean = secondService.get(1L);
        printLog(testBean);
        testBean.setStatus(4);
        secondService.update(testBean);
        printLog(testBean);
        sendNotify(testBean);
    }
 
    private void sendNotify(TestBean testBean) {
        testBean.setStatus(20);
        secondService.update(testBean);
        printLog(testBean);
    }
    ......
}
public class SecondService {
 
    private SecondDao secondDao;
 
    public TestBean get(long id) {
        return secondDao.get(id);
    }
 
    public void update(TestBean testBean) {
        secondDao.update(testBean);
    }
    ......
}
public class SecondDao extends HibernateDaoSupport {
 
    public TestBean get(long id) {
        return (TestBean) getHibernateTemplate().get(TestBean.class, 1L);
    }
 
    public void update(TestBean testBean) {
        getHibernateTemplate().update(testBean);
 
    }
}

3.核心配置spring-config-datasource.xml

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:aop="http://www.springframework.org/schema/aop"
    xmlns:tx="http://www.springframework.org/schema/tx"
    xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd
        http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx.xsd">
 
 
    <bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource"
        destroy-method="close">
        <property name="driverClassName" value="com.mysql.jdbc.Driver" />
        <property name="url"
            value="jdbc:mysql:///test?useUnicode=true&amp;characterEncoding=utf-8" />
        <property name="username" value="root" />
        <property name="password" value="root" />
        <property name="maxActive" value="100" />
        <property name="maxIdle" value="30" />
        <property name="maxWait" value="10000" />
    </bean>
    <bean id="sessionFactory"
        class="org.springframework.orm.hibernate3.LocalSessionFactoryBean"
        lazy-init="false">
        <property name="dataSource" ref="dataSource" />
        <property name="configLocation" value="classpath:hibernate.cfg.xml"></property>
        <property name="mappingLocations" value="TestBean.hbm.xml"></property>
    </bean>
    <bean id="transactionManager"
        class="org.springframework.orm.hibernate3.HibernateTransactionManager">
        <property name="sessionFactory" ref="sessionFactory" />
    </bean>
 
    <bean id="secondDao" class="zh.maven.hibernate_version.SecondDao">
        <property name="sessionFactory" ref="sessionFactory"></property>
    </bean>
    <bean id="secondService" class="zh.maven.hibernate_version.impl.service.SecondService">
        <property name="secondDao" ref="secondDao"></property>
    </bean>
    <bean id="firstService" class="zh.maven.hibernate_version.impl.service.FirstService">
        <property name="secondService" ref="secondService"></property>
    </bean>
 
    <aop:config>
        <aop:advisor pointcut="execution(* zh.maven..*Service*.*(..))"
            advice-ref="txAdvice" order="1" />
    </aop:config>
    <tx:advice id="txAdvice" transaction-manager="transactionManager">
        <tx:attributes>
            <tx:method name="update" propagation="REQUIRES_NEW"
                rollback-for="java.lang.Exception" />
            <tx:method name="*" propagation="SUPPORTS" rollback-for="java.lang.Exception" />
        </tx:attributes>
    </tx:advice
</beans>

配置了数据源,事务管理器,实体bean以及Spring AOP方式管理事务;为update方法配置了独立事务(REQUIRES_NEW),其他方法配置了,其他方法配置了SUPPORTS;
REQUIRES_NEW:每次都会新建一个事务,并且同时将上下文中的事务挂起,执行当前新建事务完成以后,上下文事务恢复再执行;
SUPPORTS:如果上下文存在事务,则支持事务加入事务,如果没有事务,则使用非事务的方式执行。

4.测试
测试类如下:

public class Test {
 
    public static void main(String[] args) throws Exception {
        ApplicationContext context = new ClassPathXmlApplicationContext("spring-config-datasource.xml");
        FirstService firstService = (FirstService) context.getBean("firstService");
        firstService.modifyProcess();
    }
}

运行结果如下:

Hibernate: select testbean0_.id as id0_0_, testbean0_.version as version0_0_, testbean0_.status as status0_0_ from test_bean testbean0_ where testbean0_.id=?
id:1,version=1,status=1
Hibernate: update test_bean set version=?, status=? where id=? and version=?
id:1,version=2,status=4
Hibernate: update test_bean set version=?, status=? where id=? and version=?
id:1,version=3,status=20
Hibernate: update test_bean set version=?, status=? where id=? and version=?
Exception in thread "main" org.springframework.orm.hibernate3.HibernateOptimisticLockingFailureException: Object of class [zh.maven.hibernate_version.TestBean] 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): [zh.maven.hibernate_version.TestBean#1]
    at org.springframework.orm.hibernate3.SessionFactoryUtils.convertHibernateAccessException(SessionFactoryUtils.java:669)
    at org.springframework.orm.hibernate3.SpringSessionSynchronization.beforeCommit(SpringSessionSynchronization.java:143)
    at org.springframework.transaction.support.TransactionSynchronizationUtils.triggerBeforeCommit(TransactionSynchronizationUtils.java:72)
    at org.springframework.transaction.support.AbstractPlatformTransactionManager.triggerBeforeCommit(AbstractPlatformTransactionManager.java:905)
    at org.springframework.transaction.support.AbstractPlatformTransactionManager.processCommit(AbstractPlatformTransactionManager.java:715)
    at org.springframework.transaction.support.AbstractPlatformTransactionManager.commit(AbstractPlatformTransactionManager.java:701)
    at org.springframework.transaction.interceptor.TransactionAspectSupport.commitTransactionAfterReturning(TransactionAspectSupport.java:321)
    at org.springframework.transaction.interceptor.TransactionInterceptor.invoke(TransactionInterceptor.java:116)
    at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:171)
    at org.springframework.aop.interceptor.ExposeInvocationInterceptor.invoke(ExposeInvocationInterceptor.java:89)
    at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:171)
    at org.springframework.aop.framework.Cglib2AopProxy$DynamicAdvisedInterceptor.intercept(Cglib2AopProxy.java:635)
    at zh.maven.hibernate_version.impl.service.FirstService$$EnhancerByCGLIB$$62556ba7.modifyProcess(<generated>)
    at zh.maven.hibernate_version.Test.main(Test.java:14)

代码中一共执行了3次sql操作,分别是:get和2次update,为什么会出现第四条update,并且因为第四条update导致了StaleObjectStateException;
数据库结果:[1,3,20],2次update,version:3,status:20,表示数据库中的数据是我们期望的。

分析
这种情况一般都是由于配置的事务引发的,所以从Spring AOP事务入手;update配置了独立的事务,2次正常更新不会有什么影响;关键在于配置其他方法为SUPPORTS,
但是SUPPORTS表示:如果上下文存在事务,则支持事务加入事务,如果没有事务,则使用非事务的方式执行,所以应该是不会提交事务的,直接深入源码查看;

Spring Aop方式的引入所有被过滤的方法都会经过拦截器TransactionInterceptor中,invoke方法中有如下代码:

TransactionInfo txInfo = createTransactionIfNecessary(txAttr, joinpointIdentification);

此方法用来创建事务信息,进入方法会发现通过PlatformTransactionManager来获取DefaultTransactionStatus对象,此对象里面有2个重要的属性分别是:

private final boolean newTransaction;
private final boolean newSynchronization;

getTransaction的部分代码如下:

public final TransactionStatus getTransaction(TransactionDefinition definition) throws TransactionException {
    ......
    else if (definition.getPropagationBehavior() == TransactionDefinition.PROPAGATION_REQUIRED ||
                definition.getPropagationBehavior() == TransactionDefinition.PROPAGATION_REQUIRES_NEW ||
            definition.getPropagationBehavior() == TransactionDefinition.PROPAGATION_NESTED) {
        try {
                doBegin(transaction, definition);
            }
        ......
         
    }
    else {
            // Create "empty" transaction: no actual transaction, but potentially synchronization.
            boolean newSynchronization = (getTransactionSynchronization() == SYNCHRONIZATION_ALWAYS);
            return newTransactionStatus(definition, null, true, newSynchronization, debugEnabled, null);
        }
}

PROPAGATION_REQUIRED,PROPAGATION_REQUIRES_NEW,PROPAGATION_NESTED这三种传播行为是会开启事务newTransaction=true
其他传播方式包括这里配置的SUPPORTS分别是:newTransaction=false,newSynchronization=true,默认的transactionSynchronization=SYNCHRONIZATION_ALWAYS;
Spring针对事务同步提供了类:TransactionSynchronization,其中有多个接口:beforeCommit,afterCommit等,分别事务提交之前,之后可以做相应的处理;
这里主要关注beforeCommit方法,因为事务传播行为是SUPPORTS,所以不会doCommit,但是会triggerBeforeCommit;
TransactionSynchronization具体实现类:SpringSessionSynchronization,其中对beforeCommit有如下实现:

public void beforeCommit(boolean readOnly) throws DataAccessException {
        if (!readOnly) {
            Session session = getCurrentSession();
            // Read-write transaction -> flush the Hibernate Session.
            // Further check: only flush when not FlushMode.NEVER/MANUAL.
            if (!session.getFlushMode().lessThan(FlushMode.COMMIT)) {
                try {
                    SessionFactoryUtils.logger.debug("Flushing Hibernate Session on transaction synchronization");
                    session.flush();
                }
                catch (HibernateException ex) {
                    if (this.jdbcExceptionTranslator != null && ex instanceof JDBCException) {
                        JDBCException jdbcEx = (JDBCException) ex;
                        throw this.jdbcExceptionTranslator.translate(
                                "Hibernate flushing: " + jdbcEx.getMessage(), jdbcEx.getSQL(), jdbcEx.getSQLException());
                    }
                    throw SessionFactoryUtils.convertHibernateAccessException(ex);
                }
            }
        }
    }

方法中首先获取当前的Session,然后对session进行flush操作,这里的session是get方法调用创建的session,和update操作创建的session没有关系;
session会保存最近一次与数据库操作的信息记录的version=1,而TestBean从get到update始终都唯一对象,所以此时的TestBean已经是[1,3,20],
所以flush的时候会执行更新操作,更新语句如下:

update test_bean set version=2, status=20 where id=1 and version=3

set version=2因为保存的version=1,但是在update时候会加1,而此时数据库version为3,所以报StaleObjectStateException。

解决
1.不给其他方法配置事务,直接移除

<tx:method name="*" propagation="SUPPORTS" rollback-for="java.lang.Exception" />

2.因为默认的transactionSynchronization=SYNCHRONIZATION_ALWAYS,值为0,可以配置为SYNCHRONIZATION_NEVER

<bean id="transactionManager"
        class="org.springframework.orm.hibernate3.HibernateTransactionManager">
        <property name="sessionFactory" ref="sessionFactory" />
        <property name="transactionSynchronization" value="2"></property>
</bean>

3.从代码入手从get对象到update一直在对持久化状态对象进行操作,与当前session关联,导致最后flush的时候dirtyCheck的时候通过了而导致update操作;
所以可以对SecondService做如下修改:

public TestBean get(long id) {
    TestBean testBean= secondDao.get(id);
    return new TestBean(testBean.getId(), testBean.getVersion(), testBean.getStatus());
}

获取到的TestBean是一个临时的值,与当前session没有关联,与session关联的是TestBean testBean= secondDao.get(id)此对象后续没有变更,所以最后不会触发update操作。

详细代码svn地址:http://code.taobao.org/svn/temp-pj/hibernate-version

© 著作权归作者所有

共有 人打赏支持
ksfzhaohui

ksfzhaohui

粉丝 301
博文 128
码字总数 158547
作品 3
南京
高级程序员
Hibernate_Optimistic Lock_乐观锁

HibernateOptimistic Lock乐观锁 相对悲观锁而言,乐观锁机制采取了更加宽松的加锁机制。悲观锁大多数情况下依靠数据库的锁机制实现,以保证操作最大程度的独占性。但随之而来的就是数据库性...

秋风醉了
2014/12/02
0
0
hibernate乐观锁 StaleObjectStateException 没有实现捕捉

在java程序内采用hibernate乐观锁,版本号后台数据库有增加,但用try catch捕捉不到StaleObjectStateException 异常。 求救!谢谢!

wuzhx
2013/03/14
409
4
hibernate 乐观锁与悲观锁使用

总结一句话概述 当多个事务同时使用相同数据时会导致并发问题,此时只能用锁来限制。 悲观锁:直接锁住数据库,一个用完,下个才能用,开销大。 乐观锁:数据库中增大version字段,每次提交时...

长平狐
2013/01/06
437
0
Java程序员从笨鸟到菜鸟之(七十六)细谈Hibernate(十八)悲观锁和乐观锁解决hibernate并发

锁( locking ),这个概念在我们学习多线程的时候曾经接触过,其实这里的锁和多线程里面处理并发的锁是一个道理,都是暴力的把资源归为自己所有。这里我们用到锁的目的就是通过一些机制来保...

长平狐
2012/11/12
94
0
Hibernate的乐观锁与悲观锁

转帖:谢谢分享 锁( locking ) 业务逻辑的实现过程中,往往需要保证数据访问的排他性。如在金融系统的日终结算处理中,我们希望针对某个 cut-off 时间点的数据进行处理,而不希望在结算进行...

four
2011/03/24
0
0
Hibernate事务和并发控制

Hibernate事务和并发控制 ++YONG原创,转载请注明 1. 事务介绍: 1.1. 事务的定义: 事务就是指作为单个逻辑工作单元执行的一组数据操作,这些操作要么必须全部成功,要么必须全部失败,以保...

mrliuze
2015/06/03
0
0
大数据量下高并发同步的讲解(不看,保证你后悔)

对于我们开发的网站,如果网站的访问量非常大的话,那么我们就需要考虑相关的并发访问问题了。而并发问题是绝大部分的程序员头疼的问题, 但话又说回来了,既然逃避不掉,那我们就坦然面对吧...

天天顺利
2015/10/15
200
0
hibernate 锁机制

Hibernate 悲观锁,乐观锁1.悲观锁 它指的是对数据被外界修改持保守态度。假定任何时刻存取数据时,都可能有另一个客户也正在存取同一笔数据,为了保持数据被操作的一致性,于是对数据采取了数...

世界和平维护者
2016/07/21
11
0
mysql事务和锁 SELECT FOR UPDATE

事务: 当然有的人用begin /begin work .推荐用START TRANSACTION 是SQL-99标准启动一个事务。 当用set autocommit = 0 的时候,你以后所有的sql都将作为事务处理,直到你用commit确认或 ro...

SibylY
2016/08/22
172
0
Hibernate OGM 4.1.0.Beta8 发布,性能提升

Hibernate OGM 4.1.0.Beta8 发布,此版本提升了性能,减少了数据往返存储,优化数据存储的乐观锁支持,提供原子性的 find-and-update 操作。 此版本同时也解决了一些 bug,还有一些 hood 下的...

叶秀兰
2014/10/31
2.1K
8

没有更多内容

加载失败,请刷新页面

加载更多

下一页

谈谈神秘的ES6——(一)初识ECMAScript

谈谈神秘的ES6——(一)初识ECMAScript 在《零基础入门JavaScript》我们就说过,ECMAScript是JavaScript的核心,是JavaScript语法和语义的解释器,同时也是一个标准。而ECMAScript标准其实也...

JandenMa
53分钟前
1
0
第16章 Tomcat配置

16.1 Tomcat介绍 ####Tomcat介绍 LNMP架构针对的开发语言是PHP语言,php 是一门开发web程序非常流行的语言,早些年流行的是asp,在Windows平台上运行的一种编程语言,但安全性差,就网站开发...

Linux学习笔记
今天
0
0
流利阅读笔记29-20180718待学习

高等教育未来成谜,前景到底在哪里? Ray 2018-07-18 1.今日导读 在这个信息爆炸的年代,获取知识是一件越来越容易的事情。人们曾经认为,如此的时代进步会给高等教育带来众多便利。但事实的...

aibinxiao
今天
11
0
OSChina 周三乱弹 —— 你被我从 osc 老婆们名单中踢出了

Osc乱弹歌单(2018)请戳(这里) 【今日歌曲】 @小鱼丁:分享五月天的单曲《后来的我们 (电影《后来的我们》片名曲)》: 《后来的我们 (电影《后来的我们》片名曲)》- 五月天 手机党少年们想...

小小编辑
今天
418
18
Spring Boot Admin 2.0开箱体验

概述 在我之前的 《Spring Boot应用监控实战》 一文中,讲述了如何利用 Spring Boot Admin 1.5.X 版本来可视化地监控 Spring Boot 应用。说时迟,那时快,现在 Spring Boot Admin 都更新到 ...

CodeSheep
今天
4
0
Python + Selenium + Chrome 使用代理 auth 的用户名密码授权

米扑代理,全球领导的代理品牌,专注代理行业近十年,提供开放、私密、独享代理,并可免费试用 米扑代理官网:https://proxy.mimvp.com 本文示例,是结合米扑代理的私密、独享、开放代理,专...

sunboy2050
今天
0
0
实现异步有哪些方法

有哪些方法可以实现异步呢? 方式一:java 线程池 示例: @Test public final void test_ThreadPool() throws InterruptedException { ScheduledThreadPoolExecutor scheduledThre......

黄威
今天
1
0
linux服务器修改mtu值优化cpu

一、jumbo frames 相关 1、什么是jumbo frames Jumbo frames 是指比标准Ethernet Frames长的frame,即比1518/1522 bit大的frames,Jumbo frame的大小是每个设备厂商规定的,不属于IEEE标准;...

六库科技
今天
0
0
牛客网刷题

1. 二维数组中的查找(难度:易) 题目描述 在一个二维数组中(每个一维数组的长度相同),每一行都按照从左到右递增的顺序排序,每一列都按照从上到下递增的顺序排序。请完成一个函数,输入...

大不了敲一辈子代码
今天
0
0
linux系统的任务计划、服务管理

linux任务计划cron 在linux下,有时候要在我们不在的时候执行一项命令,或启动一个脚本,可以使用任务计划cron功能。 任务计划要用crontab命令完成 选项: -u 指定某个用户,不加-u表示当前用...

黄昏残影
昨天
0
0

没有更多内容

加载失败,请刷新页面

加载更多

下一页

返回顶部
顶部