事务模板 VS 声明式事务

事务模板 VS声明式事务

 

一、  案例

我们首先看几个简单的案例。

1.  声明式事务示例一

   @Transactional
   public voidaddData(long orderId,Object orderDetailDTO){
       //查询操作
       OrderDO orderDO = orderManager.getById(orderId);
        // 组装数据操作
       OrderDetailDO orderDetailDO= OrderDetailHelper.create(orderDO, orderDetailDTO);
       
       //db写操作
       orderManager.update(orderDO);
       orderDetailManager.insert(orderDetailDO);
   }



这是一个数据组装和db写操作都放在同一个事务中的示例。

 

2.     声明式事务示例二

public voidaddDataV2(long orderId,Object orderDetailDTO){
       //查询操作
       OrderDO orderDO = orderManager.getById(orderId);
        // 组装数据操作
       OrderDetailDO orderDetailDO= OrderDetailHelper.create(orderDO, orderDetailDTO);
       this.addDataV2DoDB(orderDO, orderDetailDO);
   }
   
    @Transactional
   public voidaddDataV2DoDB(OrderDO orderDO, OrderDetailDOorderDetailDO){
      //db写操作
       orderManager.update(orderDO);
       orderDetailManager.insert(orderDetailDO);
   }

这是一个数据封装和db写操作分在两个方法中的示例,从代码上可以看到事务时间更短。

 

3.     声明式事务示例三

    @Transactional
   public voidaddDataV3(long orderId,Object orderDetailDTO){
       //查询操作
       OrderDO orderDO = orderManager.getById(orderId);
        // 组装数据操作
       OrderDetailDO orderDetailDO= OrderDetailHelper.create(orderDO, orderDetailDTO);
       
       //db写操作
       orderManager.update(orderDO);
       orderDetailManager.insert(orderDetailDO);
       
       orderManager.callback(orderDO);
   }

和示例一唯一的差别就是事务执行完毕以后加了一个回调操作。

 

 

4.     事务模板示例一

  public boolean addData(finalOrderDO orderDO, finalOrderDetailDO orderDetailDO){
        Boolean executeResult = transactionTemplate.execute(new TransactionCallback<Boolean>() {
           @Override
           public BooleandoInTransaction(TransactionStatus status) {
               try {
                   //插入order
                   long orderId = orderDao.insert(orderDO);
                   if (orderId <= 0 ) {
                       status.setRollbackOnly(); 
                       returnfalse;
                   }
                   //插入order detail
                   long orderDetailId = orderDetailDao.insert(orderDetailDO);
                   if (orderDetailId <= 0 ) {
                       status.setRollbackOnly(); 
                       returnfalse;
                   }
               } catch (Exception e) {
                   status.setRollbackOnly();
                   return false;
               }
               return true;
           }
        });
       return executeResult == null ? false : executeResult ;
    }



这是一个使用TransactionTemplate实现事务的示例。可以看到,需要我们手动控制事务的回滚,代码逻辑比较复杂。

 

 

 

二、  案例分析

看了上面的四个示例,估计很多小伙伴很容易得出事务模板是最不应该采用的事务方式,代码繁琐。但我这边给出的结论是,应该放弃声明式事务,使用事务模板。接下来分析一下原因。

1.     声明式事务

1)       示例代码分析

a)   “声明式事务示例一”的问题:事务范围过大,事务时间越长,占用的资源越多,性能越差,事务执行失败回滚的概率越高。

b)   “声明式事务示例二”的问题:事务不会生效。声明式事务依赖于Transactional注解和TransactionInterceptor拦截器。拦截器生效的前提是调用者是代理,而不是当前类本身。

c)   “声明式事务示例三”的问题:“声明式事务示例一”的所有问题,在此示例中都有;另外,业务层可能会进行RPC、HTTP请求,如果多次rpc中部分请求失败,事务会滚也不能保证数据一致性。

 

2)       声明式事务普遍性问题

声明式事务最大的优势是spring做了过多的封装,所以使用起来很简单,代码很简化;但这在使用中也经常会让我们的代码埋下一堆的坑。下面列举平常编码时会遇到的几个例子。

a)   绝大多数我们通过异常来回滚事务,但是如果底层模块捕获异常导致上层无法捕获异常,或者底层接口执行失败时不抛出异常(如update、delete时返回影响的记录数而不抛出异常),此时事务中的代码执行结果将会和我们的预期不一致。之前和不少人沟通过这个问题,但是不少人告诉我通过统一代码规范,来保证上层可以获取到异常,达到事务可以正常回滚的目的,但是从代码上看到的结果基本上都没有做到这一点。

b)   就我这么多年接触的平时喜欢使用@Transactional来做事务的开发同学而言,绝大多数都不清楚事务的5种隔离级别与7种传播属性他们是如何相互影响的。

c)   使用声明式事务的同学一般会将业务逻辑依平铺的方式写下来,举个例子来说,如果某个业务会在3个表中各生成一条记录,那写出来的程序很容易就出现生成一条记录插入一条记录的情况,然后在最外层包一个大事务。这容易造成逻辑不清晰,耦合严重,性能低下的问题,而且很容易在这个事务中写rpc、http调用,即出现“声明式事务示例三”的问题。

 

注意:上面列举的这些问题,在测试期间都不易发现问题,但是出问题后定位起来也比较麻烦。

 

 

2.     事务模板

1)       示例代码分析

从示例中可以看到使用事务模板的方式代码不够简化,但这种不够简化的代码正是我们验证每一步执行结果,判断和我们的预期是否一致引起的,这正是解决声明式事务不可靠(通过异常回滚事务)的关键,为此我们多写几行代码,我觉得是非常值得的事情。

保证数据正确性可以将我们从频繁定位线上问题、修复线上数据的泥潭中解救出来至关重要的一点。

事务模板不能解决事务中调用RPC、HTTP的问题,但是使用事务模板这种方式要求我们提前查询、计算好所有数据,然后放在一个事务模板中,一次更新掉,所以使用事务模板的性能会更好,因为提前已经将数据计算好了,所以事务会尽可能的短,性能会更好,会极大的避免事务中出现RPC、HTTP调用

我一向的观点是:数据是业务的生命,如果数据准确性不能保证,我们提供再多的功能也是没有什么意义的。如果实现成本差别不是非常大,应该尽一切可能保证数据可靠性。

 

 

另外,在传统的单系统的业务中,因为所有的表都在同一个库中,而且基本没有RPC、HTTP调用,所以使用声明式事务很多时候不会有问题,但是在SOA架构中,则绝大多数场景都会有问题。

 

 

三、  传播属性与隔离级别

1.     传播属性

REQUIRED

支持当前事务,如果当前没有事务,就新建一个事务。这是最常见的选择。

SUPPORTS

支持当前事务,如果当前没有事务,就以非事务方式执行。

MANDATORY

支持当前事务,如果当前没有事务,就抛出异常。

REQUIRES_NEW

新建事务,如果当前存在事务,把当前事务挂起。

NOT_SUPPORTED

以非事务方式执行操作,如果当前存在事务,就把当前事务挂起。

NEVER

以非事务方式执行,如果当前存在事务,则抛出异常。

NESTED

如果当前存在事务,则在嵌套事务内执行。如果当前没有事务,则进行与REQUIRED类似的操作

REQUIRED为默认的传播属性。

 

2.     隔离级别

1)       名词解释

a)  脏读:

针对未提交数据。

脏读就是指当一个事务正在访问数据,并且对数据进行了修改,而这种修改还没有提交到数据库中,这时,另外一个事务也访问这个数据,然后使用了这个数据。

b)  不可重复读:

针对其他提交前后,读取数据本身的对比。

是指在一个事务内,多次读同一数据。在这个事务还没有结束时,另外一个事务也访问该同一数据。那么,在第一个事务中的两次读数据之间,由于第二个事务的修改,那么第一个事务两次读到的的数据可能是不一样的。这样就发生了在一个事务内两次读到的数据是不一样的,因此称为是不可重复读。(即不能读到相同的数据内容)

c)  幻读

针对其他提交前后,读取数据条数的对比。

幻读是指同样一笔查询在整个事务过程中多次执行后,查询所得的结果集是不一样的。幻读针对的是多笔记录。在Read Uncommitted隔离级别下, 不管事务2的插入操作是否提交,事务1在插入操作之前和之后执行相同的查询,取得的结果集是不同的,所以,ReadUncommitted同样无法避免幻读的问题。

 

2)       隔离级别

DEFAULT

使用数据库默认的事务隔离级别

READ_UNCOMMITTED

这是事务最低的隔离级别,它充许别外一个事务可以看到这个事务未提交的数据。这种隔离级别会产生脏读,不可重复读和幻像读。

READ_COMMITTED

保证一个事务修改的数据提交后才能被另外一个事务读取。另外一个事务不能读取该事务未提交的数据。这种事务隔离级别可以避免脏读出现,但是可能会出现不可重复读和幻像读。

REPEATABLE_READ

这种事务隔离级别可以防止脏读,不可重复读。但是可能出现幻像读。它除了保证一个事务不能读取另一个事务未提交的数据外,还保证了避免下面的情况产生(不可重复读)

SERIALIZABLE

这是花费最高代价但是最可靠的事务隔离级别。事务被处理为顺序执行。除了防止脏读,不可重复读外,还避免了幻像读。

 

3)       影响

隔离级别 脏读 不可重复读 幻读
READ_UNCOMMITTED
READ_COMMITTED ×
REPEATABLE_READ × ×
SERIALIZABLE × × ×

√: 可能出    ×:不会出


上一篇:文档乱、调试难…TensorFlow有那么多缺点,但为何我们依然待它如初恋?


下一篇:在sublime中使用vim+ctags阅读源码