Java for Web学习笔记(一一二):再谈Entity映射(5)原生SQL和isolation

缘由

我们在动态表格中使用了原生SQL,而在之前,我们讨论了非原生(Entity可以和表格明确对应),在两个事务同时写操作某个数据时,在isolation = Isolation.REPEATABLE_READ时,出现脏读情况是会报告org.springframework.dao.DataIntegrityViolationException的异常。但是原生的情况会有不同。

小例子

在事务中依次处理:

  1. SELECT
  2. 某些数据处理
  3. UPDATE 如果结果 >0,即真的update数据,结束事务;如果==0,则进一步INSERT,结束事务。

在测试小程序中,我们要在异步线程里面运行事务,因此,需要更改异步和配置的优先级别。在一般的真实代码中,是在事物里面调用异步线程,因此一般情况下,我们需要设置@EnableAsync为Ordered.HIGHEST_PRECEDENCE,事务为Ordered.LOWEST_PRECEDENCE,如下:

@EnableAsync(mode = AdviceMode.PROXY, proxyTargetClass = false, order = Ordered.HIGHEST_PRECEDENCE)  
@EnableTransactionManagement(mode = AdviceMode.PROXY, proxyTargetClass = false, order = Ordered.LOWEST_PRECEDENCE) 
public class RootContextConfiguration implements AsyncConfigurer, SchedulingConfigurer

但现在我们要在异步线程里面运行事务,需要改为

@EnableAsync(mode = AdviceMode.PROXY, proxyTargetClass = false, order = Ordered.LOWEST_PRECEDENCE)  
@EnableTransactionManagement(mode = AdviceMode.PROXY, proxyTargetClass = false, order = Ordered.HIGHEST_PRECEDENCE) 
public class RootContextConfiguration implements AsyncConfigurer, SchedulingConfigurer

Isolation.READ_COMMITTED

对于需要原无的数据,如果我们同时运行两个事务,情况如下

Hibernate: SELECT * ......
Hibernate: SELECT * ......
Hibernate: UPDATE ......
Hibernate: UPDATE ......
Hibernate: INSERT ......
Hibernate: INSERT ......
14:29:41.680 [http-nio-8080-exec-4] [WARN ] (JPA) SqlExceptionHelper - SQL Error: 1062, SQLState: 23000
14:29:41.680 [http-nio-8080-exec-4] [ERROR] (JPA) SqlExceptionHelper - Duplicate entry '191.8.3.1' for key 'PRIMARY'
14:29:41.691 [http-nio-8080-exec-4] [ERROR] PolicyAction:46  - Error : javax.persistence.PersistenceException: org.hibernate.exception.ConstraintViolationException: could not execute statement
javax.persistence.PersistenceException: org.hibernate.exception.ConstraintViolationException: could not execute statement
       at org.hibernate.internal.ExceptionConverterImpl.convert(ExceptionConverterImpl.java:149) ~[hibernate-core-5.2.12.Final.jar:5.2.12.Final]
    ......
Caused by: org.hibernate.exception.ConstraintViolationException: could not execute statement
    ......
Caused by: com.mysql.jdbc.exceptions.jdbc4.MySQLIntegrityConstraintViolationException: Duplicate entry '191.8.3.1' for key 'PRIMARY'

Isolation.REPEATABLE_READ

对于需要原无的数据,如果我们同时运行两个事务,情况如下

Hibernate: SELECT * ......
Hibernate: SELECT * ......
Hibernate: UPDATE ......
Hibernate: UPDATE ......
Hibernate: INSERT ......
Hibernate: INSERT ......
14:36:58.375 [http-nio-8080-exec-12] [WARN ] (JPA) SqlExceptionHelper - SQL Error: 1213, SQLState: 40001
14:36:58.375 [http-nio-8080-exec-12] [ERROR] (JPA) SqlExceptionHelper - Deadlock found when trying to get lock; try restarting transaction
14:36:58.384 [http-nio-8080-exec-12] [TRACE] PolicyAction:78  - Error : javax.persistence.PersistenceException: org.hibernate.exception.LockAcquisitionException: could not execute statement
javax.persistence.PersistenceException: org.hibernate.exception.LockAcquisitionException: could not execute statement
     ......
Caused by: org.hibernate.exception.LockAcquisitionException: could not execute statement
     ......
Caused by: com.mysql.jdbc.exceptions.jdbc4.MySQLTransactionRollbackException: Deadlock found when trying to get lock; try restarting transaction

如果数据存在,即执行,则无异常,但是后处理的实际是脏数据,结果很可能是错误的。我们需要在这种情况也能捕获异常

Hibernate: SELECT * ......
Hibernate: SELECT * ......
Hibernate: UPDATE ......
Hibernate: UPDATE ......

Isolation.SERIALIZABLE

Hibernate: SELECT * ......
Hibernate: SELECT * ......
Hibernate: UPDATE ......
Hibernate: UPDATE ......
14:39:34.300 [http-nio-8080-exec-14] [WARN ] (JPA) SqlExceptionHelper - SQL Error: 1213, SQLState: 40001
14:39:34.300 [http-nio-8080-exec-14] [ERROR] (JPA) SqlExceptionHelper - Deadlock found when trying to get lock; try restarting transaction
14:39:34.307 [http-nio-8080-exec-14] [TRACE] PolicyAction:78  - Error : javax.persistence.PersistenceException: org.hibernate.exception.LockAcquisitionException: could not execute statement
javax.persistence.PersistenceException: org.hibernate.exception.LockAcquisitionException: could not execute statement
      ......
Caused by: org.hibernate.exception.LockAcquisitionException: could not execute statement
      ......
Caused by: com.mysql.jdbc.exceptions.jdbc4.MySQLTransactionRollbackException: Deadlock found when trying to get lock; try restarting transaction

小结

  JPA方式 原生SQL方式
两个事务同时insert

org.springframework.dao.DataIntegrityViolationException

对于采用自动流水号方式,insert可能不包括流水号,不在此讨论范围内

  • READ_UNCOMMITTED,READ_COMMITTED,REPEATABLE_READ
    javax.persistence.PersistenceException: org.hibernate.exception.ConstraintViolationException: could not execute statement
  • SERIALIZABLE
    javax.persistence.PersistenceException: org.hibernate.exception.LockAcquisitionException: could not execute statement

两个事务同时update
  • READ_UNCOMMITTED,READ_COMMITTED,REPEATABLE_READ
    先后执行两个update,可能会引发数据混淆
  • SERIALIZABLE
    org.springframework.dao.CannotAcquireLockException

  • READ_UNCOMMITTED,READ_COMMITTED,REPEATABLE_READ
    先后执行两个update,可能会引发数据混淆
  • SERIALIZABLE
    javax.persistence.PersistenceException: org.hibernate.exception.LockAcquisitionException: could not execute statement

事务可以在发生异常的时候进行回滚,并不会在出现脏读的时候将我们事务的方法重新执行一趟,也就是很可能会存在数据的混淆。在预期的不同,REPEATABLE_READ并没有抛出异常,猜测如果SQL属于CRUD的原子操作,是没有区别的,isolation是对复杂SQL语句的保护。在java代码中,一般的情况是多个原子操作的SQL语句,中间含有我们的业务逻辑的java代码。

此外,还要看具体的场景,如果是用户在页面上点来点去的操作,两个事务同时update或者insert的概率几乎为零。对于极为罕见的情况,我的建议需要衡量出告警通知(人工处理),或者为此增加代码之前的权衡,如果对一些几乎为零,可能不会出现的异常,花费大量代码,并另代码复杂化,可能得不偿失(代码越多,维护越难,出错的概率就越大)。

但在某些特定的场景,我们需要通过SERIALIZABLE来保护。一旦捕获到响应的异常,就重新执行事务的方法。

public viod save(String event){
    try{
        myTransactionService.action(event);
    }catch(Exception e){
        if(Utils.isWantRestartTransaction(e))
            myTransactionService.action(event);
        else
            throw e;
    }
}
public static boolean isWantRestartTransaction(Exception e){
    return e instanceof DataIntegrityViolationException //JPA中同时insert
          || e instanceof CannotAcquireLockException  //JPA SERIALIZABLE的同时update
          || (e.getCause() != null && e.getCause() instanceof ConstraintViolationException) // 原生SQL中同时insert(READ_UNCOMMITTED,READ_COMMITTED,REPEATABLE_READ)
          || (e.getCause() != null && e.getCause() instanceof LockAcquisitionException); //原生SQL SERIALIZABLE的同时insert或update
}
相关链接: 我的Professional Java for Web Applications相关文章

猜你喜欢

转载自blog.csdn.net/flowingflying/article/details/79421831