JPA乐观锁异常(OptimisticLockingFailureException)解决方案

1 背景

项目持久层框架使用spring-data-jpa,jpa实现采用hibernate。实体使用乐观锁的方式加锁,也就是添加version字段。

    @Version
    private Long version;

乐观锁:给数据加一个版本, 每一操作数据就更新版本,不会上锁,但是在更新的时候你会判断这期间有没有人去更新这个数据

悲观锁:给数据加了一把锁 ,同事务只能一个线程进行操作,使用完了锁释放, 没释放前后面想要操作的人就得排队 ,效率低,但是很安全

2 问题描述

异常信息:

org.springframework.orm.ObjectOptimisticLockingFailureException: Object of class [com.sto.station.model.PracticalVehicleInfo] with identifier [81860]: optimistic locking failed; nested exception is org.hibernate.StaleObjectStateException: Row was updated or deleted by another transaction (or unsaved-value mapping was incorrect) : [com.sto.station.model.PracticalVehicleInfo#81860]
	at org.springframework.orm.jpa.vendor.HibernateJpaDialect.convertHibernateAccessException(HibernateJpaDialect.java:337)
	at org.springframework.orm.jpa.vendor.HibernateJpaDialect.translateExceptionIfPossible(HibernateJpaDialect.java:255)
	at org.springframework.orm.jpa.AbstractEntityManagerFactoryBean.translateExceptionIfPossible(AbstractEntityManagerFactoryBean.java:531)
	at org.springframework.dao.support.ChainedPersistenceExceptionTranslator.translateExceptionIfPossible(ChainedPersistenceExceptionTranslator.java:61)
	at org.springframework.dao.support.DataAccessUtils.translateIfNecessary(DataAccessUtils.java:242)
	at org.springframework.dao.support.PersistenceExceptionTranslationInterceptor.invoke(PersistenceExceptionTranslationInterceptor.java:154)
	at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:186)
	at org.springframework.data.jpa.repository.support.CrudMethodMetadataPostProcessor$CrudMethodMetadataPopulatingMethodInterceptor.invoke(CrudMethodMetadataPostProcessor.java:149)
	at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:186)
	at org.springframework.aop.interceptor.ExposeInvocationInterceptor.invoke(ExposeInvocationInterceptor.java:95)
	at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:186)
	at org.springframework.aop.framework.JdkDynamicAopProxy.invoke(JdkDynamicAopProxy.java:212)
	at com.sun.proxy.$Proxy144.updateDirtyData(Unknown Source)
	at com.sto.station.schedule.PortSchedule.processDirtyDataJobHandler(PortSchedule.java:287)
	at com.sto.station.schedule.PortSchedule$$FastClassBySpringCGLIB$$9503b228.invoke(<generated>)
	at org.springframework.cglib.proxy.MethodProxy.invoke(MethodProxy.java:218)
	at org.springframework.aop.framework.CglibAopProxy$CglibMethodInvocation.invokeJoinpoint(CglibAopProxy.java:771)
	at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:163)
	at org.springframework.aop.framework.CglibAopProxy$CglibMethodInvocation.proceed(CglibAopProxy.java:749)
	at org.springframework.transaction.interceptor.TransactionAspectSupport.invokeWithinTransaction(TransactionAspectSupport.java:367)
	at org.springframework.transaction.interceptor.TransactionInterceptor.invoke(TransactionInterceptor.java:118)
	at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:186)
	at org.springframework.aop.framework.CglibAopProxy$CglibMethodInvocation.proceed(CglibAopProxy.java:749)
	at org.springframework.aop.framework.CglibAopProxy$DynamicAdvisedInterceptor.intercept(CglibAopProxy.java:691)
	at com.sto.station.schedule.PortSchedule$$EnhancerBySpringCGLIB$$43901398.processDirtyDataJobHandler(<generated>)
	at sun.reflect.GeneratedMethodAccessor775.invoke(Unknown Source)
	at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
	at java.lang.reflect.Method.invoke(Method.java:498)
	at com.xxl.job.core.handler.impl.MethodJobHandler.execute(MethodJobHandler.java:31)
	at com.xxl.job.core.thread.JobThread.run(JobThread.java:163)
Caused by: org.hibernate.StaleObjectStateException: Row was updated or deleted by another transaction (or unsaved-value mapping was incorrect) : [com.sto.station.model.PracticalVehicleInfo#81860]
	at org.hibernate.persister.entity.AbstractEntityPersister.check(AbstractEntityPersister.java:2651)
	at org.hibernate.persister.entity.AbstractEntityPersister.update(AbstractEntityPersister.java:3495)
	at org.hibernate.persister.entity.AbstractEntityPersister.updateOrInsert(AbstractEntityPersister.java:3358)
	at org.hibernate.persister.entity.AbstractEntityPersister.update(AbstractEntityPersister.java:3772)
	at org.hibernate.action.internal.EntityUpdateAction.execute(EntityUpdateAction.java:201)
	at org.hibernate.engine.spi.ActionQueue.executeActions(ActionQueue.java:604)
	at org.hibernate.engine.spi.ActionQueue.lambda$executeActions$1(ActionQueue.java:478)
	at java.util.LinkedHashMap.forEach(LinkedHashMap.java:684)
	at org.hibernate.engine.spi.ActionQueue.executeActions(ActionQueue.java:475)
	at org.hibernate.event.internal.AbstractFlushingEventListener.performExecutions(AbstractFlushingEventListener.java:348)
	at org.hibernate.event.internal.DefaultFlushEventListener.onFlush(DefaultFlushEventListener.java:40)
	at org.hibernate.event.service.internal.EventListenerGroupImpl.fireEventOnEachListener(EventListenerGroupImpl.java:102)
	at org.hibernate.internal.SessionImpl.doFlush(SessionImpl.java:1362)
	at org.hibernate.internal.SessionImpl.flush(SessionImpl.java:1349)
	at org.hibernate.query.internal.NativeQueryImpl.beforeQuery(NativeQueryImpl.java:267)
	at org.hibernate.query.internal.AbstractProducedQuery.list(AbstractProducedQuery.java:1591)
	at org.hibernate.query.Query.getResultList(Query.java:165)
	at org.springframework.data.jpa.repository.query.JpaQueryExecution$CollectionExecution.doExecute(JpaQueryExecution.java:126)
	at org.springframework.data.jpa.repository.query.JpaQueryExecution.execute(JpaQueryExecution.java:88)
	at org.springframework.data.jpa.repository.query.AbstractJpaQuery.doExecute(AbstractJpaQuery.java:154)
	at org.springframework.data.jpa.repository.query.AbstractJpaQuery.execute(AbstractJpaQuery.java:142)
	at org.springframework.data.repository.core.support.QueryExecutorMethodInterceptor$QueryMethodInvoker.invoke(QueryExecutorMethodInterceptor.java:195)
	at org.springframework.data.repository.core.support.QueryExecutorMethodInterceptor.doInvoke(QueryExecutorMethodInterceptor.java:152)
	at org.springframework.data.repository.core.support.QueryExecutorMethodInterceptor.invoke(QueryExecutorMethodInterceptor.java:130)
	at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:186)
	at org.springframework.data.projection.DefaultMethodInvokingMethodInterceptor.invoke(DefaultMethodInvokingMethodInterceptor.java:80)
	at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:186)
	at org.springframework.transaction.interceptor.TransactionAspectSupport.invokeWithinTransaction(TransactionAspectSupport.java:367)
	at org.springframework.transaction.interceptor.TransactionInterceptor.invoke(TransactionInterceptor.java:118)
	at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:186)
	at org.springframework.dao.support.PersistenceExceptionTranslationInterceptor.invoke(PersistenceExceptionTranslationInterceptor.java:139)
	... 24 common frames omitted

日志中偶尔抛出org.springframework.orm.ObjectOptimisticLockingFailureException: Object of class xxx optimistic locking failed异常。底层异常信息是hibernate抛出的org.hibernate.StaleObjectStateException: Row was updated or deleted by another transaction 。排查后发现是jpa并发更新同一个实体引起的。

这种异常一般是由于数据库里的某一条记录的某一个版本对应的信息,同时被两个事务读取到并试图进行写操作(包括更新和删除);这种情况下第一个写成功的事务不会有影响,第二个事务再对同一版本的同一记录进行写操作时,抛出乐观锁异常ObjectOptimisticLockingFailureException。

3 问题复现

3.1 全局异常拦截

将乐观锁异常加到全局异常拦截器中,封装该异常信息(非必要条件,可加可不加)。

3.2 编写服务端测试方法

Controller:

Service:

3.3 多线程模拟并发测试类

package com.test.service;

import cn.hutool.http.HttpUtil;
import org.apache.commons.lang3.RandomStringUtils;

import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

public class MyTest {

    public static void main(String[] args) throws Exception {
        ThreadPoolExecutor executor = new ThreadPoolExecutor(2000, 5000, 2, TimeUnit.MINUTES,
                                                             new ArrayBlockingQueue<Runnable>(5000));
        // 使用CountDownLatch模拟并发请求
        CountDownLatch latch = new CountDownLatch(1);
        // 模拟多个用户(i小于几就是几个)
        for (int i = 0; i < 6; i++) {
            JpaOptimisticLockTest jpaTest = new JpaOptimisticLockTest(latch);
            executor.execute(jpaTest);
        }
        // 计数器減一  所有线程释放 并发访问
        latch.countDown();
    }

    static class JpaOptimisticLockTest implements Runnable {
        CountDownLatch latch;

        public JpaOptimisticLockTest(CountDownLatch latch) {
            this.latch = latch;
        }

        @Override
        public void run() {
            long starTime = 0;
            // 使用随机字符(如果是相同的,走更新不会触发异常;jpa相同数据行记录version不会变)
            String randomOrgName = RandomStringUtils.randomAlphanumeric(10);
            try {
                starTime = System.currentTimeMillis();
                // 调用await()方法的线程会被挂起,它会等待直到count值为0才继续执行
                latch.await();
                System.out.println("请求开始了");
                // 调用服务接口
                String s = HttpUtil.get("http://127.0.0.1:8080/test?id=4191&orgName="+randomOrgName);
                System.out.println("返回的内容为:"+s);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            long endTime = System.currentTimeMillis();
            Long t = endTime - starTime;
            System.out.println(t / 1000F + "秒");
        }
    }

}

运行结果:

模拟2个线程并发:

服务端:

模拟10个线程并发:

模拟100个线程并发:

可以看到在多线程并发访问接口,用jpa乐观锁的形式修改数据时,大部分都是异常状态,没有办法成功更新数据行。

4 解决方案

JPA乐观锁异常有多种解决方案,主要包括重试机制、使用悲观锁、业务层加分布式锁等;根据不同项目的实际业务场景采取各自适合的方式。

4.1 异常添加重试机制

并发场景不多,并发量不大的情况可以使用重试机制,同时为了降低代码侵入,采用注解+AOP实现重试机制。针对存在并发,多线程访问的api方法,且需要失败重试的业务场景,可以加上重试注解。

注解:@RetryJpaOptimisticLockAspect

package com.sto.station.annotation;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
 * @author xiongbangwen
 */
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface RetryJpaOptimisticLock {

    /**
     * 自定义次数 默认3次
     */
    int times() default 3;
}

Aop:RetryJpaOptimisticLockAspect

package com.sto.station.aop;

import com.sto.station.annotation.RetryJpaOptimisticLock;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.core.annotation.Order;
import org.springframework.dao.OptimisticLockingFailureException;
import org.springframework.stereotype.Component;

import java.lang.reflect.Method;

/**
 * jpa乐观锁异常ObjectOptimisticLockingFailureException重试机制 通过AOP统一拦截这种异常并进行一定次数的重试
 *
 * @author xiongbangwen
 * @className RetryOnOptimisticLockingFailure
 * @date 2021/9/16 15:23
 */
@Aspect
@Component
@Order(1)
@Slf4j
public class RetryJpaOptimisticLockAspect {

    private int maxRetries;

    public void setMaxRetries(int maxRetries) {
        this.maxRetries = maxRetries;
    }

    @Pointcut("@annotation(com.sto.station.annotation.RetryJpaOptimisticLock)")
    public void retryOnFailure(){}

    @Around("retryOnFailure()")
    public Object retry(ProceedingJoinPoint pjp) throws Throwable {
        // 获取拦截的方法名
        MethodSignature msig = (MethodSignature) pjp.getSignature();
        // 返回被织入增加处理目标对象
        Object target = pjp.getTarget();
        // 为了获取注解信息
        Method currentMethod = target.getClass().getMethod(msig.getName(), msig.getParameterTypes());
        // 获取注解信息
        RetryJpaOptimisticLock annotation = currentMethod.getAnnotation(RetryJpaOptimisticLock.class);
        // 设置重试次数
        this.setMaxRetries(annotation.times());
        // 重试次数
        int count = 0;
        OptimisticLockingFailureException lockFailureException;
        do {
            count++;
            try {
                // 再次执行业务代码
                return pjp.proceed();
            } catch (OptimisticLockingFailureException ex) {
                log.error("jpa乐观锁异常ObjectOptimisticLockingFailureException重试失败, count:{}",count);
                lockFailureException = ex;
            }
        } while (count < maxRetries);
        log.error("============ jpa乐观锁异常ObjectOptimisticLockingFailureException".concat(String.valueOf(maxRetries)).concat("次机会全部重试失败 ==========="));
        throw lockFailureException;
    }

}

测试:在test方法上添加注解

执行并发测试类,并发设置2或3

服务端:

并发设置10

设置重试次数为10次,再执行测试类 @RetryJpaOptimisticLock(times = 10)

往上递增亦如此

总结:当重试次数>=并发数时,不会出现乐观锁异常,会在重试过程中全部执行成功;当重试次数<并发数时,还是会出现乐观锁异常。

4.2 数据库层面使用悲观锁

写操作使用MySQL行锁的悲观锁(for update),在数据库层面最大程度避免了并发问题,但数据库锁时间长,开销大,更应该在应用层做处理

4.3 Redis分布式锁

服务端业务层添加redis分布式锁:

新增重试机制,retryCount小于等于0 无限循环,一直尝试加锁,retryCount大于0 尝试指定次数后,退出

总结:可以看到返回的version是按前后顺序依次返回的,既redis分布式锁将异步任务变相的转为同步,若是在高并发场景下,对执行顺序有要求的可以使用。

<think>好的,用户现在遇到了持久层异常的问题,想要查找解决方案或原因,特别是需要结合服务器日志和堆栈跟踪分析。首先,我需要理解持久层异常通常涉及哪些方面。持久层一般负责数据存取,比如数据库操作,所以可能的问题包括SQL语句错误、连接问题、事务管理、ORM框架配置错误等。 接下来,用户提到了服务器日志和堆栈跟踪分析,这意味着我需要引导用户如何从日志中定位问题。首先,用户需要找到相关的错误日志,通常日志中会有异常类型和堆栈信息。例如,数据库连接失败、SQL语法错误、约束冲突等都可能被记录下来。 然后,根据常见的持久层异常类型,我应该分点列出可能的原因和对应的解决方案。比如,数据库连接问题可能涉及配置错误或网络问题;SQL语法错误可能需要检查生成的SQL语句;事务管理问题可能需要检查事务配置和隔离级别;ORM框架的配置错误,比如Hibernate的映射文件或注解是否正确;还有可能并发问题导致死锁或乐观锁异常。 另外,用户可能需要具体的日志示例来分析,比如ORA-600错误是Oracle数据库的严重错误,这时候需要检查数据库的健康状况,或者是否有数据文件损坏。引用中的ORA-600案例可能需要参考相关文档或补丁。 还需要提醒用户检查连接池配置,因为连接泄露或超时也会导致持久层异常。此外,代码层面的错误,比如未正确处理异常或资源释放,也是常见原因。 最后,应该建议用户如何逐步排查:首先查看日志定位异常类型,然后根据具体错误分析可能的原因,再针对性地检查配置、SQL、事务等部分。如果问题复杂,可能需要结合数据库的日志一起分析,或者联系技术支持。 还要生成相关问题,帮助用户深入理解,比如如何分析日志中的堆栈跟踪,持久层框架的配置注意事项,以及如何优化数据库连接等。这些相关问题可以引导用户进一步解决问题或预防类似情况。</think>### 持久层异常分析与解决方案 持久层异常通常由数据库操作失败、ORM框架配置错误、事务管理问题或数据一致性冲突引发。以下是基于日志和堆栈跟踪的分析步骤及常见原因: --- #### **1. 日志定位与关键信息提取** - **步骤**: 1. 在服务器日志中搜索关键词:`PersistenceException`、`SQLSyntaxErrorException`、`ConstraintViolationException`、`TransactionRolledbackException`。 2. 提取完整的堆栈跟踪(Stack Trace),重点关注以下部分: - **异常类型**:如`org.hibernate.exception.SQLGrammarException`(SQL语法错误)。 - **触发位置**:如`DAOImpl.save()`(具体代码行)。 - **底层错误码**:如`ORA-00942`(Oracle表不存在)或`MySQL 1064`(语法错误)[^3]。 --- #### **2. 常见原因与解决方案** ##### **(1) 数据库连接问题** - **现象**: 日志出现`Connection refused`、`Connection timed out`或`Cannot get a connection from pool`。 - **原因**: - 数据库服务未启动或网络中断。 - 连接池配置不当(如最大连接数不足)。 - **解决方案**: - 检查数据库状态:`telnet <host> <port>`。 - 调整连接池参数(如Druid、HikariCP): ```yaml # HikariCP示例 spring.datasource.hikari.maximum-pool-size=20 spring.datasource.hikari.connection-timeout=30000 ``` ##### **(2) SQL语法或对象不存在** - **现象**: 日志包含`SQLSyntaxErrorException`或`ORA-00942`。 - **原因**: - 表/列名拼写错误或未创建。 - 动态SQL拼接错误(如MyBatis中`#{}`与`${}`误用)。 - **解决方案**: - 检查ORM映射文件(如Hibernate的`.hbm.xml`或JPA实体类注解)。 - 启用SQL日志验证实际执行的语句(Hibernate配置): ```properties spring.jpa.show-sql=true spring.jpa.properties.hibernate.format_sql=true ``` ##### **(3) 事务管理异常** - **现象**: `TransactionRolledbackException`或`UnexpectedRollbackException`。 - **原因**: - 事务传播行为配置冲突(如嵌套事务未配置`REQUIRES_NEW`)。 - 非受检异常(如`NullPointerException`)未声明回滚。 - **解决方案**: - 显式声明事务回滚条件: ```java @Transactional(rollbackFor = {CustomException.class, SQLException.class}) ``` - 检查事务隔离级别是否与数据库兼容[^2]。 ##### **(4) 数据一致性冲突** - **现象**: `OptimisticLockingFailureException`(乐观锁冲突)或`DeadlockException`。 - **原因**: - 并发更新同一数据未加锁。 - 事务未及时提交导致锁超时。 - **解决方案**: - 使用悲观锁:`SELECT ... FOR UPDATE`。 - 优化事务粒度,减少持有锁的时间。 --- #### **3. 堆栈跟踪分析示例** 假设日志中出现以下错误: ``` Caused by: java.sql.SQLException: ORA-00600: internal error code, arguments: [kghstack_free2], [0x000000000], [], [], [], [], [], [] at oracle.jdbc.driver.T4CTTIoer.processError(T4CTTIoer.java:450) at com.example.dao.UserDAOImpl.save(UserDAOImpl.java:25) ``` - **分析**: - `ORA-00600`是Oracle内部错误,可能与内存管理或数据文件损坏有关。 - 需联系DBA检查数据库健康状态或应用补丁。 ---
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值