数据库锁

spring data jpa-纠错之旅-JPA高并发下的乐观锁异常 ObjectOptimisticLockingFailureException

作者:lzp158869557

纠错之旅一

1、详细报错内容:org.springframework.orm.ObjectOptimisticLockingFailureException: Object of class [***.***.**] with identifier [213938]: optimistic locking failed; nested exception is org.hibernate.StaleObjectStateException: Row was updated or deleted by another transaction (or unsaved-value mapping was incorrect)

2、错误解释:对象的乐观锁锁定失败,持久化对象状态异常:这里指的应该是version字段正在被其他事物更新

3、最终找到的原因是spring data jpa没有更新实体方法,只能通过先查询id,根据id保存对象的方法更新实体类,而我的查询和保存方法在service中分别是一个独立的方法,而查询方法的事务注解未添加readonly参数,且业务层又是批量更新操作,所以才报此错误。还有一点就是批量操作时,对一个对象更新两次的话,由于乐观锁的原因只能是死锁。

纠错之旅二

其实呢,我上边的说法是错误的,

下边是关于事务-readonly的说法

如果你一次执行单条查询语句,则没有必要启用事务支持,数据库默认支持SQL执行期间的读一致性; 
如果你一次执行多条查询语句,例如统计查询,报表查询,在这种场景下,多条查询SQL必须保证整体的读一致性,否则,在前条SQL查询之后,后条SQL查询之前,数据被其他用户改变,则该次整体的统计查询将会出现读数据不一致的状态,此时,应该启用事务支持
read-only="true"表示该事务为只读事务,比如上面说的多条查询的这种情况可以使用只读事务,
由于只读事务不存在数据的修改,因此数据库将会为只读事务提供一些优化手段,例如Oracle对于只读事务,不启动回滚段,不记录回滚log。
(1)在JDBC中,指定只读事务的办法为: connection.setReadOnly(true);
(2)在Hibernate中,指定只读事务的办法为: session.setFlushMode(FlushMode.NEVER); 
此时,Hibernate也会为只读事务提供Session方面的一些优化手段
(3)在Spring的Hibernate封装中,指定只读事务的办法为: bean配置文件中,prop属性增加“read-Only”
或者用注解方式@Transactional(readOnly=true)
Spring中设置只读事务是利用上面两种方式(根据实际情况)
在将事务设置成只读后,相当于将数据库设置成只读数据库,此时若要进行写的操作,会出现错误。

原文地址:http://blog.csdn.net/MageShuai/article/details/4544191

故第一个推断是错误的,那第二个推断呢?

同一个事务中对同一个行记录进行两次修改的话,也不会出现死锁的,原因:没有正真的同时的,会排队、依次更新,更加具体的来说是数据库操作大体分为两种,一种是读操作,一种是写操作,读操作不涉及数据的修改是不需要加锁的,写操作要先申请并获得锁的语句(准确来说是事务)先执行,另外一条就等待,直到这个完毕以后再执行,这是两个事务的情况,还要根据隔离级别而定。

深入理解读写操作和锁的关系:读写效率,读的话只需要查询,磁盘操作的话也就是寻址,然后读取就算结束了;而写的话就相对麻烦多了,要先找到可以写的位置,然后写入,写的过程中还不能让正在写的内容被其它线程读取、也不能被其它线程写入;这是就有一个锁或者临界区的概念;同时也可以看出来,对相同的内容,很多线程同时去读不会有什么问题,但很多线程同时去写就不行了。所以,可以看出,读比写要快很多。

故此,我上次的第二个推断也是错误的,我当前得出的结论是并发更新导致的,前提是该表还要有乐观锁。

过程分析:一个事务进行更新操作(即先根据条件查询行记录的id,set要更新的数据并保存数据库。),在更新的过程中,即读之后该行记录发生了另外的一个更新事务(即并发)且第二个更新事务先执行完毕,然后第一个更新事务才执行完毕,那么就导致了上述的错误。

现在,说一下我做出第一次结论的原因:我用了一个webmagic框架,他是一个爬虫框架,一开始我一直以为该框架的单线程是指从爬出到解析到入库都是在同一个线程下按顺序产生的,后来才发现,它的单线程是指只开启一个downloader,不影响其他模块的线程数,且并发的偶然性也比较大,所以就郁闷的得出了第一次的结论。

 

总结一下,其实呢,就是为了避免出现了不可重复读错误,而使用乐观锁,才会出现此异常。如果异常出现的很少且其结果影响在可以忍受的范围内的话可以不理会,否则就使用提高事物隔离级别:@Transactional(isolation = Isolation.REPEATABLE_READ)

 

悲观锁(Pessimistic Lock), 顾名思义,就是很悲观,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会block直到它拿到锁。传统的关系型数据库里边就用到了很多这种锁机制,比如行锁,表锁等,读锁,写锁等,都是在做操作之前先上锁。

乐观锁(Optimistic Lock), 顾名思义,就是很乐观,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,可以使用版本号等机制。乐观锁适用于多读的应用类型,这样可以提高吞吐量,像数据库如果提供类似于write_condition机制的其实都是提供的乐观锁。

两种锁各有优缺点,不可认为一种好于另一种,像乐观锁适用于写比较少的情况下,即冲突真的很少发生的时候,这样可以省去了锁的开销,加大了系统的整个吞吐量。但如果经常产生冲突,上层应用会不断的进行retry,这样反倒是降低了性能,所以这种情况下用悲观锁就比较合适。

一个事务中,对同一条行记录进行多次更新操作,不会产生死锁问题。

事务的传播规律:事务嵌套:一个service层调用另外一个service层,加入两个都有事务注解,且注解没有参数的话(即事务的传播特性是默认的值-required),那么就是第一个service创建事务,第二个service不创建事务。详细请看required的含义

事务-只读:用于多个读操作,防止数据不一致。

数据库事务隔离级别:读未提交--->脏读-->读提交-->不可重复读-->重复读-->幻读-->序列化

 

纠错之旅三-这真的是终极解决方案了

其实呢,我上边的说法还是错误的,好痛苦啊,不过在这个过程中学到了很多!

 

错误重现:jpa只能通过id实现更新,这样的话  只能先通过其他条件查找id,然后根据id更新实体类。当多个线程去更新同一条记录的话,会出现这样一种情况,线程a读过记录尚未进行更新时,线程b也读过记录,当其中一个线程update完后,另一个线程再次修改就会报此错,因为查出记录的version值跟数据库中的version值不想等。

上边的解决方案是错误的,原因是使用Repeatable read事物隔离级别是不允许其他事物破坏数据的完整性,但是不禁止其他事物读取数据(需要说明的是oracle也没有这个事物隔离级别,同时jpa也不支持事务隔离注解,jpa支持事务隔离注解解决方案:http://www.byteslounge.com/tutorials/spring-change-transaction-isolation-level-example)。由此可知加悲观锁也不可行,原因和Repeatable read事物隔离级别相同(jpa使用悲观锁的方法是在dao方法上使用@Lock(value=“”))。可能有的人会说使用Serializable事务隔离级别解决这个问题,其实这个方案还是不行,原因是它会引起另外一个异常:无法连续访问此事务处理 ,原因是事务隔离级别越高,并发性越差,是不是感到无解了,哈哈。

还是有解决方案的:事务重试机制

根据注解进行事务重试

1、注解

 

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

/**
 * 自定义尝试切面接口
 */
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface IsTryAgain {  
    // marker annotation  
}
2、拦截器(这是一个悲观锁事务重试机制,如果想要乐观锁事务重试的话,修改要捕捉的异常即可)

 

 

import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.core.Ordered;
import org.springframework.dao.PessimisticLockingFailureException;
import org.springframework.stereotype.Component;

import ***.annotation.IsTryAgain;

@Aspect
@Component
public class ConcurrentOperationExecutor implements Ordered {

    private static final int DEFAULT_MAX_RETRIES = 2;

    private int maxRetries = DEFAULT_MAX_RETRIES;
    private int order = 1;

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

    @Override
    public int getOrder() {
        return this.order;
    }

    // @Pointcut("execution(* com..creepers.service.impl..saveOrUpdate(..))")
    // public void saveOrUpdate() {
    // }

    @Around(value = "@annotation(isTryAgain)")
    public Object doConcurrentOperation(ProceedingJoinPoint pjp,IsTryAgain isTryAgain) throws Throwable {
        int numAttempts = 0;
        PessimisticLockingFailureException lockFailureException = null;
        do {
            numAttempts++;
            try {
                return pjp.proceed();
            } catch (PessimisticLockingFailureException ex) {
                lockFailureException = ex;
            }
        } while (numAttempts <= this.maxRetries);
        throw lockFailureException;
    }

}

3、在service需要事务重试的方法上添加注解即可

 

现在再看看上边的分析感到自己好弱智啊抓狂

相关推荐:

1、http://blog.csdn.net/qq361301276/article/details/25632241 乐观锁异常解析和解决方案

2、http://blog.csdn.net/Terry_Long/article/details/54291455 jpa的悲观锁使用案例

发表评论
 

2个评论

  • qq_30917349

    不明白 , 全是SB 写的这个方法, 你们不试试吗? 乐观锁, 你捕获悲观锁异常, 异常都捕获错了, 而且就算是 捕获的 正确异常,能捕获到吗 ? 根本捕获不到 。

    2018-01-23 14:54:04回复

  • lzp158869557

    回复qq_30917349: 有疑问就提出,不用骂人,还有这事原创文章,没有亲身试过没有解决问题,我会写博客来让别人骂我吗?异常捕获根本捕获不到?那你应该再去学学java的基础知识了。

    2018-03-14 10:00:20回复

posted @ 2018-10-25 12:13  吃饭了吗  阅读(1010)  评论(0编辑  收藏  举报