Spring事务失效场景原理及解决方案

1.事务失效-自身调用(通过REQUIRES、REQUIRES_NEW传播属性):自身调用即调该类自己的方法。
同类OrderServiceImpl 中 doSomeThing()方法 不存在事务,该方法去调用本类中的存在事务注解的 insertAndUpdateOrderInfo() 方法。但是insertAndUpdateOrderInfo() 其实是无法保证预想的事务性。
示列验证:

OrderServiceImpl.insertAndUpdateOrderInfo方法中upateData(updateParam) 发生异常时,insertData(insertParam) 未发生回滚
说明:自身调用时候,无论是以下哪种传播属性均是无效的,因为自身调用时的子方法压根就不会被AOP 代理拦截到以下的这两种方式均经过验证,无法保证子方法事务的有效性

@Transactional(propagation = Propagation.REQUIRES)
@Transactional(propagation = Propagation.REQUIRES_NEW)

@Controller
@RequestMapping("/trans")
public class TransactionalController {

  @Autowired
  OrderService orderService;

  @RequestMapping("/test.do")
  @ResponseBody
  public void getIndex(HttpServletRequest request, HttpServletResponse response, Model model) {

    orderService.doSomeThing();

  }

}

@Service
public interface OrderService {

  /*
  *添加订单和修改其他订单信息
  * */
  public void doSomeThing();

}



@Service
public class OrderServiceImpl implements OrderService {
  @Autowired
  TransBusiness transBusiness;

  @Override
  public void doSomeThing() {

    insertAndUpdateOrderInfo();

  }

  @Transactional(propagation = Propagation.REQUIRED)
  public void insertAndUpdateOrderInfo(){
    Date date = new Date();
    SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
    String updateTime = dateFormat.format(date);

    //步骤1:插入订单记录信息
    String[] insertParam = {"555555555", "977723233", updateTime, updateTime};
    transBusiness.insertData(insertParam);

    //步骤2:修改订单记录信息
    String[] updateParam = {"1111111111", updateTime, "1"};
    transBusiness.upateData(updateParam);
  }

}



@Service
public class TransBusiness {

  @Autowired
  JdbcTemplate dalClient;

  public void insertData(String[] param) {
    Map resultMap = new HashMap<>();
    String sql = "INSERT INTO test_order (`order_no`, `cust_no`,create_time,update_time) VALUES (?, ?,?,?)";

    int i = dalClient.update(sql, param);
    System.out.println("TransBusiness>>>insertData" + i);
    resultMap.put("插入的记录数", i);
  }

  public void upateData(String[] param) {
    Map resultMap = new HashMap<>();
    String sql = "update test_order set order_no =?,update_time=? ? where id= ?";

    int i = dalClient.update(sql, param);
    System.out.println("TransBusiness>>>upateData" + i);
    resultMap.put("修改的记录数", i);
  }

}

2.1自身调用事务失效解决方法1—在父方法中添加事务
通过doSomeThing()方法中添加事务性,可以解决1中事务自身调用失效的问题。

示列验证:

OrderServiceImpl.insertAndUpdateOrderInfo方法中当步骤1执行完成后,数据库中并不会存在该订单记录。当执行步骤2时发生了异常,整个事务发生了回滚。说明才方法解决了1自身调用事务失效的问题。
说明:此处的@Transactional等同于 @Transactional(propagation = Propagation.REQUIRED) 表示支持当前事务,如果没有事务就新建一个事务,这是常见的选择,也是spring默认的事务传播

@Override
  @Transactional(propagation = Propagation.REQUIRED,rollbackFor = Exception.class)
  public void doSomeThing1() {
    insertAndUpdateOrderInfo();
  }

  public void insertAndUpdateOrderInfo(){
    Date date = new Date();
    SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
    String updateTime = dateFormat.format(date);

    //步骤1:插入订单记录信息
    String[] insertParam = {"8888888888", "977723233", updateTime, updateTime};
    transBusiness.insertData(insertParam);

    //步骤2:修改订单记录信息
    String[] updateParam = {"1111111112", updateTime, "1"};
    transBusiness.upateData(updateParam);
  }

2.2自身调用事务失效解决方法2—将事务方法拆分到另外一个类中

@Service
public class TransBusiness {

  @Autowired
  JdbcTemplate dalClient;

  @Transactional(propagation = Propagation.REQUIRED,rollbackFor = Exception.class)
  public void insertAndUpdateOrderInfo(){
    Date date = new Date();
    SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
    String updateTime = dateFormat.format(date);

    //步骤1:插入订单记录信息
    String[] insertParam = {"8888888888", "977723233", updateTime, updateTime};
    insertData(insertParam);

    //步骤2:修改订单记录信息
    String[] updateParam = {"1111111112", updateTime, "1"};
    upateData(updateParam);
  }

}

2.2自身调用事务失效解决方法2—将事务方法拆分到另外一个类中

@Service
public class TransBusiness {

  @Autowired
  JdbcTemplate dalClient;

  @Transactional(propagation = Propagation.REQUIRED,rollbackFor = Exception.class)
  public void insertAndUpdateOrderInfo(){
    Date date = new Date();
    SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
    String updateTime = dateFormat.format(date);

    //步骤1:插入订单记录信息
    String[] insertParam = {"8888888888", "977723233", updateTime, updateTime};
    insertData(insertParam);

    //步骤2:修改订单记录信息
    String[] updateParam = {"1111111112", updateTime, "1"};
    upateData(updateParam);
  }

}

3.SQL规范于1992年提出了数据库事务隔离级别,以此用来保证并发操作数据的正确性及一致性。Mysql的事务隔离级别由低往高可分为以下几类:

  1. READ UNCOMMITTED(读取未提交的数据)

这是最不安全的一种级别,查询语句在无锁的情况下运行,就读取到别的未提交的数据,造成脏读,如果未提交的那个事务数据全部回滚了,而之前读取了这个事务的数据即是脏数据,这种数据不一致性读造成的危害是可想而知的。

  1. READ COMMITTED(读取已提交的数据)

一个事务只能读取数据库中已经提交过的数据,解决了脏读问题,但不能重复读,即一个事务内的两次查询返回的数据是不一样的。如第一次查询金额是100,第二次去查询可能就是50了,这就是不可重复读取。

  1. REPEATABLE READ(可重复读取数据,这也是Mysql默认的隔离级别)

一个事务内的两次无锁查询返回的数据都是一样的,但别的事务的新增数据也能读取到。比如另一个事务插入了一条数据并提交,这个事务第二次去读取的时候发现多了一条之前查询数据列表里面不存在的数据,这时候就是传说的中幻读了。这个级别避免了不可重复读取,但不能避免幻读的问题。

  1. SERIALIZABLE(可串行化读)

这是效率最低最耗费资源的一个事务级别,和可重复读类似,但在自动提交模式关闭情况下可串行化读会给每个查询加上共享锁和排他锁,意味着所有的读操作之间不阻塞,但读操作会阻塞别的事务的写操作,写操作也阻塞读操作。

4.spring事务管理其实是对数据库事务进行了封装而已,并提了5种事务隔离级别和7种事务传播机制。

4.1声明式事务(declarative transaction management)是Spring提供的对程序事务管理的方式之一。Spring使用AOP来完成声明式的事务管理,因而声明式事务是以方法为单位,Spring的事务属性自然就在于描述事务应用至方法上的策略,在Spring中事务属性有以下参数:

image

readOnly属性的详细理解:

1)readonly并不是所有数据库都支持的,不同的数据库下会有不同的结果。

2)设置了readonly后,connection都会被赋予readonly,效果取决于数据库的实现。

a. 在oracle下测试,发现不支持readOnly,也就是不论Connection里的readOnly属性是true还是false均不影响SQL的增删改查;

b. 在mysql下测试,发现支持readOnly,设置为true时,只能查询,若增删改会发生如下异常:

Caused by: java.sql.SQLException: Connection is read-only. Queries leading to data modification are not allowed
at com.mysql.jdbc.SQLError.createSQLException(SQLError.java:910)
at com.mysql.jdbc.PreparedStatement.execute(PreparedStatement.java:792)

3)在ORM中,设置了readonly会赋予一些额外的优化,例如在Hibernate中,会被禁止flush等。

4.2 spring 的 5种事务隔离级别

  1. ISOLATION_DEFAULT     (使用后端数据库默认的隔离级别)

以下四个与JDBC的隔离级别相对应:

  1. ISOLATION_READ_UNCOMMITTED (允许读取尚未提交的更改,可能导致脏读、幻影读或不可重复读)

  2. ISOLATION_READ_COMMITTED (允许从已经提交的并发事务读取,可防止脏读,但幻影读和不可重复读仍可能会发生)

  3. ISOLATION_REPEATABLE_READ (对相同字段的多次读取的结果是一致的,除非数据被当前事务本身改变。可防止脏读和不可重复读,但幻影读仍可能发生)

  4. ISOLATION_SERIALIZABLE (完全服从ACID的隔离级别,确保不发生脏读、不可重复读和幻影读。这在所有隔离级别中也是最慢的,因为它通常是通过完全锁定当前事务所涉及的数据表来完成的)

4.3 spring的7种事务传播机制:

  1. REQUIRED(需要事务): 业务方法需要在一个事务中运行,如果方法运行时,已处在一个事务中,那么就加入该事务,否则自己创建一个新的事务.这是spring默认的传播行为;

  2. NOT_SUPPORTED(不支持事务): 声明方法需要事务,如果方法没有关联到一个事务,容器不会为它开启事务.如果方法在一个事务中被调用,该事务会被挂起,在方法调用结束后,原先的事务便会恢复执行;

  3. REQUIREDS_NEW(需要新事务):业务方法总是会为自己发起一个新的事务,如果方法已运行在一个事务中,则原有事务被挂起,新的事务被创建,直到方法结束,新事务才结束,原先的事务才会恢复执行;
    备注:新建的事务如果没有进行异常捕获,发生异常那么原事务方法也会发生回滚。(该结论经过自测验证)

  4. MANDATORY(强制性事务):只能在一个已存在事务中执行。业务方法不能发起自己的事务,如果业务方法在没有事务的环境下调用,就抛异常

  5. NEVER(不能存在事务):声明方法绝对不能在事务范围内执行,如果方法在某个事务范围内执行,容器就抛异常.只有没关联到事务,才正常执行.

  6. SUPPORTS(支持事务):如果业务方法在某个事务范围内被调用,则方法成为该事务的一部分,如果业务方法在事务范围外被调用,则方法在没有事务的环境下执行.

  7. NESTED(嵌套事务):如果一个活动的事务存在,则运行在一个嵌套的事务中.如果没有活动的事务,则按REQUIRED属性执行.它使用了一个单独的事务,这个事务拥有多个可以回滚的保证点.内部事务回滚不会对外部事务造成影响, 它只对DataSourceTransactionManager 事务管理器起效.

思考:Nested和RequiresNew的区别:

a. RequiresNew每次都创建新的独立的物理事务,而Nested只有一个物理事务;

b. Nested嵌套事务回滚或提交不会导致外部事务回滚或提交,但外部事务回滚将导致嵌套事务回滚,而 RequiresNew由于都是全新的事务,所以之间是无关联的;

c. Nested使用JDBC 3的保存点实现,即如果使用低版本驱动将导致不支持嵌套事务。

实际应用中一般使用默认的事务传播行为,偶尔会用到RequiresNew和Nested方式。


最新2020整理收集的一些高频面试题(都整理成文档),有很多干货,包含mysql,netty,spring,线程,spring cloud、jvm、源码、算法等详细讲解,也有详细的学习规划图,面试题整理等,需要获取这些内容的朋友请加Q君样:11604713672

你可能感兴趣的:(Spring事务失效场景原理及解决方案)